1 /*****************************************************************************
2  * Copyright (C) 2008 Václav Juza <vaclavjuza@gmail.com>                     *
3  * Copyright (C) 2008-2019 Krusader Krew [https://krusader.org]              *
4  *                                                                           *
5  * This file is part of Krusader [https://krusader.org].                     *
6  *                                                                           *
7  * Krusader is free software: you can redistribute it and/or modify          *
8  * it under the terms of the GNU General Public License as published by      *
9  * the Free Software Foundation, either version 2 of the License, or         *
10  * (at your option) any later version.                                       *
11  *                                                                           *
12  * Krusader is distributed in the hope that it will be useful,               *
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of            *
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             *
15  * GNU General Public License for more details.                              *
16  *                                                                           *
17  * You should have received a copy of the GNU General Public License         *
18  * along with Krusader.  If not, see [http://www.gnu.org/licenses/].         *
19  *****************************************************************************/
20 
21 #include "terminaldock.h"
22 
23 // QtCore
24 #include <QDebug>
25 #include <QDir>
26 #include <QEvent>
27 #include <QObject>
28 #include <QString>
29 #include <QUrl>
30 // QtGui
31 #include <QKeyEvent>
32 #include <QClipboard>
33 // QtWidgets
34 #include <QHBoxLayout>
35 #include <QApplication>
36 #include <QWidget>
37 
38 #include <kde_terminal_interface.h>
39 #include <KCoreAddons/KPluginLoader>
40 #include <KCoreAddons/KPluginFactory>
41 #include <KI18n/KLocalizedString>
42 #include <KService/KService>
43 #include <KWidgetsAddons/KToggleAction>
44 #include <KWidgetsAddons/KMessageBox>
45 
46 #include "kcmdline.h"
47 #include "../kractions.h"
48 #include "../krmainwindow.h"
49 #include "../krservices.h"
50 #include "../krslots.h"
51 #include "../krusaderview.h"
52 #include "../FileSystem/filesystem.h"
53 #include "../Panel/PanelView/krview.h"
54 #include "../Panel/listpanel.h"
55 #include "../Panel/listpanelactions.h"
56 #include "../Panel/panelfunc.h"
57 
58 /**
59  * A widget containing the konsolepart for the Embedded terminal emulator
60  */
TerminalDock(QWidget * parent,KrMainWindow * mainWindow)61 TerminalDock::TerminalDock(QWidget* parent, KrMainWindow *mainWindow) : QWidget(parent),
62     _mainWindow(mainWindow), konsole_part(0), t(0), initialised(false), firstInput(true)
63 {
64     terminal_hbox = new QHBoxLayout(this);
65 }
66 
~TerminalDock()67 TerminalDock::~TerminalDock()
68 {
69 }
70 
initialise()71 bool TerminalDock::initialise()
72 {
73     if (! initialised) { // konsole part is not yet loaded or it has already failed
74         KService::Ptr service = KService::serviceByDesktopName("konsolepart");
75 
76         if (service) {
77             QWidget *focusW = qApp->focusWidget();
78             // Create the part
79             QString error;
80             konsole_part = service->createInstance<KParts::ReadOnlyPart>(this, this, QVariantList(), &error);
81 
82             if (konsole_part) { //loaded successfully
83                 terminal_hbox->addWidget(konsole_part->widget());
84                 setFocusProxy(konsole_part->widget());
85                 connect(konsole_part, &KParts::ReadOnlyPart::destroyed, this,
86                         &TerminalDock::killTerminalEmulator);
87                 // must filter app events, because some of them are processed
88                 // by child widgets of konsole_part->widget()
89                 // and would not be received on konsole_part->widget()
90                 qApp->installEventFilter(this);
91                 t = qobject_cast<TerminalInterface*>(konsole_part);
92                 if (t) {
93                     lastPath = QDir::currentPath();
94                     t->showShellInDir(lastPath);
95                 }
96                 initialised = true;
97                 firstInput = true;
98             } else
99                 KMessageBox::error(0, i18n("<b>Cannot create embedded terminal.</b><br/>"
100                                            "The reported error was: %1", error));
101             // the Terminal Emulator may be hidden (if we are creating it only
102             // to send command there and see the results later)
103             if (focusW) {
104                 focusW->setFocus();
105             } else {
106                 ACTIVE_PANEL->gui->slotFocusOnMe();
107             }
108         } else
109             KMessageBox::sorry(0, i18nc("missing program - arg1 is a URL",
110                                         "<b>Cannot create embedded terminal.</b><br>"
111                                         "You can fix this by installing Konsole:<br/>%1",
112                                         QString("<a href='%1'>%1</a>").arg(
113                                             "http://www.kde.org/applications/system/konsole")),
114                                0, KMessageBox::AllowLink);
115     }
116     return isInitialised();
117 }
118 
killTerminalEmulator()119 void TerminalDock::killTerminalEmulator()
120 {
121     qDebug() << "killed";
122 
123     initialised = false;
124     konsole_part = NULL;
125     t = NULL;
126     qApp->removeEventFilter(this);
127     MAIN_VIEW->setTerminalEmulator(false);
128 }
129 
sendInput(const QString & input,bool clearCommand)130 void TerminalDock::sendInput(const QString& input, bool clearCommand)
131 {
132     if (!t)
133         return;
134 
135     if (clearCommand) {
136         // send SIGINT before input command to avoid unwanted behaviour when current line is not empty
137         // and command is appended to current input (e.g. "rm -rf x " concatenated with 'cd /usr');
138         // code "borrowed" from Dolphin, Copyright (C) 2007-2010 by Peter Penz <peter.penz19@gmail.com>
139         const int processId = t->terminalProcessId();
140         // workaround (firstInput): kill is sent to terminal if shell is not initialized yet
141         if (processId > 0 && !firstInput) {
142             kill(processId, SIGINT);
143         }
144     }
145     firstInput = false;
146 
147     t->sendInput(input);
148 }
149 
150 /*! Sends a `cd` command to the embedded terminal emulator so as to synchronize the directory of the actual panel and
151     the directory of the embedded terminal emulator.
152 
153     To avoid that Krusader's embedded terminal adds a lot of `cd` messages to the shell history: the user has to use
154     bash and have set `HISTCONTROL=ignorespace` or `HISTCONTROL=ignoreboth` (which is the default in a lot of Linux
155     distributions so in that case the user hasn't got to do anything), or the user has to use an equivalent method.
156 */
sendCd(const QString & path)157 void TerminalDock::sendCd(const QString& path)
158 {
159     if (path.compare(lastPath) != 0) {
160         // A space exists in front of the `cd` so as to avoid that Krusader's embedded terminal adds a lot of `cd`
161         // messages to the shell history, in Dolphin it's done the same way: https://bugs.kde.org/show_bug.cgi?id=204039
162         sendInput(QString(" cd ") + KrServices::quote(path) + QString("\n"));
163         lastPath = path;
164     }
165 }
166 
applyShortcuts(QKeyEvent * ke)167 bool TerminalDock::applyShortcuts(QKeyEvent * ke)
168 {
169     int pressedKey = (ke->key() | ke->modifiers());
170 
171     // TODO KF5 removed
172     if (KrActions::actToggleTerminal->shortcut().matches(pressedKey)) {
173         KrActions::actToggleTerminal->activate(QAction::Trigger);
174         return true;
175     }
176 
177     if (!krSwitchFullScreenTE->shortcut().isEmpty() && krSwitchFullScreenTE->shortcut().matches(pressedKey)) {
178         krSwitchFullScreenTE->activate(QAction::Trigger);
179         return true;
180     }
181 
182     if (_mainWindow->listPanelActions()->actPaste->shortcut().matches(pressedKey)) {
183         QString text = QApplication::clipboard()->text();
184         if (! text.isEmpty()) {
185             text.replace('\n', '\r');
186             sendInput(text, false);
187         }
188         return true;
189     }
190 
191     //insert current to the terminal
192     if ((ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return) &&
193         (ke->modifiers() & Qt::ControlModifier)) {
194         SLOTS->insertFileName((ke->modifiers() & Qt::ShiftModifier) != 0);
195         return true;
196     }
197 
198     //navigation
199     if ((ke->key() ==  Qt::Key_Down) && (ke->modifiers() == Qt::ControlModifier)) {
200         if (MAIN_VIEW->cmdLine()->isVisible()) {
201             MAIN_VIEW->cmdLine()->setFocus();
202         }
203         return true;
204     } else if ((ke->key() ==  Qt::Key_Up)
205                && ((ke->modifiers()  == Qt::ControlModifier)
206                    || (ke->modifiers()  == (Qt::ControlModifier | Qt::ShiftModifier)))) {
207         ACTIVE_PANEL->gui->slotFocusOnMe();
208         return true;
209     }
210 
211     return false;
212 }
213 
eventFilter(QObject * watched,QEvent * e)214 bool TerminalDock::eventFilter(QObject * watched, QEvent * e)
215 {
216     if (konsole_part == NULL || konsole_part->widget() == NULL)
217         return false;
218 
219     // we must watch for child widgets as well,
220     // otherwise some shortcuts are "eaten" by them before
221     // being procesed in konsole_part->widget() context
222     // examples are Ctrl+F, Ctrl+Enter
223     QObject *w;
224     for (w = watched; w != NULL; w = w->parent())
225         if (w == konsole_part->widget())
226             break;
227     if (w == NULL)    // is not a child of konsole_part
228         return false;
229 
230     switch (e->type()) {
231     case QEvent::ShortcutOverride: {
232         QKeyEvent *ke = (QKeyEvent *)e;
233         // If not present, some keys would be considered a shortcut, for example "a"
234         if ((ke->key() ==  Qt::Key_Insert) && (ke->modifiers()  == Qt::ShiftModifier)) {
235             ke->accept();
236             return true;
237         }
238         if ((ke->modifiers() == Qt::NoModifier || ke->modifiers() == Qt::ShiftModifier) &&
239                 (ke->key() >= 32) && (ke->key() <= 127)) {
240             ke->accept();
241             return true;
242         }
243         break;
244     }
245     case QEvent::KeyPress: {
246         QKeyEvent *ke = (QKeyEvent *)e;
247         if (applyShortcuts(ke)) {
248             ke->accept();
249             return true;
250         }
251         break;
252     }
253     default:
254         return false;
255     }
256     return false;
257 }
258 
isTerminalVisible() const259 bool TerminalDock::isTerminalVisible() const
260 {
261     return isVisible() && konsole_part != NULL && konsole_part->widget() != NULL
262            && konsole_part->widget()->isVisible();
263 }
264 
isInitialised() const265 bool TerminalDock::isInitialised() const
266 {
267     return konsole_part != NULL && konsole_part->widget() != NULL;
268 }
269 
hideEvent(QHideEvent *)270 void TerminalDock::hideEvent(QHideEvent * /*e*/)
271 {
272     // BUGFIX: when the terminal emulator is toggled on, first it is shown in minimum size
273     //         then QSplitter resizes it to the desired size.
274     //         this minimum resize scrolls up the content of the konsole widget
275     // SOLUTION:
276     //         we hide the console widget while the resize ceremony happens, then reenable it
277     if (konsole_part && konsole_part->widget())
278         konsole_part->widget()->hide(); // hide the widget to prevent from resize
279 }
280 
showEvent(QShowEvent *)281 void TerminalDock::showEvent(QShowEvent * /*e*/)
282 {
283     if (konsole_part && konsole_part->widget()) {
284         // BUGFIX: TE scrolling bug (see upper)
285         //         show the Konsole part delayed
286         QTimer::singleShot(0, konsole_part->widget(), SLOT(show()));
287     }
288 }
289 
290