1 /*
2     SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>
3     SPDX-FileCopyrightText: 2009 Thomas Dreibholz <dreibh@iem.uni-due.de>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 // Own
9 #include "SessionController.h"
10 
11 #include "konsoledebug.h"
12 #include "profile/ProfileManager.h"
13 #include "terminalDisplay/TerminalColor.h"
14 #include "terminalDisplay/TerminalFonts.h"
15 
16 // Qt
17 #include <QAction>
18 #include <QApplication>
19 #include <QFileDialog>
20 #include <QIcon>
21 #include <QKeyEvent>
22 #include <QList>
23 #include <QMenu>
24 #include <QPainter>
25 #include <QStandardPaths>
26 #include <QUrl>
27 
28 // KDE
29 #include <KActionCollection>
30 #include <KActionMenu>
31 #include <KCodecAction>
32 #include <KConfigGroup>
33 #include <KLocalizedString>
34 #include <KMessageBox>
35 #include <KNotification>
36 #include <KSelectAction>
37 #include <KSharedConfig>
38 #include <KShell>
39 #include <KStringHandler>
40 #include <KToggleAction>
41 #include <KUriFilter>
42 #include <KXMLGUIBuilder>
43 #include <KXMLGUIFactory>
44 #include <KXmlGuiWindow>
45 
46 #include <KIO/CommandLauncherJob>
47 
48 #include <KIO/JobUiDelegate>
49 #include <KIO/OpenUrlJob>
50 
51 #include <kconfigwidgets_version.h>
52 #include <kwidgetsaddons_version.h>
53 
54 // Konsole
55 #include "CopyInputDialog.h"
56 #include "Emulation.h"
57 #include "HistorySizeDialog.h"
58 #include "RenameTabDialog.h"
59 #include "SaveHistoryTask.h"
60 #include "ScreenWindow.h"
61 #include "SearchHistoryTask.h"
62 
63 #include "filterHotSpots/ColorFilter.h"
64 #include "filterHotSpots/EscapeSequenceUrlFilter.h"
65 #include "filterHotSpots/FileFilter.h"
66 #include "filterHotSpots/Filter.h"
67 #include "filterHotSpots/FilterChain.h"
68 #include "filterHotSpots/HotSpot.h"
69 #include "filterHotSpots/RegExpFilter.h"
70 #include "filterHotSpots/UrlFilter.h"
71 
72 #include "history/HistoryType.h"
73 #include "history/HistoryTypeFile.h"
74 #include "history/HistoryTypeNone.h"
75 #include "history/compact/CompactHistoryType.h"
76 
77 #include "profile/ProfileList.h"
78 
79 #include "SessionGroup.h"
80 #include "SessionManager.h"
81 
82 #include "widgets/EditProfileDialog.h"
83 #include "widgets/IncrementalSearchBar.h"
84 
85 #include "terminalDisplay/TerminalColor.h"
86 #include "terminalDisplay/TerminalDisplay.h"
87 
88 // For Unix signal names
89 #include <csignal>
90 
91 using namespace Konsole;
92 
93 QSet<SessionController *> SessionController::_allControllers;
94 int SessionController::_lastControllerId;
95 
SessionController(Session * sessionParam,TerminalDisplay * viewParam,QObject * parent)96 SessionController::SessionController(Session *sessionParam, TerminalDisplay *viewParam, QObject *parent)
97     : ViewProperties(parent)
98     , KXMLGUIClient()
99     , _copyToGroup(nullptr)
100     , _profileList(nullptr)
101     , _sessionIcon(QIcon())
102     , _sessionIconName(QString())
103     , _searchFilter(nullptr)
104     , _urlFilter(nullptr)
105     , _fileFilter(nullptr)
106     , _colorFilter(nullptr)
107     , _copyInputToAllTabsAction(nullptr)
108     , _findAction(nullptr)
109     , _findNextAction(nullptr)
110     , _findPreviousAction(nullptr)
111     , _interactionTimer(nullptr)
112     , _searchStartLine(0)
113     , _prevSearchResultLine(0)
114     , _codecAction(nullptr)
115     , _switchProfileMenu(nullptr)
116     , _webSearchMenu(nullptr)
117     , _listenForScreenWindowUpdates(false)
118     , _preventClose(false)
119     , _selectedText(QString())
120     , _showMenuAction(nullptr)
121     , _bookmarkValidProgramsToClear(QStringList())
122     , _isSearchBarEnabled(false)
123     , _searchBar(viewParam->searchBar())
124     , _monitorProcessFinish(false)
125     , _escapedUrlFilter(nullptr)
126 {
127     Q_ASSERT(sessionParam);
128     Q_ASSERT(viewParam);
129 
130     _sessionDisplayConnection = new SessionDisplayConnection(sessionParam, viewParam, this);
131     viewParam->setSessionController(this);
132 
133     // handle user interface related to session (menus etc.)
134     if (isKonsolePart()) {
135         setComponentName(QStringLiteral("konsole"), i18n("Konsole"));
136         setXMLFile(QStringLiteral("partui.rc"));
137         setupCommonActions();
138     } else {
139         setXMLFile(QStringLiteral("sessionui.rc"));
140         setupCommonActions();
141         setupExtraActions();
142     }
143 
144     connect(this, &SessionController::requestPrint, view(), &TerminalDisplay::printScreen);
145 
146     actionCollection()->addAssociatedWidget(viewParam);
147 
148     const QList<QAction *> actionsList = actionCollection()->actions();
149     for (QAction *action : actionsList) {
150         action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
151     }
152 
153     setIdentifier(++_lastControllerId);
154     sessionAttributeChanged();
155 
156     connect(view(), &TerminalDisplay::compositeFocusChanged, this, &SessionController::viewFocusChangeHandler);
157 
158     // install filter on the view to highlight URLs and files
159     updateFilterList(SessionManager::instance()->sessionProfile(session()));
160 
161     // listen for changes in session, we might need to change the enabled filters
162     connect(ProfileManager::instance(), &Konsole::ProfileManager::profileChanged, this, &Konsole::SessionController::updateFilterList);
163 
164     // listen for session resize requests
165     connect(session(), &Konsole::Session::resizeRequest, this, &Konsole::SessionController::sessionResizeRequest);
166 
167     // listen for popup menu requests
168     connect(view(), &Konsole::TerminalDisplay::configureRequest, this, &Konsole::SessionController::showDisplayContextMenu);
169 
170     // move view to newest output when keystrokes occur
171     connect(view(), &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::SessionController::trackOutput);
172 
173     // listen to activity / silence notifications from session
174     connect(session(), &Konsole::Session::notificationsChanged, this, &Konsole::SessionController::sessionNotificationsChanged);
175     // listen to title and icon changes
176     connect(session(), &Konsole::Session::sessionAttributeChanged, this, &Konsole::SessionController::sessionAttributeChanged);
177     connect(session(), &Konsole::Session::readOnlyChanged, this, &Konsole::SessionController::sessionReadOnlyChanged);
178 
179     connect(this, &Konsole::SessionController::tabRenamedByUser, session(), &Konsole::Session::tabTitleSetByUser);
180     connect(this, &Konsole::SessionController::tabColoredByUser, session(), &Konsole::Session::tabColorSetByUser);
181 
182     connect(session(), &Konsole::Session::currentDirectoryChanged, this, &Konsole::SessionController::currentDirectoryChanged);
183 
184     // listen for color changes
185     connect(session(), &Konsole::Session::changeBackgroundColorRequest, view()->terminalColor(), &TerminalColor::setBackgroundColor);
186     connect(session(), &Konsole::Session::changeForegroundColorRequest, view()->terminalColor(), &TerminalColor::setForegroundColor);
187 
188     // update the title when the session starts
189     connect(session(), &Konsole::Session::started, this, &Konsole::SessionController::snapshot);
190 
191     // listen for output changes to set activity flag
192     connect(session()->emulation(), &Konsole::Emulation::outputChanged, this, &Konsole::SessionController::fireActivity);
193 
194     // listen for detection of ZModem transfer
195     connect(session(), &Konsole::Session::zmodemDownloadDetected, this, &Konsole::SessionController::zmodemDownload);
196     connect(session(), &Konsole::Session::zmodemUploadDetected, this, &Konsole::SessionController::zmodemUpload);
197 
198     // listen for flow control status changes
199     connect(session(), &Konsole::Session::flowControlEnabledChanged, view(), &Konsole::TerminalDisplay::setFlowControlWarningEnabled);
200     view()->setFlowControlWarningEnabled(session()->flowControlEnabled());
201 
202     // take a snapshot of the session state every so often when
203     // user activity occurs
204     //
205     // the timer is owned by the session so that it will be destroyed along
206     // with the session
207     _interactionTimer = new QTimer(session());
208     _interactionTimer->setSingleShot(true);
209     _interactionTimer->setInterval(500);
210     connect(_interactionTimer, &QTimer::timeout, this, &Konsole::SessionController::snapshot);
211     connect(view(), &Konsole::TerminalDisplay::compositeFocusChanged, this, [this](bool focused) {
212         if (focused) {
213             interactionHandler();
214         }
215     });
216     connect(view(), &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::SessionController::interactionHandler);
217 
218     // take a snapshot of the session state periodically in the background
219     auto backgroundTimer = new QTimer(session());
220     backgroundTimer->setSingleShot(false);
221     backgroundTimer->setInterval(2000);
222     connect(backgroundTimer, &QTimer::timeout, this, &Konsole::SessionController::snapshot);
223     backgroundTimer->start();
224 
225     // xterm '10;?' request
226     connect(session(), &Konsole::Session::getForegroundColor, this, &Konsole::SessionController::sendForegroundColor);
227     // xterm '11;?' request
228     connect(session(), &Konsole::Session::getBackgroundColor, this, &Konsole::SessionController::sendBackgroundColor);
229 
230     _allControllers.insert(this);
231 
232     // A list of programs that accept Ctrl+C to clear command line used
233     // before outputting bookmark.
234     _bookmarkValidProgramsToClear =
235         QStringList({QStringLiteral("bash"), QStringLiteral("fish"), QStringLiteral("sh"), QStringLiteral("tcsh"), QStringLiteral("zsh")});
236 
237     setupSearchBar();
238     _searchBar->setVisible(_isSearchBarEnabled);
239 }
240 
~SessionController()241 SessionController::~SessionController()
242 {
243     _allControllers.remove(this);
244 
245     if (factory() != nullptr) {
246         factory()->removeClient(this);
247     }
248 }
trackOutput(QKeyEvent * event)249 void SessionController::trackOutput(QKeyEvent *event)
250 {
251     Q_ASSERT(view()->screenWindow());
252 
253     // Qt has broken something, so we can't rely on just checking if certain
254     // keys are passed as modifiers anymore.
255     const int key = event->key();
256 
257     /* clang-format off */
258     const bool shouldNotTriggerScroll =
259         key == Qt::Key_Super_L
260         || key == Qt::Key_Super_R
261         || key == Qt::Key_Hyper_L
262         || key == Qt::Key_Hyper_R
263         || key == Qt::Key_Shift
264         || key == Qt::Key_Control
265         || key == Qt::Key_Meta
266         || key == Qt::Key_Alt
267         || key == Qt::Key_AltGr
268         || key == Qt::Key_CapsLock
269         || key == Qt::Key_NumLock
270         || key == Qt::Key_ScrollLock;
271     /* clang-format on */
272 
273     // Only jump to the bottom if the user actually typed something in,
274     // not if the user e. g. just pressed a modifier.
275     if (event->text().isEmpty() && ((event->modifiers() != 0u) || shouldNotTriggerScroll)) {
276         return;
277     }
278 
279     view()->screenWindow()->setTrackOutput(true);
280 }
281 
viewFocusChangeHandler(bool focused)282 void SessionController::viewFocusChangeHandler(bool focused)
283 {
284     if (focused) {
285         // notify the world that the view associated with this session has been focused
286         // used by the view manager to update the title of the MainWindow widget containing the view
287         Q_EMIT viewFocused(this);
288 
289         // when the view is focused, set bell events from the associated session to be delivered
290         // by the focused view
291 
292         // first, disconnect any other views which are listening for bell signals from the session
293         disconnect(session(), &Konsole::Session::bellRequest, nullptr, nullptr);
294         // second, connect the newly focused view to listen for the session's bell signal
295         connect(session(), &Konsole::Session::bellRequest, view(), &Konsole::TerminalDisplay::bell);
296 
297         if ((_copyInputToAllTabsAction != nullptr) && _copyInputToAllTabsAction->isChecked()) {
298             // A session with "Copy To All Tabs" has come into focus:
299             // Ensure that newly created sessions are included in _copyToGroup!
300             copyInputToAllTabs();
301         }
302     }
303 }
304 
interactionHandler()305 void SessionController::interactionHandler()
306 {
307     _interactionTimer->start();
308 }
309 
snapshot()310 void SessionController::snapshot()
311 {
312     Q_ASSERT(!session().isNull());
313 
314     QString title = session()->getDynamicTitle();
315     title = title.simplified();
316 
317     // Visualize that the session is broadcasting to others
318     if ((_copyToGroup != nullptr) && _copyToGroup->sessions().count() > 1) {
319         title.append(QLatin1Char('*'));
320     }
321 
322     // use the fallback title if needed
323     if (title.isEmpty()) {
324         title = session()->title(Session::NameRole);
325     }
326 
327     QColor color = session()->color();
328     // use the fallback color if needed
329     if (!color.isValid()) {
330         color = QColor(QColor::Invalid);
331     }
332 
333     // apply new title
334     session()->setTitle(Session::DisplayedTitleRole, title);
335 
336     // apply new color
337     session()->setColor(color);
338 
339     // check if foreground process ended and notify if this option was requested
340     if (_monitorProcessFinish) {
341         bool isForegroundProcessActive = session()->isForegroundProcessActive();
342         if (!_previousForegroundProcessName.isNull() && !isForegroundProcessActive) {
343             KNotification::event(session()->hasFocus() ? QStringLiteral("ProcessFinished") : QStringLiteral("ProcessFinishedHidden"),
344                                  i18n("The process '%1' has finished running in session '%2'", _previousForegroundProcessName, session()->nameTitle()),
345                                  QPixmap(),
346                                  QApplication::activeWindow(),
347                                  KNotification::CloseWhenWidgetActivated);
348         }
349         _previousForegroundProcessName = isForegroundProcessActive ? session()->foregroundProcessName() : QString();
350     }
351 
352     // do not forget icon
353     updateSessionIcon();
354 }
355 
currentDir() const356 QString SessionController::currentDir() const
357 {
358     return session()->currentWorkingDirectory();
359 }
360 
url() const361 QUrl SessionController::url() const
362 {
363     return session()->getUrl();
364 }
365 
rename()366 void SessionController::rename()
367 {
368     renameSession();
369 }
370 
openUrl(const QUrl & url)371 void SessionController::openUrl(const QUrl &url)
372 {
373     // Clear shell's command line
374     if (!session()->isForegroundProcessActive() && _bookmarkValidProgramsToClear.contains(session()->foregroundProcessName())) {
375         session()->sendTextToTerminal(QChar(0x03), QLatin1Char('\n')); // Ctrl+C
376     }
377 
378     // handle local paths
379     if (url.isLocalFile()) {
380         QString path = url.toLocalFile();
381         session()->sendTextToTerminal(QStringLiteral("cd ") + KShell::quoteArg(path), QLatin1Char('\r'));
382     } else if (url.scheme().isEmpty()) {
383         // QUrl couldn't parse what the user entered into the URL field
384         // so just dump it to the shell
385         // If you change this, change it also in autotests/BookMarkTest.cpp
386         QString command = QUrl::fromPercentEncoding(url.toEncoded());
387         if (!command.isEmpty()) {
388             session()->sendTextToTerminal(command, QLatin1Char('\r'));
389         }
390     } else if (url.scheme() == QLatin1String("ssh")) {
391         QString sshCommand = QStringLiteral("ssh ");
392 
393         if (url.port() > -1) {
394             sshCommand += QStringLiteral("-p %1 ").arg(url.port());
395         }
396         if (!url.userName().isEmpty()) {
397             sshCommand += (url.userName() + QLatin1Char('@'));
398         }
399         if (!url.host().isEmpty()) {
400             sshCommand += url.host();
401         }
402         session()->sendTextToTerminal(sshCommand, QLatin1Char('\r'));
403 
404     } else if (url.scheme() == QLatin1String("telnet")) {
405         QString telnetCommand = QStringLiteral("telnet ");
406 
407         if (!url.userName().isEmpty()) {
408             telnetCommand += QStringLiteral("-l %1 ").arg(url.userName());
409         }
410         if (!url.host().isEmpty()) {
411             telnetCommand += (url.host() + QLatin1Char(' '));
412         }
413         if (url.port() > -1) {
414             telnetCommand += QString::number(url.port());
415         }
416 
417         session()->sendTextToTerminal(telnetCommand, QLatin1Char('\r'));
418 
419     } else {
420         // TODO Implement handling for other Url types
421 
422         KMessageBox::sorry(view()->window(), i18n("Konsole does not know how to open the bookmark: ") + url.toDisplayString());
423 
424         qCDebug(KonsoleDebug) << "Unable to open bookmark at url" << url << ", I do not know"
425                               << " how to handle the protocol " << url.scheme();
426     }
427 }
428 
setupPrimaryScreenSpecificActions(bool use)429 void SessionController::setupPrimaryScreenSpecificActions(bool use)
430 {
431     KActionCollection *collection = actionCollection();
432     QAction *clearAction = collection->action(QStringLiteral("clear-history"));
433     QAction *resetAction = collection->action(QStringLiteral("clear-history-and-reset"));
434     QAction *selectAllAction = collection->action(QStringLiteral("select-all"));
435     QAction *selectLineAction = collection->action(QStringLiteral("select-line"));
436 
437     // these actions are meaningful only when primary screen is used.
438     clearAction->setEnabled(use);
439     resetAction->setEnabled(use);
440     selectAllAction->setEnabled(use);
441     selectLineAction->setEnabled(use);
442 }
443 
selectionChanged(const QString & selectedText)444 void SessionController::selectionChanged(const QString &selectedText)
445 {
446     _selectedText = selectedText;
447     updateCopyAction(selectedText);
448 }
449 
updateCopyAction(const QString & selectedText)450 void SessionController::updateCopyAction(const QString &selectedText)
451 {
452     QAction *copyAction = actionCollection()->action(QStringLiteral("edit_copy"));
453     QAction *copyContextMenu = actionCollection()->action(QStringLiteral("edit_copy_contextmenu"));
454     // copy action is meaningful only when some text is selected.
455     copyAction->setEnabled(!selectedText.isEmpty());
456     copyContextMenu->setVisible(!selectedText.isEmpty());
457 }
458 
updateWebSearchMenu()459 void SessionController::updateWebSearchMenu()
460 {
461     // reset
462     _webSearchMenu->setVisible(false);
463     _webSearchMenu->menu()->clear();
464 
465     if (_selectedText.isEmpty()) {
466         return;
467     }
468 
469     QString searchText = _selectedText;
470     searchText = searchText.replace(QLatin1Char('\n'), QLatin1Char(' ')).replace(QLatin1Char('\r'), QLatin1Char(' ')).simplified();
471 
472     if (searchText.isEmpty()) {
473         return;
474     }
475 
476     // Is 'Enable Web shortcuts' checked in System Settings?
477     KSharedConfigPtr kuriikwsConfig = KSharedConfig::openConfig(QStringLiteral("kuriikwsfilterrc"));
478     if (!kuriikwsConfig->group("General").readEntry("EnableWebShortcuts", true)) {
479         return;
480     }
481 
482     KUriFilterData filterData(searchText);
483     filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly);
484 
485     if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) {
486         const QStringList searchProviders = filterData.preferredSearchProviders();
487         if (!searchProviders.isEmpty()) {
488             _webSearchMenu->setText(i18n("Search for '%1' with", KStringHandler::rsqueeze(searchText, 16)));
489 
490             QAction *action = nullptr;
491 
492             for (const QString &searchProvider : searchProviders) {
493                 action = new QAction(searchProvider, _webSearchMenu);
494                 action->setIcon(QIcon::fromTheme(filterData.iconNameForPreferredSearchProvider(searchProvider)));
495                 action->setData(filterData.queryForPreferredSearchProvider(searchProvider));
496                 connect(action, &QAction::triggered, this, &Konsole::SessionController::handleWebShortcutAction);
497                 _webSearchMenu->addAction(action);
498             }
499 
500             _webSearchMenu->addSeparator();
501 
502             action = new QAction(i18n("Configure Web Shortcuts..."), _webSearchMenu);
503             action->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
504             connect(action, &QAction::triggered, this, &Konsole::SessionController::configureWebShortcuts);
505             _webSearchMenu->addAction(action);
506 
507             _webSearchMenu->setVisible(true);
508         }
509     }
510 }
511 
handleWebShortcutAction()512 void SessionController::handleWebShortcutAction()
513 {
514     auto *action = qobject_cast<QAction *>(sender());
515     if (action == nullptr) {
516         return;
517     }
518 
519     KUriFilterData filterData(action->data().toString());
520 
521     if (KUriFilter::self()->filterUri(filterData, {QStringLiteral("kurisearchfilter")})) {
522         const QUrl url = filterData.uri();
523 
524         auto *job = new KIO::OpenUrlJob(url);
525         job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, QApplication::activeWindow()));
526         job->start();
527     }
528 }
529 
configureWebShortcuts()530 void SessionController::configureWebShortcuts()
531 {
532     auto job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell5"), {QStringLiteral("webshortcuts")});
533     job->start();
534 }
535 
sendSignal(QAction * action)536 void SessionController::sendSignal(QAction *action)
537 {
538     const auto signal = action->data().toInt();
539     session()->sendSignal(signal);
540 }
541 
sendForegroundColor(uint terminator)542 void SessionController::sendForegroundColor(uint terminator)
543 {
544     const QColor c = view()->terminalColor()->foregroundColor();
545     session()->reportForegroundColor(c, terminator);
546 }
547 
sendBackgroundColor(uint terminator)548 void Konsole::SessionController::sendBackgroundColor(uint terminator)
549 {
550     const QColor c = view()->terminalColor()->backgroundColor();
551     session()->reportBackgroundColor(c, terminator);
552 }
553 
toggleReadOnly()554 void SessionController::toggleReadOnly()
555 {
556     auto *action = qobject_cast<QAction *>(sender());
557     if (action != nullptr) {
558         bool readonly = !isReadOnly();
559         session()->setReadOnly(readonly);
560     }
561 }
562 
removeSearchFilter()563 void SessionController::removeSearchFilter()
564 {
565     if (_searchFilter == nullptr) {
566         return;
567     }
568 
569     view()->filterChain()->removeFilter(_searchFilter);
570     delete _searchFilter;
571     _searchFilter = nullptr;
572 }
573 
setupSearchBar()574 void SessionController::setupSearchBar()
575 {
576     connect(_searchBar, &Konsole::IncrementalSearchBar::unhandledMovementKeyPressed, this, &Konsole::SessionController::movementKeyFromSearchBarReceived);
577     connect(_searchBar, &Konsole::IncrementalSearchBar::closeClicked, this, &Konsole::SessionController::searchClosed);
578     connect(_searchBar, &Konsole::IncrementalSearchBar::searchFromClicked, this, &Konsole::SessionController::searchFrom);
579     connect(_searchBar, &Konsole::IncrementalSearchBar::findNextClicked, this, &Konsole::SessionController::findNextInHistory);
580     connect(_searchBar, &Konsole::IncrementalSearchBar::findPreviousClicked, this, &Konsole::SessionController::findPreviousInHistory);
581     connect(_searchBar, &Konsole::IncrementalSearchBar::reverseSearchToggled, this, &Konsole::SessionController::updateMenuIconsAccordingToReverseSearchSetting);
582     connect(_searchBar, &Konsole::IncrementalSearchBar::highlightMatchesToggled, this, &Konsole::SessionController::highlightMatches);
583     connect(_searchBar, &Konsole::IncrementalSearchBar::matchCaseToggled, this, &Konsole::SessionController::changeSearchMatch);
584     connect(_searchBar, &Konsole::IncrementalSearchBar::matchRegExpToggled, this, &Konsole::SessionController::changeSearchMatch);
585 
586     updateMenuIconsAccordingToReverseSearchSetting();
587 }
588 
setShowMenuAction(QAction * action)589 void SessionController::setShowMenuAction(QAction *action)
590 {
591     _showMenuAction = action;
592 }
593 
setupCommonActions()594 void SessionController::setupCommonActions()
595 {
596     KActionCollection *collection = actionCollection();
597 
598     // Close Session
599     QAction *action = collection->addAction(QStringLiteral("close-session"), this, &SessionController::closeSession);
600     action->setText(i18n("&Close Session"));
601 
602     action->setIcon(QIcon::fromTheme(QStringLiteral("tab-close")));
603     collection->setDefaultShortcut(action, Konsole::ACCEL | Qt::SHIFT | Qt::Key_W);
604 
605     // Open Browser
606     action = collection->addAction(QStringLiteral("open-browser"), this, &SessionController::openBrowser);
607     action->setText(i18n("Open File Manager"));
608     action->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager")));
609 
610     // Copy and Paste
611     action = KStandardAction::copy(this, &SessionController::copy, collection);
612 #ifdef Q_OS_MACOS
613     // Don't use the Konsole::ACCEL const here, we really want the Command key (Qt::META)
614     // TODO: check what happens if we leave it to Qt to assign the default?
615     collection->setDefaultShortcut(action, Qt::META | Qt::Key_C);
616 #else
617     collection->setDefaultShortcut(action, Konsole::ACCEL | Qt::SHIFT | Qt::Key_C);
618 #endif
619     // disabled at first, since nothing has been selected now
620     action->setEnabled(false);
621 
622     // We need a different QAction on the context menu because one will be disabled when there's no selection,
623     // other will be hidden.
624     action = collection->addAction(QStringLiteral("edit_copy_contextmenu"));
625     action->setText(i18n("Copy"));
626     action->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
627     action->setVisible(false);
628     connect(action, &QAction::triggered, this, &SessionController::copy);
629 
630     action = KStandardAction::paste(this, &SessionController::paste, collection);
631     QList<QKeySequence> pasteShortcut;
632 #ifdef Q_OS_MACOS
633     pasteShortcut.append(QKeySequence(Qt::META | Qt::Key_V));
634     // No Insert key on Mac keyboards
635 #else
636     pasteShortcut.append(QKeySequence(Konsole::ACCEL | Qt::SHIFT | Qt::Key_V));
637     pasteShortcut.append(QKeySequence(Qt::SHIFT | Qt::Key_Insert));
638 #endif
639     collection->setDefaultShortcuts(action, pasteShortcut);
640 
641     action = collection->addAction(QStringLiteral("paste-selection"), this, &SessionController::pasteFromX11Selection);
642     action->setText(i18n("Paste Selection"));
643 #ifdef Q_OS_MACOS
644     collection->setDefaultShortcut(action, Qt::META | Qt::SHIFT | Qt::Key_V);
645 #else
646     collection->setDefaultShortcut(action, Konsole::ACCEL | Qt::SHIFT | Qt::Key_Insert);
647 #endif
648 
649     _webSearchMenu = new KActionMenu(i18n("Web Search"), this);
650     _webSearchMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts")));
651     _webSearchMenu->setVisible(false);
652     collection->addAction(QStringLiteral("web-search"), _webSearchMenu);
653 
654     action = collection->addAction(QStringLiteral("select-all"), this, &SessionController::selectAll);
655     action->setText(i18n("&Select All"));
656     action->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all")));
657 
658     action = collection->addAction(QStringLiteral("select-line"), this, &SessionController::selectLine);
659     action->setText(i18n("Select &Line"));
660 
661     action = KStandardAction::saveAs(this, &SessionController::saveHistory, collection);
662     action->setText(i18n("Save Output &As..."));
663 #ifdef Q_OS_MACOS
664     action->setShortcut(QKeySequence(Qt::META | Qt::Key_S));
665 #endif
666 
667     action = KStandardAction::print(this, &SessionController::requestPrint, collection);
668     action->setText(i18n("&Print Screen..."));
669     collection->setDefaultShortcut(action, Konsole::ACCEL | Qt::SHIFT | Qt::Key_P);
670 
671     action = collection->addAction(QStringLiteral("adjust-history"), this, &SessionController::showHistoryOptions);
672     action->setText(i18n("Adjust Scrollback..."));
673     action->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
674 
675     action = collection->addAction(QStringLiteral("clear-history"), this, &SessionController::clearHistory);
676     action->setText(i18n("Clear Scrollback"));
677     action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history")));
678 
679     action = collection->addAction(QStringLiteral("clear-history-and-reset"), this, &SessionController::clearHistoryAndReset);
680     action->setText(i18n("Clear Scrollback and Reset"));
681     action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history")));
682     collection->setDefaultShortcut(action, Konsole::ACCEL | Qt::SHIFT | Qt::Key_K);
683 
684     // Profile Options
685     action = collection->addAction(QStringLiteral("edit-current-profile"), this, &SessionController::editCurrentProfile);
686     action->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
687     setEditProfileActionText(SessionManager::instance()->sessionProfile(session()));
688 
689     _switchProfileMenu = new KActionMenu(i18n("Switch Profile"), this);
690     collection->addAction(QStringLiteral("switch-profile"), _switchProfileMenu);
691     connect(_switchProfileMenu->menu(), &QMenu::aboutToShow, this, &Konsole::SessionController::prepareSwitchProfileMenu);
692 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 77, 0)
693     _switchProfileMenu->setPopupMode(QToolButton::MenuButtonPopup);
694 #else
695     _switchProfileMenu->setDelayed(false);
696 #endif
697 
698     // History
699     _findAction = KStandardAction::find(this, &SessionController::searchBarEvent, collection);
700 
701     _findNextAction = KStandardAction::findNext(this, &SessionController::findNextInHistory, collection);
702     _findNextAction->setEnabled(false);
703 
704     _findPreviousAction = KStandardAction::findPrev(this, &SessionController::findPreviousInHistory, collection);
705     _findPreviousAction->setEnabled(false);
706 
707 #ifdef Q_OS_MACOS
708     collection->setDefaultShortcut(_findAction, Qt::META | Qt::Key_F);
709     collection->setDefaultShortcut(_findNextAction, Qt::META | Qt::Key_G);
710     collection->setDefaultShortcut(_findPreviousAction, Qt::META | Qt::SHIFT | Qt::Key_G);
711 #else
712     collection->setDefaultShortcut(_findAction, Konsole::ACCEL | Qt::SHIFT | Qt::Key_F);
713     collection->setDefaultShortcut(_findNextAction, Qt::Key_F3);
714     collection->setDefaultShortcut(_findPreviousAction, Qt::SHIFT | Qt::Key_F3);
715 #endif
716 
717     // Character Encoding
718     _codecAction = new KCodecAction(i18n("Set &Encoding"), this);
719     _codecAction->setIcon(QIcon::fromTheme(QStringLiteral("character-set")));
720     collection->addAction(QStringLiteral("set-encoding"), _codecAction);
721     _codecAction->setCurrentCodec(QString::fromUtf8(session()->codec()));
722     connect(session(), &Konsole::Session::sessionCodecChanged, this, &Konsole::SessionController::updateCodecAction);
723     connect(_codecAction,
724 #if KCONFIGWIDGETS_VERSION >= QT_VERSION_CHECK(5, 78, 0)
725             QOverload<QTextCodec *>::of(&KCodecAction::codecTriggered),
726             this,
727 #else
728             QOverload<QTextCodec *>::of(&KCodecAction::triggered),
729             this,
730 #endif
731             &Konsole::SessionController::changeCodec);
732 
733     // Read-only
734     action = collection->addAction(QStringLiteral("view-readonly"), this, &SessionController::toggleReadOnly);
735     action->setText(i18nc("@item:inmenu A read only (locked) session", "Read-only"));
736     action->setCheckable(true);
737     updateReadOnlyActionStates();
738 }
739 
setupExtraActions()740 void SessionController::setupExtraActions()
741 {
742     KActionCollection *collection = actionCollection();
743 
744     // Rename Session
745     QAction *action = collection->addAction(QStringLiteral("rename-session"), this, &SessionController::renameSession);
746     action->setText(i18n("&Configure or Rename Tab..."));
747     action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename")));
748     collection->setDefaultShortcut(action, Konsole::ACCEL | Qt::ALT | Qt::Key_S);
749 
750     // Copy input to ==> all tabs
751     auto *copyInputToAllTabsAction = collection->add<KToggleAction>(QStringLiteral("copy-input-to-all-tabs"));
752     copyInputToAllTabsAction->setText(i18n("&All Tabs in Current Window"));
753     copyInputToAllTabsAction->setData(CopyInputToAllTabsMode);
754     // this action is also used in other place, so remember it
755     _copyInputToAllTabsAction = copyInputToAllTabsAction;
756 
757     // Copy input to ==> selected tabs
758     auto *copyInputToSelectedTabsAction = collection->add<KToggleAction>(QStringLiteral("copy-input-to-selected-tabs"));
759     copyInputToSelectedTabsAction->setText(i18n("&Select Tabs..."));
760     collection->setDefaultShortcut(copyInputToSelectedTabsAction, Konsole::ACCEL | Qt::SHIFT | Qt::Key_Period);
761     copyInputToSelectedTabsAction->setData(CopyInputToSelectedTabsMode);
762 
763     // Copy input to ==> none
764     auto *copyInputToNoneAction = collection->add<KToggleAction>(QStringLiteral("copy-input-to-none"));
765     copyInputToNoneAction->setText(i18nc("@action:inmenu Do not select any tabs", "&None"));
766     collection->setDefaultShortcut(copyInputToNoneAction, Konsole::ACCEL | Qt::SHIFT | Qt::Key_Slash);
767     copyInputToNoneAction->setData(CopyInputToNoneMode);
768     copyInputToNoneAction->setChecked(true); // the default state
769 
770     // The "Copy Input To" submenu
771     // The above three choices are represented as combo boxes
772     auto *copyInputActions = collection->add<KSelectAction>(QStringLiteral("copy-input-to"));
773     copyInputActions->setText(i18n("Copy Input To"));
774     copyInputActions->addAction(copyInputToAllTabsAction);
775     copyInputActions->addAction(copyInputToSelectedTabsAction);
776     copyInputActions->addAction(copyInputToNoneAction);
777     connect(copyInputActions, QOverload<QAction *>::of(&KSelectAction::triggered), this, &Konsole::SessionController::copyInputActionsTriggered);
778 
779     action = collection->addAction(QStringLiteral("zmodem-upload"), this, &SessionController::zmodemUpload);
780     action->setText(i18n("&ZModem Upload..."));
781     action->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
782     collection->setDefaultShortcut(action, Konsole::ACCEL | Qt::ALT | Qt::Key_U);
783 
784     // Monitor
785     KToggleAction *toggleAction = new KToggleAction(i18n("Monitor for &Activity"), this);
786     collection->setDefaultShortcut(toggleAction, Konsole::ACCEL | Qt::SHIFT | Qt::Key_A);
787     action = collection->addAction(QStringLiteral("monitor-activity"), toggleAction);
788     connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorActivity);
789     action->setIcon(QIcon::fromTheme(QStringLiteral("tools-media-optical-burn")));
790 
791     toggleAction = new KToggleAction(i18n("Monitor for &Silence"), this);
792     collection->setDefaultShortcut(toggleAction, Konsole::ACCEL | Qt::SHIFT | Qt::Key_I);
793     action = collection->addAction(QStringLiteral("monitor-silence"), toggleAction);
794     connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorSilence);
795     action->setIcon(QIcon::fromTheme(QStringLiteral("tools-media-optical-copy")));
796 
797     toggleAction = new KToggleAction(i18n("Monitor for Process Finishing"), this);
798     action = collection->addAction(QStringLiteral("monitor-process-finish"), toggleAction);
799     connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorProcessFinish);
800     action->setIcon(QIcon::fromTheme(QStringLiteral("tools-media-optical-burn-image")));
801 
802     // Text Size
803     action = collection->addAction(QStringLiteral("enlarge-font"), this, &SessionController::increaseFontSize);
804     action->setText(i18n("Enlarge Font"));
805     action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-more")));
806     QList<QKeySequence> enlargeFontShortcut;
807     enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL | Qt::Key_Plus));
808     enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL | Qt::Key_Equal));
809     collection->setDefaultShortcuts(action, enlargeFontShortcut);
810 
811     action = collection->addAction(QStringLiteral("shrink-font"), this, &SessionController::decreaseFontSize);
812     action->setText(i18n("Shrink Font"));
813     action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-less")));
814     collection->setDefaultShortcut(action, Konsole::ACCEL | Qt::Key_Minus);
815 
816     action = collection->addAction(QStringLiteral("reset-font-size"), this, &SessionController::resetFontSize);
817     action->setText(i18n("Reset Font Size"));
818     collection->setDefaultShortcut(action, Konsole::ACCEL | Qt::ALT | Qt::Key_0);
819 
820     // Send signal
821     auto *sendSignalActions = collection->add<KSelectAction>(QStringLiteral("send-signal"));
822     sendSignalActions->setText(i18n("Send Signal"));
823     connect(sendSignalActions, QOverload<QAction *>::of(&KSelectAction::triggered), this, &Konsole::SessionController::sendSignal);
824 
825     action = collection->addAction(QStringLiteral("sigstop-signal"));
826     action->setText(i18n("&Suspend Task") + QStringLiteral(" (STOP)"));
827     action->setData(SIGSTOP);
828     sendSignalActions->addAction(action);
829 
830     action = collection->addAction(QStringLiteral("sigcont-signal"));
831     action->setText(i18n("&Continue Task") + QStringLiteral(" (CONT)"));
832     action->setData(SIGCONT);
833     sendSignalActions->addAction(action);
834 
835     action = collection->addAction(QStringLiteral("sighup-signal"));
836     action->setText(i18n("&Hangup") + QStringLiteral(" (HUP)"));
837     action->setData(SIGHUP);
838     sendSignalActions->addAction(action);
839 
840     action = collection->addAction(QStringLiteral("sigint-signal"));
841     action->setText(i18n("&Interrupt Task") + QStringLiteral(" (INT)"));
842     action->setData(SIGINT);
843     sendSignalActions->addAction(action);
844 
845     action = collection->addAction(QStringLiteral("sigterm-signal"));
846     action->setText(i18n("&Terminate Task") + QStringLiteral(" (TERM)"));
847     action->setData(SIGTERM);
848     sendSignalActions->addAction(action);
849 
850     action = collection->addAction(QStringLiteral("sigkill-signal"));
851     action->setText(i18n("&Kill Task") + QStringLiteral(" (KILL)"));
852     action->setData(SIGKILL);
853     sendSignalActions->addAction(action);
854 
855     action = collection->addAction(QStringLiteral("sigusr1-signal"));
856     action->setText(i18n("User Signal &1") + QStringLiteral(" (USR1)"));
857     action->setData(SIGUSR1);
858     sendSignalActions->addAction(action);
859 
860     action = collection->addAction(QStringLiteral("sigusr2-signal"));
861     action->setText(i18n("User Signal &2") + QStringLiteral(" (USR2)"));
862     action->setData(SIGUSR2);
863     sendSignalActions->addAction(action);
864 }
865 
switchProfile(const Profile::Ptr & profile)866 void SessionController::switchProfile(const Profile::Ptr &profile)
867 {
868     SessionManager::instance()->setSessionProfile(session(), profile);
869     _switchProfileMenu->setIcon(QIcon::fromTheme(profile->icon()));
870     updateFilterList(profile);
871     setEditProfileActionText(profile);
872 }
873 
setEditProfileActionText(const Profile::Ptr & profile)874 void SessionController::setEditProfileActionText(const Profile::Ptr &profile)
875 {
876     QAction *action = actionCollection()->action(QStringLiteral("edit-current-profile"));
877     if (profile->isFallback()) {
878         action->setText(i18n("Create New Profile..."));
879     } else {
880         action->setText(i18n("Edit Current Profile..."));
881     }
882 }
883 
prepareSwitchProfileMenu()884 void SessionController::prepareSwitchProfileMenu()
885 {
886     if (_switchProfileMenu->menu()->isEmpty()) {
887         _profileList = new ProfileList(false, this);
888         connect(_profileList, &Konsole::ProfileList::profileSelected, this, &Konsole::SessionController::switchProfile);
889     }
890 
891     _switchProfileMenu->menu()->clear();
892     _switchProfileMenu->menu()->addActions(_profileList->actions());
893 }
updateCodecAction(QTextCodec * codec)894 void SessionController::updateCodecAction(QTextCodec *codec)
895 {
896     _codecAction->setCurrentCodec(codec);
897 }
898 
changeCodec(QTextCodec * codec)899 void SessionController::changeCodec(QTextCodec *codec)
900 {
901     session()->setCodec(codec);
902 }
903 
editCurrentProfile()904 void SessionController::editCurrentProfile()
905 {
906     auto *dialog = new EditProfileDialog(QApplication::activeWindow());
907     dialog->setAttribute(Qt::WA_DeleteOnClose);
908     dialog->setModal(true);
909 
910     auto profile = SessionManager::instance()->sessionProfile(session());
911     auto state = EditProfileDialog::ExistingProfile;
912     // Don't edit the Fallback profile, instead create a new one
913     if (profile->isFallback()) {
914         auto newProfile = Profile::Ptr(new Profile(profile));
915         newProfile->clone(profile, true);
916         const QString uniqueName = ProfileManager::instance()->generateUniqueName();
917         newProfile->setProperty(Profile::Name, uniqueName);
918         newProfile->setProperty(Profile::UntranslatedName, uniqueName);
919         profile = newProfile;
920         SessionManager::instance()->setSessionProfile(session(), profile);
921         state = EditProfileDialog::NewProfile;
922 
923         connect(dialog, &QDialog::accepted, this, [this, profile]() {
924             setEditProfileActionText(profile);
925         });
926     }
927 
928     dialog->setProfile(profile, state);
929 
930     dialog->show();
931 }
932 
renameSession()933 void SessionController::renameSession()
934 {
935     const QString sessionLocalTabTitleFormat = session()->tabTitleFormat(Session::LocalTabTitle);
936     const QString sessionRemoteTabTitleFormat = session()->tabTitleFormat(Session::RemoteTabTitle);
937     const QColor sessionTabColor = session()->color();
938 
939     auto *dialog = new RenameTabDialog(QApplication::activeWindow());
940     dialog->setAttribute(Qt::WA_DeleteOnClose);
941     dialog->setModal(true);
942     dialog->setTabTitleText(sessionLocalTabTitleFormat);
943     dialog->setRemoteTabTitleText(sessionRemoteTabTitleFormat);
944     dialog->setColor(sessionTabColor);
945 
946     if (session()->isRemote()) {
947         dialog->focusRemoteTabTitleText();
948     } else {
949         dialog->focusTabTitleText();
950     }
951 
952     connect(dialog, &QDialog::accepted, this, [=]() {
953         const QString tabTitle = dialog->tabTitleText();
954         const QString remoteTabTitle = dialog->remoteTabTitleText();
955         const QColor tabColor = dialog->color();
956 
957         if (tabTitle != sessionLocalTabTitleFormat) {
958             session()->setTabTitleFormat(Session::LocalTabTitle, tabTitle);
959             Q_EMIT tabRenamedByUser(true);
960             // trigger an update of the tab text
961             snapshot();
962         }
963 
964         if (remoteTabTitle != sessionRemoteTabTitleFormat) {
965             session()->setTabTitleFormat(Session::RemoteTabTitle, remoteTabTitle);
966             Q_EMIT tabRenamedByUser(true);
967             snapshot();
968         }
969 
970         if (tabColor != sessionTabColor) {
971             session()->setColor(tabColor);
972             Q_EMIT tabColoredByUser(true);
973             snapshot();
974         }
975     });
976 
977     dialog->show();
978 }
979 
980 // This is called upon Menu->Close Sesssion and right-click on tab->Close Tab
confirmClose() const981 bool SessionController::confirmClose() const
982 {
983     if (session()->isForegroundProcessActive()) {
984         QString title = session()->foregroundProcessName();
985 
986         // hard coded for now.  In future make it possible for the user to specify which programs
987         // are ignored when considering whether to display a confirmation
988         QStringList ignoreList;
989         ignoreList << QString::fromUtf8(qgetenv("SHELL")).section(QLatin1Char('/'), -1);
990         if (ignoreList.contains(title)) {
991             return true;
992         }
993 
994         QString question;
995         if (title.isEmpty()) {
996             question = i18n(
997                 "A program is currently running in this session."
998                 "  Are you sure you want to close it?");
999         } else {
1000             question = i18n(
1001                 "The program '%1' is currently running in this session."
1002                 "  Are you sure you want to close it?",
1003                 title);
1004         }
1005 
1006         int result = KMessageBox::warningYesNo(view()->window(),
1007                                                question,
1008                                                i18n("Confirm Close"),
1009                                                KStandardGuiItem::yes(),
1010                                                KStandardGuiItem::no(),
1011                                                QStringLiteral("CloseSingleTab"));
1012         return result == KMessageBox::Yes;
1013     }
1014     return true;
1015 }
confirmForceClose() const1016 bool SessionController::confirmForceClose() const
1017 {
1018     if (session()->isRunning()) {
1019         QString title = session()->program();
1020 
1021         // hard coded for now.  In future make it possible for the user to specify which programs
1022         // are ignored when considering whether to display a confirmation
1023         QStringList ignoreList;
1024         ignoreList << QString::fromUtf8(qgetenv("SHELL")).section(QLatin1Char('/'), -1);
1025         if (ignoreList.contains(title)) {
1026             return true;
1027         }
1028 
1029         QString question;
1030         if (title.isEmpty()) {
1031             question = i18n(
1032                 "A program in this session would not die."
1033                 "  Are you sure you want to kill it by force?");
1034         } else {
1035             question = i18n(
1036                 "The program '%1' is in this session would not die."
1037                 "  Are you sure you want to kill it by force?",
1038                 title);
1039         }
1040 
1041         int result = KMessageBox::warningYesNo(view()->window(), question, i18n("Confirm Close"));
1042         return result == KMessageBox::Yes;
1043     }
1044     return true;
1045 }
closeSession()1046 void SessionController::closeSession()
1047 {
1048     if (_preventClose) {
1049         return;
1050     }
1051 
1052     if (!confirmClose()) {
1053         return;
1054     }
1055 
1056     if (!session()->closeInNormalWay()) {
1057         if (!confirmForceClose()) {
1058             return;
1059         }
1060 
1061         if (!session()->closeInForceWay()) {
1062             qCDebug(KonsoleDebug) << "Konsole failed to close a session in any way.";
1063             return;
1064         }
1065     }
1066 
1067     if (factory() != nullptr) {
1068         factory()->removeClient(this);
1069     }
1070 }
1071 
1072 // Trying to open a remote Url may produce unexpected results.
1073 // Therefore, if a remote url, open the user's home path.
1074 // TODO consider: 1) disable menu upon remote session
1075 //   2) transform url to get the desired result (ssh -> sftp, etc)
openBrowser()1076 void SessionController::openBrowser()
1077 {
1078     const QUrl currentUrl = url().isLocalFile() ? url() : QUrl::fromLocalFile(QDir::homePath());
1079 
1080     auto *job = new KIO::OpenUrlJob(currentUrl);
1081     job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, QApplication::activeWindow()));
1082     job->start();
1083 }
1084 
copy()1085 void SessionController::copy()
1086 {
1087     view()->copyToClipboard();
1088 }
1089 
paste()1090 void SessionController::paste()
1091 {
1092     view()->pasteFromClipboard();
1093 }
pasteFromX11Selection()1094 void SessionController::pasteFromX11Selection()
1095 {
1096     view()->pasteFromX11Selection();
1097 }
selectAll()1098 void SessionController::selectAll()
1099 {
1100     view()->selectAll();
1101 }
selectLine()1102 void SessionController::selectLine()
1103 {
1104     view()->selectCurrentLine();
1105 }
findWindow(const QObject * object)1106 static const KXmlGuiWindow *findWindow(const QObject *object)
1107 {
1108     // Walk up the QObject hierarchy to find a KXmlGuiWindow.
1109     while (object != nullptr) {
1110         const auto *window = qobject_cast<const KXmlGuiWindow *>(object);
1111         if (window != nullptr) {
1112             return (window);
1113         }
1114         object = object->parent();
1115     }
1116     return (nullptr);
1117 }
1118 
hasTerminalDisplayInSameWindow(const Session * session,const KXmlGuiWindow * window)1119 static bool hasTerminalDisplayInSameWindow(const Session *session, const KXmlGuiWindow *window)
1120 {
1121     // Iterate all TerminalDisplays of this Session ...
1122     const QList<TerminalDisplay *> views = session->views();
1123     for (const TerminalDisplay *terminalDisplay : views) {
1124         // ... and check whether a TerminalDisplay has the same
1125         // window as given in the parameter
1126         if (window == findWindow(terminalDisplay)) {
1127             return (true);
1128         }
1129     }
1130     return (false);
1131 }
1132 
copyInputActionsTriggered(QAction * action)1133 void SessionController::copyInputActionsTriggered(QAction *action)
1134 {
1135     const auto mode = action->data().toInt();
1136 
1137     switch (mode) {
1138     case CopyInputToAllTabsMode:
1139         copyInputToAllTabs();
1140         break;
1141     case CopyInputToSelectedTabsMode:
1142         copyInputToSelectedTabs();
1143         break;
1144     case CopyInputToNoneMode:
1145         copyInputToNone();
1146         break;
1147     default:
1148         Q_ASSERT(false);
1149     }
1150 }
1151 
copyInputToAllTabs()1152 void SessionController::copyInputToAllTabs()
1153 {
1154     if (_copyToGroup == nullptr) {
1155         _copyToGroup = new SessionGroup(this);
1156     }
1157 
1158     // Find our window ...
1159     const KXmlGuiWindow *myWindow = findWindow(view());
1160 
1161     const QList<Session *> sessionsList = SessionManager::instance()->sessions();
1162     QSet<Session *> group(sessionsList.begin(), sessionsList.end());
1163     for (auto session : group) {
1164         // First, ensure that the session is removed
1165         // (necessary to avoid duplicates on addSession()!)
1166         _copyToGroup->removeSession(session);
1167 
1168         // Add current session if it is displayed our window
1169         if (hasTerminalDisplayInSameWindow(session, myWindow)) {
1170             _copyToGroup->addSession(session);
1171         }
1172     }
1173     _copyToGroup->setMasterStatus(session(), true);
1174     _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll);
1175 
1176     snapshot();
1177     Q_EMIT copyInputChanged(this);
1178 }
1179 
copyInputToSelectedTabs()1180 void SessionController::copyInputToSelectedTabs()
1181 {
1182     if (_copyToGroup == nullptr) {
1183         _copyToGroup = new SessionGroup(this);
1184         _copyToGroup->addSession(session());
1185         _copyToGroup->setMasterStatus(session(), true);
1186         _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll);
1187     }
1188 
1189     auto *dialog = new CopyInputDialog(view());
1190     dialog->setAttribute(Qt::WA_DeleteOnClose);
1191     dialog->setModal(true);
1192     dialog->setMasterSession(session());
1193 
1194     const QList<Session *> sessionsList = _copyToGroup->sessions();
1195     QSet<Session *> currentGroup(sessionsList.begin(), sessionsList.end());
1196 
1197     currentGroup.remove(session());
1198 
1199     dialog->setChosenSessions(currentGroup);
1200 
1201     connect(dialog, &QDialog::accepted, this, [=]() {
1202         QSet<Session *> newGroup = dialog->chosenSessions();
1203         newGroup.remove(session());
1204 
1205         const QSet<Session *> completeGroup = newGroup | currentGroup;
1206         for (Session *session : completeGroup) {
1207             if (newGroup.contains(session) && !currentGroup.contains(session)) {
1208                 _copyToGroup->addSession(session);
1209             } else if (!newGroup.contains(session) && currentGroup.contains(session)) {
1210                 _copyToGroup->removeSession(session);
1211             }
1212         }
1213 
1214         _copyToGroup->setMasterStatus(session(), true);
1215         _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll);
1216         snapshot();
1217         Q_EMIT copyInputChanged(this);
1218     });
1219 
1220     dialog->show();
1221 }
1222 
copyInputToNone()1223 void SessionController::copyInputToNone()
1224 {
1225     if (_copyToGroup == nullptr) { // No 'Copy To' is active
1226         return;
1227     }
1228 
1229     // Once Qt5.14+ is the mininum, change to use range constructors
1230     const QList<Session *> groupList = SessionManager::instance()->sessions();
1231     QSet<Session *> group(groupList.begin(), groupList.end());
1232 
1233     for (auto iterator : group) {
1234         Session *s = iterator;
1235 
1236         if (s != session()) {
1237             _copyToGroup->removeSession(iterator);
1238         }
1239     }
1240     delete _copyToGroup;
1241     _copyToGroup = nullptr;
1242     snapshot();
1243     Q_EMIT copyInputChanged(this);
1244 }
1245 
searchClosed()1246 void SessionController::searchClosed()
1247 {
1248     _isSearchBarEnabled = false;
1249     searchHistory(false);
1250 }
1251 
updateFilterList(const Profile::Ptr & profile)1252 void SessionController::updateFilterList(const Profile::Ptr &profile)
1253 {
1254     if (profile != SessionManager::instance()->sessionProfile(session())) {
1255         return;
1256     }
1257 
1258     auto *filterChain = view()->filterChain();
1259 
1260     const QString currentWordCharacters = profile->wordCharacters();
1261     static QString _wordChars = currentWordCharacters;
1262 
1263     if (profile->underlineFilesEnabled()) {
1264         if (_fileFilter == nullptr) { // Initialize
1265             _fileFilter = new FileFilter(session(), currentWordCharacters);
1266             filterChain->addFilter(_fileFilter);
1267         } else {
1268             // If wordCharacters changed, we need to change the static regex
1269             // pattern in _fileFilter
1270             if (_wordChars != currentWordCharacters) {
1271                 _wordChars = currentWordCharacters;
1272                 _fileFilter->updateRegex(currentWordCharacters);
1273             }
1274         }
1275     } else if (_fileFilter != nullptr) { // It became disabled, clean up
1276         filterChain->removeFilter(_fileFilter);
1277         delete _fileFilter;
1278         _fileFilter = nullptr;
1279     }
1280 
1281     if (profile->underlineLinksEnabled()) {
1282         if (_urlFilter == nullptr) { // Initialize
1283             _urlFilter = new UrlFilter();
1284             filterChain->addFilter(_urlFilter);
1285         }
1286     } else if (_urlFilter != nullptr) { // It became disabled, clean up
1287         filterChain->removeFilter(_urlFilter);
1288         delete _urlFilter;
1289         _urlFilter = nullptr;
1290     }
1291 
1292     if (profile->allowEscapedLinks()) {
1293         if (_escapedUrlFilter == nullptr) { // Initialize
1294             _escapedUrlFilter = new EscapeSequenceUrlFilter(session(), view());
1295             filterChain->addFilter(_escapedUrlFilter);
1296         }
1297     } else if (_escapedUrlFilter != nullptr) { // It became disabled, clean up
1298         filterChain->removeFilter(_escapedUrlFilter);
1299         delete _escapedUrlFilter;
1300         _escapedUrlFilter = nullptr;
1301     }
1302 
1303     const bool allowColorFilters = profile->colorFilterEnabled();
1304     if (!allowColorFilters && (_colorFilter != nullptr)) {
1305         filterChain->removeFilter(_colorFilter);
1306         delete _colorFilter;
1307         _colorFilter = nullptr;
1308     } else if (allowColorFilters && (_colorFilter == nullptr)) {
1309         _colorFilter = new ColorFilter();
1310         filterChain->addFilter(_colorFilter);
1311     }
1312 }
1313 
setSearchStartToWindowCurrentLine()1314 void SessionController::setSearchStartToWindowCurrentLine()
1315 {
1316     setSearchStartTo(-1);
1317 }
1318 
setSearchStartTo(int line)1319 void SessionController::setSearchStartTo(int line)
1320 {
1321     _searchStartLine = line;
1322     _prevSearchResultLine = line;
1323 }
1324 
listenForScreenWindowUpdates()1325 void SessionController::listenForScreenWindowUpdates()
1326 {
1327     if (_listenForScreenWindowUpdates) {
1328         return;
1329     }
1330 
1331     connect(view()->screenWindow(), &Konsole::ScreenWindow::outputChanged, this, &Konsole::SessionController::updateSearchFilter);
1332     connect(view()->screenWindow(), &Konsole::ScreenWindow::scrolled, this, &Konsole::SessionController::updateSearchFilter);
1333     connect(view()->screenWindow(), &Konsole::ScreenWindow::currentResultLineChanged, view(), QOverload<>::of(&Konsole::TerminalDisplay::update));
1334 
1335     _listenForScreenWindowUpdates = true;
1336 }
1337 
updateSearchFilter()1338 void SessionController::updateSearchFilter()
1339 {
1340     if ((_searchFilter != nullptr) && (!_searchBar.isNull())) {
1341         view()->processFilters();
1342     }
1343 }
1344 
searchBarEvent()1345 void SessionController::searchBarEvent()
1346 {
1347     QString selectedText = view()->screenWindow()->selectedText(Screen::PreserveLineBreaks | Screen::TrimLeadingWhitespace | Screen::TrimTrailingWhitespace);
1348     if (!selectedText.isEmpty()) {
1349         _searchBar->setSearchText(selectedText);
1350     }
1351 
1352     if (_searchBar->isVisible()) {
1353         _searchBar->focusLineEdit();
1354     } else {
1355         searchHistory(true);
1356         _isSearchBarEnabled = true;
1357     }
1358 }
1359 
enableSearchBar(bool showSearchBar)1360 void SessionController::enableSearchBar(bool showSearchBar)
1361 {
1362     if (_searchBar.isNull()) {
1363         return;
1364     }
1365 
1366     if (showSearchBar && !_searchBar->isVisible()) {
1367         setSearchStartToWindowCurrentLine();
1368     }
1369 
1370     _searchBar->setVisible(showSearchBar);
1371     if (showSearchBar) {
1372         connect(_searchBar, &Konsole::IncrementalSearchBar::searchChanged, this, &Konsole::SessionController::searchTextChanged);
1373         connect(_searchBar, &Konsole::IncrementalSearchBar::searchReturnPressed, this, &Konsole::SessionController::findPreviousInHistory);
1374         connect(_searchBar, &Konsole::IncrementalSearchBar::searchShiftPlusReturnPressed, this, &Konsole::SessionController::findNextInHistory);
1375     } else {
1376         disconnect(_searchBar, &Konsole::IncrementalSearchBar::searchChanged, this, &Konsole::SessionController::searchTextChanged);
1377         disconnect(_searchBar, &Konsole::IncrementalSearchBar::searchReturnPressed, this, &Konsole::SessionController::findPreviousInHistory);
1378         disconnect(_searchBar, &Konsole::IncrementalSearchBar::searchShiftPlusReturnPressed, this, &Konsole::SessionController::findNextInHistory);
1379         if ((!view().isNull()) && (view()->screenWindow() != nullptr)) {
1380             view()->screenWindow()->setCurrentResultLine(-1);
1381         }
1382     }
1383 }
1384 
reverseSearchChecked() const1385 bool SessionController::reverseSearchChecked() const
1386 {
1387     Q_ASSERT(_searchBar);
1388 
1389     QBitArray options = _searchBar->optionsChecked();
1390     return options.at(IncrementalSearchBar::ReverseSearch);
1391 }
1392 
regexpFromSearchBarOptions() const1393 QRegularExpression SessionController::regexpFromSearchBarOptions() const
1394 {
1395     QBitArray options = _searchBar->optionsChecked();
1396 
1397     QString text(_searchBar->searchText());
1398 
1399     QRegularExpression regExp;
1400     if (options.at(IncrementalSearchBar::RegExp)) {
1401         regExp.setPattern(text);
1402     } else {
1403         regExp.setPattern(QRegularExpression::escape(text));
1404     }
1405 
1406     if (!options.at(IncrementalSearchBar::MatchCase)) {
1407         regExp.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
1408     }
1409 
1410     return regExp;
1411 }
1412 
1413 // searchHistory() may be called either as a result of clicking a menu item or
1414 // as a result of changing the search bar widget
searchHistory(bool showSearchBar)1415 void SessionController::searchHistory(bool showSearchBar)
1416 {
1417     enableSearchBar(showSearchBar);
1418 
1419     if (!_searchBar.isNull()) {
1420         if (showSearchBar) {
1421             removeSearchFilter();
1422 
1423             listenForScreenWindowUpdates();
1424 
1425             _searchFilter = new RegExpFilter();
1426             _searchFilter->setRegExp(regexpFromSearchBarOptions());
1427             view()->filterChain()->addFilter(_searchFilter);
1428             view()->processFilters();
1429 
1430             setFindNextPrevEnabled(true);
1431         } else {
1432             setFindNextPrevEnabled(false);
1433 
1434             removeSearchFilter();
1435 
1436             view()->setFocus(Qt::ActiveWindowFocusReason);
1437         }
1438     }
1439 }
1440 
setFindNextPrevEnabled(bool enabled)1441 void SessionController::setFindNextPrevEnabled(bool enabled)
1442 {
1443     _findNextAction->setEnabled(enabled);
1444     _findPreviousAction->setEnabled(enabled);
1445 }
searchTextChanged(const QString & text)1446 void SessionController::searchTextChanged(const QString &text)
1447 {
1448     Q_ASSERT(view()->screenWindow());
1449 
1450     if (_searchText == text) {
1451         return;
1452     }
1453 
1454     _searchText = text;
1455 
1456     if (text.isEmpty()) {
1457         view()->screenWindow()->clearSelection();
1458         view()->screenWindow()->scrollTo(_searchStartLine);
1459     }
1460 
1461     // update search.  this is called even when the text is
1462     // empty to clear the view's filters
1463     beginSearch(text, reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch);
1464 }
searchCompleted(bool success)1465 void SessionController::searchCompleted(bool success)
1466 {
1467     _prevSearchResultLine = view()->screenWindow()->currentResultLine();
1468 
1469     if (!_searchBar.isNull()) {
1470         _searchBar->setFoundMatch(success);
1471     }
1472 }
1473 
beginSearch(const QString & text,Enum::SearchDirection direction)1474 void SessionController::beginSearch(const QString &text, Enum::SearchDirection direction)
1475 {
1476     Q_ASSERT(_searchBar);
1477     Q_ASSERT(_searchFilter);
1478 
1479     QRegularExpression regExp = regexpFromSearchBarOptions();
1480     _searchFilter->setRegExp(regExp);
1481 
1482     if (_searchStartLine < 0 || _searchStartLine > view()->screenWindow()->lineCount()) {
1483         if (direction == Enum::ForwardsSearch) {
1484             setSearchStartTo(view()->screenWindow()->currentLine());
1485         } else {
1486             setSearchStartTo(view()->screenWindow()->currentLine() + view()->screenWindow()->windowLines());
1487         }
1488     }
1489 
1490     if (!regExp.pattern().isEmpty()) {
1491         view()->screenWindow()->setCurrentResultLine(-1);
1492         auto task = new SearchHistoryTask(this);
1493 
1494         connect(task, &Konsole::SearchHistoryTask::completed, this, &Konsole::SessionController::searchCompleted);
1495 
1496         task->setRegExp(regExp);
1497         task->setSearchDirection(direction);
1498         task->setAutoDelete(true);
1499         task->setStartLine(_searchStartLine);
1500         task->addScreenWindow(session(), view()->screenWindow());
1501         task->execute();
1502     } else if (text.isEmpty()) {
1503         searchCompleted(false);
1504     }
1505 
1506     view()->processFilters();
1507 }
highlightMatches(bool highlight)1508 void SessionController::highlightMatches(bool highlight)
1509 {
1510     if (highlight) {
1511         view()->filterChain()->addFilter(_searchFilter);
1512         view()->processFilters();
1513     } else {
1514         view()->filterChain()->removeFilter(_searchFilter);
1515     }
1516 
1517     view()->update();
1518 }
1519 
searchFrom()1520 void SessionController::searchFrom()
1521 {
1522     Q_ASSERT(_searchBar);
1523     Q_ASSERT(_searchFilter);
1524 
1525     if (reverseSearchChecked()) {
1526         setSearchStartTo(view()->screenWindow()->lineCount());
1527     } else {
1528         setSearchStartTo(0);
1529     }
1530 
1531     beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch);
1532 }
findNextInHistory()1533 void SessionController::findNextInHistory()
1534 {
1535     Q_ASSERT(_searchBar);
1536     Q_ASSERT(_searchFilter);
1537 
1538     setSearchStartTo(_prevSearchResultLine);
1539 
1540     beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch);
1541 }
findPreviousInHistory()1542 void SessionController::findPreviousInHistory()
1543 {
1544     Q_ASSERT(_searchBar);
1545     Q_ASSERT(_searchFilter);
1546 
1547     setSearchStartTo(_prevSearchResultLine);
1548 
1549     beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::ForwardsSearch : Enum::BackwardsSearch);
1550 }
updateMenuIconsAccordingToReverseSearchSetting()1551 void SessionController::updateMenuIconsAccordingToReverseSearchSetting()
1552 {
1553     if (reverseSearchChecked()) {
1554         _findNextAction->setIcon(QIcon::fromTheme(QStringLiteral("go-up")));
1555         _findPreviousAction->setIcon(QIcon::fromTheme(QStringLiteral("go-down")));
1556     } else {
1557         _findNextAction->setIcon(QIcon::fromTheme(QStringLiteral("go-down")));
1558         _findPreviousAction->setIcon(QIcon::fromTheme(QStringLiteral("go-up")));
1559     }
1560 }
changeSearchMatch()1561 void SessionController::changeSearchMatch()
1562 {
1563     Q_ASSERT(_searchBar);
1564     Q_ASSERT(_searchFilter);
1565 
1566     // reset Selection for new case match
1567     view()->screenWindow()->clearSelection();
1568     beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch);
1569 }
showHistoryOptions()1570 void SessionController::showHistoryOptions()
1571 {
1572     auto *dialog = new HistorySizeDialog(QApplication::activeWindow());
1573     dialog->setAttribute(Qt::WA_DeleteOnClose);
1574     dialog->setModal(true);
1575 
1576     const HistoryType &currentHistory = session()->historyType();
1577     if (currentHistory.isEnabled()) {
1578         if (currentHistory.isUnlimited()) {
1579             dialog->setMode(Enum::UnlimitedHistory);
1580         } else {
1581             dialog->setMode(Enum::FixedSizeHistory);
1582             dialog->setLineCount(currentHistory.maximumLineCount());
1583         }
1584     } else {
1585         dialog->setMode(Enum::NoHistory);
1586     }
1587 
1588     connect(dialog, &QDialog::accepted, this, [this, dialog]() {
1589         scrollBackOptionsChanged(dialog->mode(), dialog->lineCount());
1590     });
1591 
1592     dialog->show();
1593 }
sessionResizeRequest(const QSize & size)1594 void SessionController::sessionResizeRequest(const QSize &size)
1595 {
1596     ////qDebug() << "View resize requested to " << size;
1597     view()->setSize(size.width(), size.height());
1598 }
scrollBackOptionsChanged(int mode,int lines)1599 void SessionController::scrollBackOptionsChanged(int mode, int lines)
1600 {
1601     switch (mode) {
1602     case Enum::NoHistory:
1603         session()->setHistoryType(HistoryTypeNone());
1604         break;
1605     case Enum::FixedSizeHistory:
1606         session()->setHistoryType(CompactHistoryType(lines));
1607         break;
1608     case Enum::UnlimitedHistory:
1609         session()->setHistoryType(HistoryTypeFile());
1610         break;
1611     }
1612 }
1613 
saveHistory()1614 void SessionController::saveHistory()
1615 {
1616     SessionTask *task = new SaveHistoryTask(this);
1617     task->setAutoDelete(true);
1618     task->addSession(session());
1619     task->execute();
1620 }
1621 
clearHistory()1622 void SessionController::clearHistory()
1623 {
1624     session()->clearHistory();
1625     view()->updateImage(); // To reset view scrollbar
1626     view()->repaint();
1627 }
1628 
clearHistoryAndReset()1629 void SessionController::clearHistoryAndReset()
1630 {
1631     Profile::Ptr profile = SessionManager::instance()->sessionProfile(session());
1632     QByteArray name = profile->defaultEncoding().toUtf8();
1633 
1634     Emulation *emulation = session()->emulation();
1635     emulation->reset();
1636     session()->refresh();
1637     session()->setCodec(QTextCodec::codecForName(name));
1638     clearHistory();
1639 }
1640 
increaseFontSize()1641 void SessionController::increaseFontSize()
1642 {
1643     view()->terminalFont()->increaseFontSize();
1644 }
1645 
decreaseFontSize()1646 void SessionController::decreaseFontSize()
1647 {
1648     view()->terminalFont()->decreaseFontSize();
1649 }
1650 
resetFontSize()1651 void SessionController::resetFontSize()
1652 {
1653     view()->terminalFont()->resetFontSize();
1654 }
1655 
monitorActivity(bool monitor)1656 void SessionController::monitorActivity(bool monitor)
1657 {
1658     session()->setMonitorActivity(monitor);
1659 }
monitorSilence(bool monitor)1660 void SessionController::monitorSilence(bool monitor)
1661 {
1662     session()->setMonitorSilence(monitor);
1663 }
monitorProcessFinish(bool monitor)1664 void SessionController::monitorProcessFinish(bool monitor)
1665 {
1666     _monitorProcessFinish = monitor;
1667 }
updateSessionIcon()1668 void SessionController::updateSessionIcon()
1669 {
1670     // If the default profile icon is being used, don't put it on the tab
1671     // Only show the icon if the user specifically chose one
1672     if (session()->iconName() == QStringLiteral("utilities-terminal")) {
1673         _sessionIconName = QString();
1674     } else {
1675         _sessionIconName = session()->iconName();
1676     }
1677     _sessionIcon = QIcon::fromTheme(_sessionIconName);
1678 
1679     setIcon(_sessionIcon);
1680 }
1681 
updateReadOnlyActionStates()1682 void SessionController::updateReadOnlyActionStates()
1683 {
1684     bool readonly = isReadOnly();
1685     QAction *readonlyAction = actionCollection()->action(QStringLiteral("view-readonly"));
1686     Q_ASSERT(readonlyAction != nullptr);
1687     readonlyAction->setIcon(QIcon::fromTheme(readonly ? QStringLiteral("object-locked") : QStringLiteral("object-unlocked")));
1688     readonlyAction->setChecked(readonly);
1689 
1690     auto updateActionState = [this, readonly](const QString &name) {
1691         QAction *action = actionCollection()->action(name);
1692         if (action != nullptr) {
1693             action->setVisible(!readonly);
1694         }
1695     };
1696 
1697     updateActionState(QStringLiteral("edit_paste"));
1698     updateActionState(QStringLiteral("clear-history"));
1699     updateActionState(QStringLiteral("clear-history-and-reset"));
1700     updateActionState(QStringLiteral("edit-current-profile"));
1701     updateActionState(QStringLiteral("switch-profile"));
1702     updateActionState(QStringLiteral("adjust-history"));
1703     updateActionState(QStringLiteral("send-signal"));
1704     updateActionState(QStringLiteral("zmodem-upload"));
1705 
1706     _codecAction->setEnabled(!readonly);
1707 
1708     // Without the timer, when detaching a tab while the message widget is visible,
1709     // the size of the terminal becomes really small...
1710     QTimer::singleShot(0, this, [this, readonly]() {
1711         view()->updateReadOnlyState(readonly);
1712     });
1713 }
1714 
isReadOnly() const1715 bool SessionController::isReadOnly() const
1716 {
1717     if (!session().isNull()) {
1718         return session()->isReadOnly();
1719     } else {
1720         return false;
1721     }
1722 }
1723 
isCopyInputActive() const1724 bool SessionController::isCopyInputActive() const
1725 {
1726     return ((_copyToGroup != nullptr) && _copyToGroup->sessions().count() > 1);
1727 }
1728 
sessionAttributeChanged()1729 void SessionController::sessionAttributeChanged()
1730 {
1731     if (_sessionIconName != session()->iconName()) {
1732         updateSessionIcon();
1733     }
1734 
1735     QString title = session()->title(Session::DisplayedTitleRole);
1736 
1737     // special handling for the "%w" marker which is replaced with the
1738     // window title set by the shell
1739     title.replace(QLatin1String("%w"), session()->userTitle());
1740     // special handling for the "%#" marker which is replaced with the
1741     // number of the shell
1742     title.replace(QLatin1String("%#"), QString::number(session()->sessionId()));
1743 
1744     if (title.isEmpty()) {
1745         title = session()->title(Session::NameRole);
1746     }
1747 
1748     setTitle(title);
1749     setColor(session()->color());
1750     Q_EMIT rawTitleChanged();
1751 }
1752 
sessionReadOnlyChanged()1753 void SessionController::sessionReadOnlyChanged()
1754 {
1755     updateReadOnlyActionStates();
1756 
1757     // Update all views
1758     const QList<TerminalDisplay *> viewsList = session()->views();
1759     for (TerminalDisplay *terminalDisplay : viewsList) {
1760         if (terminalDisplay != view()) {
1761             terminalDisplay->updateReadOnlyState(isReadOnly());
1762         }
1763         Q_EMIT readOnlyChanged(this);
1764     }
1765 }
1766 
showDisplayContextMenu(const QPoint & position)1767 void SessionController::showDisplayContextMenu(const QPoint &position)
1768 {
1769     // needed to make sure the popup menu is available, even if a hosting
1770     // application did not merge our GUI.
1771     if (factory() == nullptr) {
1772         if (clientBuilder() == nullptr) {
1773             // Client builder does not get deleted automatically, we handle this
1774             _clientBuilder.reset(new KXMLGUIBuilder(view()));
1775             setClientBuilder(_clientBuilder.get());
1776         }
1777 
1778         auto factory = new KXMLGUIFactory(clientBuilder(), view());
1779         factory->addClient(this);
1780     }
1781 
1782     QPointer<QMenu> popup = qobject_cast<QMenu *>(factory()->container(QStringLiteral("session-popup-menu"), this));
1783     if (!popup.isNull()) {
1784         updateReadOnlyActionStates();
1785 
1786         auto contentSeparator = new QAction(popup);
1787         contentSeparator->setSeparator(true);
1788 
1789         // We don't actually use this shortcut, but we need to display it for consistency :/
1790         QAction *copy = actionCollection()->action(QStringLiteral("edit_copy_contextmenu"));
1791 #ifdef Q_OS_MACOS
1792         copy->setShortcut(Qt::META | Qt::Key_C);
1793 #else
1794         copy->setShortcut(Konsole::ACCEL | Qt::SHIFT | Qt::Key_C);
1795 #endif
1796 
1797         QList<QAction *> toRemove;
1798         // prepend content-specific actions such as "Open Link", "Copy Email Address" etc
1799         QSharedPointer<HotSpot> hotSpot = view()->filterActions(position);
1800         if (hotSpot != nullptr) {
1801             popup->insertActions(popup->actions().value(0, nullptr), hotSpot->actions() << contentSeparator);
1802             popup->addAction(contentSeparator);
1803             toRemove = hotSpot->setupMenu(popup.data());
1804         }
1805 
1806         // always update this submenu before showing the context menu,
1807         // because the available search services might have changed
1808         // since the context menu is shown last time
1809         updateWebSearchMenu();
1810 
1811         _preventClose = true;
1812 
1813         if (_showMenuAction != nullptr) {
1814             if (_showMenuAction->isChecked()) {
1815                 popup->removeAction(_showMenuAction);
1816             } else {
1817                 popup->insertAction(_switchProfileMenu, _showMenuAction);
1818             }
1819         }
1820 
1821         // they are here.
1822         // qDebug() << popup->actions().indexOf(contentActions[0]) << popup->actions().indexOf(contentActions[1]) << popup->actions()[3];
1823         QAction *chosen = popup->exec(QCursor::pos());
1824 
1825         // check for validity of the pointer to the popup menu
1826         if (!popup.isNull()) {
1827             delete contentSeparator;
1828             // Remove the 'Open with' actions from it.
1829             for (auto *act : toRemove) {
1830                 popup->removeAction(act);
1831             }
1832 
1833             // Remove the Accelerator for the copy shortcut so we don't have two actions with same shortcut.
1834             copy->setShortcut({});
1835         }
1836 
1837         // This should be at the end, to prevent crashes if the session
1838         // is closed from the menu in e.g. konsole kpart
1839         _preventClose = false;
1840         if ((chosen != nullptr) && chosen->objectName() == QLatin1String("close-session")) {
1841             chosen->trigger();
1842         }
1843     } else {
1844         qCDebug(KonsoleDebug) << "Unable to display popup menu for session" << session()->title(Session::NameRole)
1845                               << ", no GUI factory available to build the popup.";
1846     }
1847 }
1848 
movementKeyFromSearchBarReceived(QKeyEvent * event)1849 void SessionController::movementKeyFromSearchBarReceived(QKeyEvent *event)
1850 {
1851     QCoreApplication::sendEvent(view(), event);
1852     setSearchStartToWindowCurrentLine();
1853 }
1854 
sessionNotificationsChanged(Session::Notification notification,bool enabled)1855 void SessionController::sessionNotificationsChanged(Session::Notification notification, bool enabled)
1856 {
1857     Q_EMIT notificationChanged(this, notification, enabled);
1858 }
1859 
zmodemDownload()1860 void SessionController::zmodemDownload()
1861 {
1862     QString zmodem = QStandardPaths::findExecutable(QStringLiteral("rz"));
1863     if (zmodem.isEmpty()) {
1864         zmodem = QStandardPaths::findExecutable(QStringLiteral("lrz"));
1865     }
1866     if (!zmodem.isEmpty()) {
1867         const QString path = QFileDialog::getExistingDirectory(view(),
1868                                                                i18n("Save ZModem Download to..."),
1869                                                                QDir::homePath(),
1870                                                                QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
1871 
1872         if (!path.isEmpty()) {
1873             session()->startZModem(zmodem, path, QStringList());
1874             return;
1875         }
1876     } else {
1877         KMessageBox::error(view(),
1878                            i18n("<p>A ZModem file transfer attempt has been detected, "
1879                                 "but no suitable ZModem software was found on this system.</p>"
1880                                 "<p>You may wish to install the 'rzsz' or 'lrzsz' package.</p>"));
1881     }
1882     session()->cancelZModem();
1883 }
1884 
zmodemUpload()1885 void SessionController::zmodemUpload()
1886 {
1887     if (session()->isZModemBusy()) {
1888         KMessageBox::sorry(view(), i18n("<p>The current session already has a ZModem file transfer in progress.</p>"));
1889         return;
1890     }
1891 
1892     QString zmodem = QStandardPaths::findExecutable(QStringLiteral("sz"));
1893     if (zmodem.isEmpty()) {
1894         zmodem = QStandardPaths::findExecutable(QStringLiteral("lsz"));
1895     }
1896     if (zmodem.isEmpty()) {
1897         KMessageBox::sorry(view(),
1898                            i18n("<p>No suitable ZModem software was found on this system.</p>"
1899                                 "<p>You may wish to install the 'rzsz' or 'lrzsz' package.</p>"));
1900         return;
1901     }
1902 
1903     QStringList files = QFileDialog::getOpenFileNames(view(), i18n("Select Files for ZModem Upload"), QDir::homePath());
1904     if (!files.isEmpty()) {
1905         session()->startZModem(zmodem, QString(), files);
1906     }
1907 }
1908 
isKonsolePart() const1909 bool SessionController::isKonsolePart() const
1910 {
1911     // Check to see if we are being called from Konsole or a KPart
1912     return !(qApp->applicationName() == QLatin1String("konsole"));
1913 }
1914 
userTitle() const1915 QString SessionController::userTitle() const
1916 {
1917     if (!session().isNull()) {
1918         return session()->userTitle();
1919     } else {
1920         return QString();
1921     }
1922 }
1923 
isValid() const1924 bool SessionController::isValid() const
1925 {
1926     return _sessionDisplayConnection->isValid();
1927 }
1928