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 }