File indexing completed on 2024-04-28 17:06:05
0001 /* 0002 SPDX-FileCopyrightText: 2008 Václav Juza <vaclavjuza@gmail.com> 0003 SPDX-FileCopyrightText: 2008-2022 Krusader Krew <https://krusader.org> 0004 SPDX-FileCopyrightText: 2007-2010 Peter Penz <peter.penz19@gmail.com> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "terminaldock.h" 0010 0011 // QtCore 0012 #include <QDebug> 0013 #include <QDir> 0014 #include <QEvent> 0015 #include <QObject> 0016 #include <QString> 0017 #include <QUrl> 0018 // QtGui 0019 #include <QClipboard> 0020 #include <QKeyEvent> 0021 // QtWidgets 0022 #include <QApplication> 0023 #include <QHBoxLayout> 0024 #include <QWidget> 0025 0026 #include <KConfigCore/KConfig> 0027 #include <KConfigCore/KConfigGroup> 0028 #include <KCoreAddons/KPluginFactory> 0029 #include <KCoreAddons/KPluginLoader> 0030 #include <KI18n/KLocalizedString> 0031 #include <KService/KService> 0032 #include <KWidgetsAddons/KMessageBox> 0033 #include <KWidgetsAddons/KToggleAction> 0034 #include <kde_terminal_interface.h> 0035 0036 #include "../FileSystem/filesystem.h" 0037 #include "../Panel/PanelView/krview.h" 0038 #include "../Panel/listpanel.h" 0039 #include "../Panel/listpanelactions.h" 0040 #include "../Panel/panelfunc.h" 0041 #include "../defaults.h" 0042 #include "../kractions.h" 0043 #include "../krmainwindow.h" 0044 #include "../krservices.h" 0045 #include "../krslots.h" 0046 #include "../krusaderview.h" 0047 #include "kcmdline.h" 0048 0049 /** 0050 * A widget containing the konsolepart for the Embedded terminal emulator 0051 */ 0052 TerminalDock::TerminalDock(QWidget *parent, KrMainWindow *mainWindow) 0053 : QWidget(parent) 0054 , _mainWindow(mainWindow) 0055 , konsole_part(nullptr) 0056 , t(nullptr) 0057 , initialised(false) 0058 , firstInput(true) 0059 { 0060 terminal_hbox = new QHBoxLayout(this); 0061 } 0062 0063 TerminalDock::~TerminalDock() = default; 0064 0065 bool TerminalDock::initialise() 0066 { 0067 if (!initialised) { // konsole part is not yet loaded or it has already failed 0068 KService::Ptr service = KService::serviceByDesktopName("konsolepart"); 0069 0070 if (service) { 0071 QWidget *focusW = qApp->focusWidget(); 0072 // Create the part 0073 QString error; 0074 konsole_part = service->createInstance<KParts::ReadOnlyPart>(this, this, QVariantList(), &error); 0075 0076 if (konsole_part) { // loaded successfully 0077 terminal_hbox->addWidget(konsole_part->widget()); 0078 setFocusProxy(konsole_part->widget()); 0079 connect(konsole_part, &KParts::ReadOnlyPart::destroyed, this, &TerminalDock::killTerminalEmulator); 0080 // must filter app events, because some of them are processed 0081 // by child widgets of konsole_part->widget() 0082 // and would not be received on konsole_part->widget() 0083 qApp->installEventFilter(this); 0084 t = qobject_cast<TerminalInterface *>(konsole_part); 0085 if (t) { 0086 lastPath = QDir::currentPath(); 0087 t->showShellInDir(lastPath); 0088 } 0089 initialised = true; 0090 firstInput = true; 0091 } else 0092 KMessageBox::error(nullptr, 0093 i18n("<b>Cannot create embedded terminal.</b><br/>" 0094 "The reported error was: %1", 0095 error)); 0096 // the Terminal Emulator may be hidden (if we are creating it only 0097 // to send command there and see the results later) 0098 if (focusW) { 0099 focusW->setFocus(); 0100 } else { 0101 ACTIVE_PANEL->gui->slotFocusOnMe(); 0102 } 0103 } else 0104 KMessageBox::error(nullptr, 0105 i18nc("missing program - arg1 is a URL", 0106 "<b>Cannot create embedded terminal.</b><br>" 0107 "You can fix this by installing Konsole:<br/>%1", 0108 QString("<a href='%1'>%1</a>").arg("https://www.kde.org/applications/system/konsole")), 0109 nullptr, 0110 KMessageBox::AllowLink); 0111 } 0112 return isInitialised(); 0113 } 0114 0115 void TerminalDock::killTerminalEmulator() 0116 { 0117 qDebug() << "killed"; 0118 0119 initialised = false; 0120 konsole_part = nullptr; 0121 t = nullptr; 0122 qApp->removeEventFilter(this); 0123 MAIN_VIEW->setTerminalEmulator(false); 0124 } 0125 0126 void TerminalDock::sendInput(const QString &input, bool clearCommand) 0127 { 0128 if (!t) 0129 return; 0130 0131 if (clearCommand) { 0132 // send SIGINT before input command to avoid unwanted behaviour when current line is not empty 0133 // and command is appended to current input (e.g. "rm -rf x " concatenated with 'cd /usr'); 0134 // code "borrowed" from Dolphin 0135 const int processId = t->terminalProcessId(); 0136 // workaround (firstInput): kill is sent to terminal if shell is not initialized yet 0137 if (processId > 0 && !firstInput) { 0138 kill(processId, SIGINT); 0139 } 0140 } 0141 firstInput = false; 0142 0143 t->sendInput(input); 0144 } 0145 0146 /*! Sends a `cd` command to the embedded terminal emulator so as to synchronize the directory of the actual panel and 0147 the directory of the embedded terminal emulator. 0148 0149 To avoid that Krusader's embedded terminal adds a lot of `cd` messages to the shell history: the user has to use 0150 bash and have set `HISTCONTROL=ignorespace` or `HISTCONTROL=ignoreboth` (which is the default in a lot of Linux 0151 distributions so in that case the user hasn't got to do anything), or the user has to use an equivalent method. 0152 */ 0153 void TerminalDock::sendCd(const QString &path) 0154 { 0155 if (path.compare(lastPath) != 0) { 0156 // A space exists in front of the `cd` so as to avoid that Krusader's embedded terminal adds a lot of `cd` 0157 // messages to the shell history, in Dolphin it's done the same way: https://bugs.kde.org/show_bug.cgi?id=204039 0158 sendInput(QString(" cd ") + KrServices::quote(path) + QString("\n")); 0159 lastPath = path; 0160 } 0161 } 0162 0163 bool TerminalDock::applyShortcuts(QKeyEvent *ke) 0164 { 0165 int pressedKey = (ke->key() | ke->modifiers()); 0166 0167 // TODO KF5 removed 0168 if (KrActions::actToggleTerminal->shortcut().matches(pressedKey)) { 0169 KrActions::actToggleTerminal->activate(QAction::Trigger); 0170 return true; 0171 } 0172 0173 if (!krSwitchFullScreenTE->shortcut().isEmpty() && krSwitchFullScreenTE->shortcut().matches(pressedKey)) { 0174 krSwitchFullScreenTE->activate(QAction::Trigger); 0175 return true; 0176 } 0177 0178 if (_mainWindow->listPanelActions()->actPaste->shortcuts().contains(pressedKey)) { 0179 QString text = QApplication::clipboard()->text(); 0180 if (!text.isEmpty()) { 0181 text.replace('\n', '\r'); 0182 sendInput(text, false); 0183 } 0184 return true; 0185 } 0186 0187 // insert current to the terminal 0188 if ((ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return) && (ke->modifiers() & Qt::ControlModifier)) { 0189 SLOTS->insertFileName((ke->modifiers() & Qt::ShiftModifier) != 0); 0190 return true; 0191 } 0192 0193 // navigation 0194 if ((ke->key() == Qt::Key_Down) && (ke->modifiers() == Qt::ControlModifier)) { 0195 if (MAIN_VIEW->cmdLine()->isVisible()) { 0196 MAIN_VIEW->cmdLine()->setFocus(); 0197 } 0198 return true; 0199 } else if ((ke->key() == Qt::Key_Up) && ((ke->modifiers() == Qt::ControlModifier) || (ke->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier)))) { 0200 ACTIVE_PANEL->gui->slotFocusOnMe(); 0201 return true; 0202 } 0203 0204 return false; 0205 } 0206 0207 bool TerminalDock::eventFilter(QObject *watched, QEvent *e) 0208 { 0209 if (konsole_part == nullptr || konsole_part->widget() == nullptr) 0210 return false; 0211 0212 // we must watch for child widgets as well, 0213 // otherwise some shortcuts are "eaten" by them before 0214 // being processed in konsole_part->widget() context 0215 // examples are Ctrl+F, Ctrl+Enter 0216 QObject *w; 0217 for (w = watched; w != nullptr; w = w->parent()) 0218 if (w == konsole_part->widget()) 0219 break; 0220 if (w == nullptr) // is not a child of konsole_part 0221 return false; 0222 0223 switch (e->type()) { 0224 case QEvent::ShortcutOverride: { 0225 auto *ke = dynamic_cast<QKeyEvent *>(e); 0226 // If not present, some keys would be considered a shortcut, for example "a" 0227 if ((ke->key() == Qt::Key_Insert) && (ke->modifiers() == Qt::ShiftModifier)) { 0228 ke->accept(); 0229 return true; 0230 } 0231 if ((ke->modifiers() == Qt::NoModifier || ke->modifiers() == Qt::ShiftModifier) && (ke->key() >= 32) && (ke->key() <= 127)) { 0232 ke->accept(); 0233 return true; 0234 } 0235 break; 0236 } 0237 case QEvent::KeyPress: { 0238 auto *ke = dynamic_cast<QKeyEvent *>(e); 0239 if (applyShortcuts(ke)) { 0240 ke->accept(); 0241 return true; 0242 } 0243 break; 0244 } 0245 case QEvent::FocusIn: 0246 case QEvent::FocusOut: { 0247 onTerminalFocusChanged(e->type() == QEvent::FocusIn); 0248 break; 0249 } 0250 default: 0251 return false; 0252 } 0253 return false; 0254 } 0255 0256 bool TerminalDock::isTerminalVisible() const 0257 { 0258 return isVisible() && konsole_part != nullptr && konsole_part->widget() != nullptr && konsole_part->widget()->isVisible(); 0259 } 0260 0261 bool TerminalDock::isInitialised() const 0262 { 0263 return konsole_part != nullptr && konsole_part->widget() != nullptr; 0264 } 0265 0266 void TerminalDock::hideEvent(QHideEvent * /*e*/) 0267 { 0268 // BUGFIX: when the terminal emulator is toggled on, first it is shown in minimum size 0269 // then QSplitter resizes it to the desired size. 0270 // this minimum resize scrolls up the content of the konsole widget 0271 // SOLUTION: 0272 // we hide the console widget while the resize ceremony happens, then reenable it 0273 if (konsole_part && konsole_part->widget()) 0274 konsole_part->widget()->hide(); // hide the widget to prevent from resize 0275 } 0276 0277 void TerminalDock::showEvent(QShowEvent * /*e*/) 0278 { 0279 if (konsole_part && konsole_part->widget()) { 0280 // BUGFIX: TE scrolling bug (see upper) 0281 // show the Konsole part delayed 0282 QTimer::singleShot(0, konsole_part->widget(), &QWidget::show); 0283 } 0284 } 0285 0286 void TerminalDock::onTerminalFocusChanged(bool focused) 0287 { 0288 if (konsole_part == nullptr) { 0289 return; 0290 } 0291 0292 if (!focused) { 0293 disconnect(konsole_part, SIGNAL(currentDirectoryChanged(QString)), nullptr, nullptr); 0294 return; 0295 } 0296 0297 const KConfigGroup cfg = krConfig->group("General"); 0298 if (!cfg.readEntry("Follow Terminal CD", _FollowTerminalCD)) { 0299 return; 0300 } 0301 0302 connect(konsole_part, SIGNAL(currentDirectoryChanged(QString)), this, SLOT(currentDirChanged(QString))); 0303 } 0304 0305 void TerminalDock::currentDirChanged(const QString &terminalPath) 0306 { 0307 const QString panelPath = ACTIVE_PANEL->virtualPath().toLocalFile(); 0308 if (panelPath == terminalPath) { 0309 return; 0310 } 0311 0312 qDebug() << "terminal cwd changed=" << terminalPath << "panelPath=" << panelPath; 0313 lastPath = terminalPath; 0314 _mainWindow->activePanel()->func->openUrl(QUrl::fromLocalFile(terminalPath)); 0315 }