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