diff --git a/src/Session.cpp b/src/Session.cpp index d970b4f9..568ed534 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -1,1808 +1,1808 @@ /* This file is part of Konsole Copyright 2006-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "Session.h" // Standard #include #include #include // Qt #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include // Konsole #include #include "ProcessInfo.h" #include "Pty.h" #include "TerminalDisplay.h" #include "ShellCommand.h" #include "Vt102Emulation.h" #include "ZModemDialog.h" #include "History.h" #include "konsoledebug.h" #include "SessionManager.h" #include "ProfileManager.h" #include "Profile.h" using namespace Konsole; int Session::lastSessionId = 0; static bool show_disallow_certain_dbus_methods_message = true; static const int ZMODEM_BUFFER_SIZE = 1048576; // 1 Mb Session::Session(QObject* parent) : QObject(parent) , _uniqueIdentifier(QUuid()) , _shellProcess(nullptr) , _emulation(nullptr) , _views(QList()) , _monitorActivity(false) , _monitorSilence(false) , _notifiedActivity(false) , _silenceSeconds(10) , _silenceTimer(nullptr) , _activityTimer(nullptr) , _autoClose(true) , _closePerUserRequest(false) , _nameTitle(QString()) , _displayTitle(QString()) , _userTitle(QString()) , _localTabTitleFormat(QString()) , _remoteTabTitleFormat(QString()) , _tabTitleSetByUser(false) , _iconName(QString()) , _iconText(QString()) , _addToUtmp(true) , _flowControlEnabled(true) , _program(QString()) , _arguments(QStringList()) , _environment(QStringList()) , _sessionId(0) , _initialWorkingDir(QString()) , _currentWorkingDir(QString()) , _reportedWorkingUrl(QUrl()) , _sessionProcessInfo(nullptr) , _foregroundProcessInfo(nullptr) , _foregroundPid(0) , _zmodemBusy(false) , _zmodemProc(nullptr) , _zmodemProgress(nullptr) , _hasDarkBackground(false) , _preferredSize(QSize()) , _readOnly(false) , _isPrimaryScreen(true) { _uniqueIdentifier = QUuid::createUuid(); //prepare DBus communication new SessionAdaptor(this); _sessionId = ++lastSessionId; QDBusConnection::sessionBus().registerObject(QLatin1String("/Sessions/") + QString::number(_sessionId), this); //create emulation backend _emulation = new Vt102Emulation(); connect(_emulation, &Konsole::Emulation::sessionAttributeChanged, this, &Konsole::Session::setSessionAttribute); connect(_emulation, &Konsole::Emulation::bell, this, [this]() { emit bellRequest(i18n("Bell in session '%1'", _nameTitle)); this->setPendingNotification(Notification::Bell); }); connect(_emulation, &Konsole::Emulation::zmodemDownloadDetected, this, &Konsole::Session::fireZModemDownloadDetected); connect(_emulation, &Konsole::Emulation::zmodemUploadDetected, this, &Konsole::Session::fireZModemUploadDetected); connect(_emulation, &Konsole::Emulation::changeTabTextColorRequest, this, &Konsole::Session::changeTabTextColor); connect(_emulation, &Konsole::Emulation::profileChangeCommandReceived, this, &Konsole::Session::profileChangeCommandReceived); connect(_emulation, &Konsole::Emulation::flowControlKeyPressed, this, &Konsole::Session::updateFlowControlState); connect(_emulation, &Konsole::Emulation::primaryScreenInUse, this, &Konsole::Session::onPrimaryScreenInUse); connect(_emulation, &Konsole::Emulation::selectionChanged, this, &Konsole::Session::selectionChanged); connect(_emulation, &Konsole::Emulation::imageResizeRequest, this, &Konsole::Session::resizeRequest); connect(_emulation, &Konsole::Emulation::sessionAttributeRequest, this, &Konsole::Session::sessionAttributeRequest); //create new teletype for I/O with shell process openTeletype(-1); //setup timer for monitoring session activity & silence _silenceTimer = new QTimer(this); _silenceTimer->setSingleShot(true); connect(_silenceTimer, &QTimer::timeout, this, &Konsole::Session::silenceTimerDone); _activityTimer = new QTimer(this); _activityTimer->setSingleShot(true); connect(_activityTimer, &QTimer::timeout, this, &Konsole::Session::activityTimerDone); } Session::~Session() { delete _foregroundProcessInfo; delete _sessionProcessInfo; delete _emulation; delete _shellProcess; delete _zmodemProc; } void Session::openTeletype(int fd) { if (isRunning()) { qWarning() << "Attempted to open teletype in a running session."; return; } delete _shellProcess; if (fd < 0) { _shellProcess = new Pty(); } else { _shellProcess = new Pty(fd); } _shellProcess->setUtf8Mode(_emulation->utf8()); // connect the I/O between emulator and pty process connect(_shellProcess, &Konsole::Pty::receivedData, this, &Konsole::Session::onReceiveBlock); connect(_emulation, &Konsole::Emulation::sendData, _shellProcess, &Konsole::Pty::sendData); // UTF8 mode connect(_emulation, &Konsole::Emulation::useUtf8Request, _shellProcess, &Konsole::Pty::setUtf8Mode); // get notified when the pty process is finished connect(_shellProcess, QOverload::of(&Konsole::Pty::finished), this, &Konsole::Session::done); // emulator size // Use a direct connection to ensure that the window size is set before it runs connect(_emulation, &Konsole::Emulation::imageSizeChanged, this, &Konsole::Session::updateWindowSize, Qt::DirectConnection); connect(_emulation, &Konsole::Emulation::imageSizeInitialized, this, &Konsole::Session::run); } WId Session::windowId() const { // Returns a window ID for this session which is used // to set the WINDOWID environment variable in the shell // process. // // Sessions can have multiple views or no views, which means // that a single ID is not always going to be accurate. // // If there are no views, the window ID is just 0. If // there are multiple views, then the window ID for the // top-level window which contains the first view is // returned if (_views.count() == 0) { return 0; } else { /** * compute the windows id to use * doesn't call winId on some widget, as this might lead * to rendering artifacts as this will trigger the * creation of a native window, see https://doc.qt.io/qt-5/qwidget.html#winId * instead, use https://doc.qt.io/qt-5/qwidget.html#effectiveWinId */ QWidget* widget = _views.first(); Q_ASSERT(widget); return widget->effectiveWinId(); } } void Session::setDarkBackground(bool darkBackground) { _hasDarkBackground = darkBackground; } bool Session::isRunning() const { return (_shellProcess != nullptr) && (_shellProcess->state() == QProcess::Running); } void Session::setCodec(QTextCodec* codec) { if (isReadOnly()) { return; } emulation()->setCodec(codec); } bool Session::setCodec(const QByteArray& name) { QTextCodec* codec = QTextCodec::codecForName(name); if (codec != nullptr) { setCodec(codec); return true; } else { return false; } } QByteArray Session::codec() { return _emulation->codec()->name(); } void Session::setProgram(const QString& program) { _program = ShellCommand::expand(program); } void Session::setArguments(const QStringList& arguments) { _arguments = ShellCommand::expand(arguments); } void Session::setInitialWorkingDirectory(const QString& dir) { _initialWorkingDir = validDirectory(KShell::tildeExpand(ShellCommand::expand(dir))); } QString Session::currentWorkingDirectory() { if (_reportedWorkingUrl.isValid() && _reportedWorkingUrl.isLocalFile()) { return _reportedWorkingUrl.path(); } // only returned cached value if (_currentWorkingDir.isEmpty()) { updateWorkingDirectory(); } return _currentWorkingDir; } void Session::updateWorkingDirectory() { updateSessionProcessInfo(); const QString currentDir = _sessionProcessInfo->validCurrentDir(); if (currentDir != _currentWorkingDir) { _currentWorkingDir = currentDir; emit currentDirectoryChanged(_currentWorkingDir); } } QList Session::views() const { return _views; } void Session::addView(TerminalDisplay* widget) { Q_ASSERT(!_views.contains(widget)); _views.append(widget); // connect emulation - view signals and slots connect(widget, &Konsole::TerminalDisplay::keyPressedSignal, _emulation, &Konsole::Emulation::sendKeyEvent); connect(widget, &Konsole::TerminalDisplay::mouseSignal, _emulation, &Konsole::Emulation::sendMouseEvent); connect(widget, &Konsole::TerminalDisplay::sendStringToEmu, _emulation, &Konsole::Emulation::sendString); // allow emulation to notify the view when the foreground process // indicates whether or not it is interested in Mouse Tracking events connect(_emulation, &Konsole::Emulation::programRequestsMouseTracking, widget, &Konsole::TerminalDisplay::setUsesMouseTracking); widget->setUsesMouseTracking(_emulation->programUsesMouseTracking()); connect(_emulation, &Konsole::Emulation::enableAlternateScrolling, widget, &Konsole::TerminalDisplay::setAlternateScrolling); connect(_emulation, &Konsole::Emulation::programBracketedPasteModeChanged, widget, &Konsole::TerminalDisplay::setBracketedPasteMode); widget->setBracketedPasteMode(_emulation->programBracketedPasteMode()); widget->setScreenWindow(_emulation->createWindow()); //connect view signals and slots connect(widget, &Konsole::TerminalDisplay::changedContentSizeSignal, this, &Konsole::Session::onViewSizeChange); connect(widget, &Konsole::TerminalDisplay::destroyed, this, &Konsole::Session::viewDestroyed); connect(widget, &Konsole::TerminalDisplay::compositeFocusChanged, _emulation, &Konsole::Emulation::focusChanged); connect(_emulation, &Konsole::Emulation::setCursorStyleRequest, widget, &Konsole::TerminalDisplay::setCursorStyle); connect(_emulation, &Konsole::Emulation::resetCursorStyleRequest, widget, &Konsole::TerminalDisplay::resetCursorStyle); connect(widget, &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::Session::resetNotifications); } void Session::viewDestroyed(QObject* view) { auto* display = reinterpret_cast(view); Q_ASSERT(_views.contains(display)); removeView(display); } void Session::removeView(TerminalDisplay* widget) { _views.removeAll(widget); disconnect(widget, nullptr, this, nullptr); // disconnect // - key presses signals from widget // - mouse activity signals from widget // - string sending signals from widget // // ... and any other signals connected in addView() disconnect(widget, nullptr, _emulation, nullptr); // disconnect state change signals emitted by emulation disconnect(_emulation, nullptr, widget, nullptr); // close the session automatically when the last view is removed if (_views.count() == 0) { close(); } } // Upon a KPty error, there is no description on what that error was... // Check to see if the given program is executable. QString Session::checkProgram(const QString& program) { QString exec = program; if (exec.isEmpty()) { return QString(); } QFileInfo info(exec); if (info.isAbsolute() && info.exists() && info.isExecutable()) { return exec; } exec = KIO::DesktopExecParser::executablePath(exec); exec = KShell::tildeExpand(exec); const QString pexec = QStandardPaths::findExecutable(exec); if (pexec.isEmpty()) { qCritical() << i18n("Could not find binary: ") << exec; return QString(); } return exec; } void Session::terminalWarning(const QString& message) { static const QByteArray warningText = i18nc("@info:shell Alert the user with red color text", "Warning: ").toLocal8Bit(); QByteArray messageText = message.toLocal8Bit(); static const char redPenOn[] = "\033[1m\033[31m"; static const char redPenOff[] = "\033[0m"; _emulation->receiveData(redPenOn, qstrlen(redPenOn)); _emulation->receiveData("\n\r\n\r", 4); _emulation->receiveData(warningText.constData(), qstrlen(warningText.constData())); _emulation->receiveData(messageText.constData(), qstrlen(messageText.constData())); _emulation->receiveData("\n\r\n\r", 4); _emulation->receiveData(redPenOff, qstrlen(redPenOff)); } QString Session::shellSessionId() const { QString friendlyUuid(_uniqueIdentifier.toString()); friendlyUuid.remove(QLatin1Char('-')).remove(QLatin1Char('{')).remove(QLatin1Char('}')); return friendlyUuid; } void Session::run() { // FIXME: run() is called twice in some instances if (isRunning()) { qCDebug(KonsoleDebug) << "Attempted to re-run an already running session (" << _shellProcess->pid() << ")"; return; } //check that everything is in place to run the session if (_program.isEmpty()) { qWarning() << "Program to run not set."; } if (_arguments.isEmpty()) { qWarning() << "No command line arguments specified."; } if (_uniqueIdentifier.isNull()) { _uniqueIdentifier = QUuid::createUuid(); } const int CHOICE_COUNT = 3; // if '_program' is empty , fall back to default shell. If that is not set // then fall back to /bin/sh QString programs[CHOICE_COUNT] = {_program, QString::fromUtf8(qgetenv("SHELL")), QStringLiteral("/bin/sh")}; QString exec; int choice = 0; while (choice < CHOICE_COUNT) { exec = checkProgram(programs[choice]); if (exec.isEmpty()) { choice++; } else { break; } } // if a program was specified via setProgram(), but it couldn't be found, print a warning if (choice != 0 && choice < CHOICE_COUNT && !_program.isEmpty()) { terminalWarning(i18n("Could not find '%1', starting '%2' instead. Please check your profile settings.", _program, exec)); // if none of the choices are available, print a warning } else if (choice == CHOICE_COUNT) { terminalWarning(i18n("Could not find an interactive shell to start.")); return; } // if no arguments are specified, fall back to program name QStringList arguments = _arguments.join(QLatin1Char(' ')).isEmpty() ? QStringList() << exec : _arguments; if (!_initialWorkingDir.isEmpty()) { _shellProcess->setInitialWorkingDirectory(_initialWorkingDir); } else { _shellProcess->setInitialWorkingDirectory(QDir::currentPath()); } _shellProcess->setFlowControlEnabled(_flowControlEnabled); _shellProcess->setEraseChar(_emulation->eraseChar()); _shellProcess->setUseUtmp(_addToUtmp); // this is not strictly accurate use of the COLORFGBG variable. This does not // tell the terminal exactly which colors are being used, but instead approximates // the color scheme as "black on white" or "white on black" depending on whether // the background color is deemed dark or not const QString backgroundColorHint = _hasDarkBackground ? QStringLiteral("COLORFGBG=15;0") : QStringLiteral("COLORFGBG=0;15"); addEnvironmentEntry(backgroundColorHint); addEnvironmentEntry(QStringLiteral("SHELL_SESSION_ID=%1").arg(shellSessionId())); addEnvironmentEntry(QStringLiteral("WINDOWID=%1").arg(QString::number(windowId()))); const QString dbusService = QDBusConnection::sessionBus().baseService(); addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_SERVICE=%1").arg(dbusService)); const QString dbusObject = QStringLiteral("/Sessions/%1").arg(QString::number(_sessionId)); addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_SESSION=%1").arg(dbusObject)); int result = _shellProcess->start(exec, arguments, _environment); if (result < 0) { terminalWarning(i18n("Could not start program '%1' with arguments '%2'.", exec, arguments.join(QLatin1Char(' ')))); terminalWarning(_shellProcess->errorString()); return; } _shellProcess->setWriteable(false); // We are reachable via kwrited. emit started(); } void Session::setSessionAttribute(int what, const QString& caption) { // set to true if anything has actually changed // eg. old _nameTitle != new _nameTitle bool modified = false; if ((what == IconNameAndWindowTitle) || (what == WindowTitle)) { if (_userTitle != caption) { _userTitle = caption; modified = true; } } if ((what == IconNameAndWindowTitle) || (what == IconName)) { if (_iconText != caption) { _iconText = caption; modified = true; } } if (what == TextColor || what == BackgroundColor) { QString colorString = caption.section(QLatin1Char(';'), 0, 0); QColor color = QColor(colorString); if (color.isValid()) { if (what == TextColor) { emit changeForegroundColorRequest(color); } else { emit changeBackgroundColorRequest(color); } } } if (what == SessionName) { if (_localTabTitleFormat != caption) { _localTabTitleFormat = caption; setTitle(Session::DisplayedTitleRole, caption); modified = true; } } /* The below use of 32 works but appears to non-standard. It is from a commit from 2004 c20973eca8776f9b4f15bee5fdcb5a3205aa69de */ // change icon via \033]32;Icon\007 if (what == SessionIcon) { if (_iconName != caption) { _iconName = caption; modified = true; } } if (what == CurrentDirectory) { _reportedWorkingUrl = QUrl::fromUserInput(caption); emit currentDirectoryChanged(currentWorkingDirectory()); modified = true; } if (what == ProfileChange) { emit profileChangeCommandReceived(caption); return; } if (modified) { emit sessionAttributeChanged(); } } QString Session::userTitle() const { return _userTitle; } void Session::setTabTitleFormat(TabTitleContext context , const QString& format) { if (context == LocalTabTitle) { _localTabTitleFormat = format; ProcessInfo* process = getProcessInfo(); process->setUserNameRequired(format.contains(QLatin1String("%u"))); } else if (context == RemoteTabTitle) { _remoteTabTitleFormat = format; } } QString Session::tabTitleFormat(TabTitleContext context) const { if (context == LocalTabTitle) { return _localTabTitleFormat; } else if (context == RemoteTabTitle) { return _remoteTabTitleFormat; } return QString(); } void Session::tabTitleSetByUser(bool set) { _tabTitleSetByUser = set; } bool Session::isTabTitleSetByUser() const { return _tabTitleSetByUser; } void Session::silenceTimerDone() { //FIXME: The idea here is that the notification popup will appear to tell the user than output from //the terminal has stopped and the popup will disappear when the user activates the session. // //This breaks with the addition of multiple views of a session. The popup should disappear //when any of the views of the session becomes active //FIXME: Make message text for this notification and the activity notification more descriptive. if (!_monitorSilence) { setPendingNotification(Notification::Silence, false); return; } bool hasFocus = false; for (const TerminalDisplay *display : qAsConst(_views)) { if (display->hasFocus()) { hasFocus = true; break; } } KNotification::event(hasFocus ? QStringLiteral("Silence") : QStringLiteral("SilenceHidden"), i18n("Silence in session '%1'", _nameTitle), QPixmap(), QApplication::activeWindow(), KNotification::CloseWhenWidgetActivated); setPendingNotification(Notification::Silence); } void Session::activityTimerDone() { _notifiedActivity = false; } void Session::resetNotifications() { static const Notification availableNotifications[] = {Activity, Silence, Bell}; for (auto notification: availableNotifications) { setPendingNotification(notification, false); } } void Session::updateFlowControlState(bool suspended) { if (suspended) { if (flowControlEnabled()) { for (TerminalDisplay *display : qAsConst(_views)) { if (display->flowControlWarningEnabled()) { display->outputSuspended(true); } } } } else { for (TerminalDisplay *display : qAsConst(_views)) { display->outputSuspended(false); } } } void Session::changeTabTextColor(int i) { qCDebug(KonsoleDebug) << "Changing tab text color is not implemented "<isHidden() && view->lines() >= VIEW_LINES_THRESHOLD && view->columns() >= VIEW_COLUMNS_THRESHOLD) { minLines = (minLines == -1) ? view->lines() : qMin(minLines , view->lines()); minColumns = (minColumns == -1) ? view->columns() : qMin(minColumns , view->columns()); view->processFilters(); } } // backend emulation must have a _terminal of at least 1 column x 1 line in size if (minLines > 0 && minColumns > 0) { _emulation->setImageSize(minLines , minColumns); } } void Session::updateWindowSize(int lines, int columns) { Q_ASSERT(lines > 0 && columns > 0); _shellProcess->setWindowSize(columns, lines); } void Session::refresh() { // attempt to get the shell process to redraw the display // // this requires the program running in the shell // to cooperate by sending an update in response to // a window size change // // the window size is changed twice, first made slightly larger and then // resized back to its normal size so that there is actually a change // in the window size (some shells do nothing if the // new and old sizes are the same) // // if there is a more 'correct' way to do this, please // send an email with method or patches to konsole-devel@kde.org const QSize existingSize = _shellProcess->windowSize(); _shellProcess->setWindowSize(existingSize.width() + 1, existingSize.height()); // introduce small delay to avoid changing size too quickly QThread::usleep(500); _shellProcess->setWindowSize(existingSize.width(), existingSize.height()); } void Session::sendSignal(int signal) { const ProcessInfo* process = getProcessInfo(); bool ok = false; int pid; pid = process->foregroundPid(&ok); if (ok) { ::kill(pid, signal); } } void Session::reportBackgroundColor(const QColor& c) { #define to65k(a) (QStringLiteral("%1").arg(int(((a)*0xFFFF)), 4, 16, QLatin1Char('0'))) QString msg = QStringLiteral("\033]11;rgb:") + to65k(c.redF()) + QLatin1Char('/') + to65k(c.greenF()) + QLatin1Char('/') + to65k(c.blueF()) + QLatin1Char('\a'); _emulation->sendString(msg.toUtf8()); #undef to65k } bool Session::kill(int signal) { if (_shellProcess->pid() <= 0) { return false; } int result = ::kill(_shellProcess->pid(), signal); if (result == 0) { return _shellProcess->waitForFinished(1000); } else { return false; } } void Session::close() { if (isRunning()) { if (!closeInNormalWay()) { closeInForceWay(); } } else { // terminal process has finished, just close the session QTimer::singleShot(1, this, &Konsole::Session::finished); } } bool Session::closeInNormalWay() { _autoClose = true; _closePerUserRequest = true; // for the possible case where following events happen in sequence: // // 1). the terminal process crashes // 2). the tab stays open and displays warning message // 3). the user closes the tab explicitly // if (!isRunning()) { emit finished(); return true; } static QSet knownShells({QStringLiteral("ash"), QStringLiteral("bash"), QStringLiteral("csh"), QStringLiteral("dash"), QStringLiteral("fish"), QStringLiteral("hush"), QStringLiteral("ksh"), QStringLiteral("mksh"), QStringLiteral("pdksh"), QStringLiteral("tcsh"), QStringLiteral("zsh")}); // If only the session's shell is running, try sending an EOF for a clean exit if (!isForegroundProcessActive() && knownShells.contains(QFileInfo(_program).fileName())) { _shellProcess->sendEof(); if (_shellProcess->waitForFinished(1000)) { return true; } qWarning() << "shell did not close, sending SIGHUP"; } // We tried asking nicely, ask a bit less nicely if (kill(SIGHUP)) { return true; } else { qWarning() << "Process " << _shellProcess->pid() << " did not die with SIGHUP"; _shellProcess->closePty(); return (_shellProcess->waitForFinished(1000)); } } bool Session::closeInForceWay() { _autoClose = true; _closePerUserRequest = true; if (kill(SIGKILL)) { return true; } else { qWarning() << "Process " << _shellProcess->pid() << " did not die with SIGKILL"; return false; } } void Session::sendTextToTerminal(const QString& text, const QChar& eol) const { if (isReadOnly()) { return; } if (eol.isNull()) { _emulation->sendText(text); } else { _emulation->sendText(text + eol); } } // Only D-Bus calls this function (via SendText or runCommand) void Session::sendText(const QString& text) const { if (isReadOnly()) { return; } #if !defined(REMOVE_SENDTEXT_RUNCOMMAND_DBUS_METHODS) if (show_disallow_certain_dbus_methods_message) { KNotification::event(KNotification::Warning, QStringLiteral("Konsole D-Bus Warning"), i18n("The D-Bus methods sendText/runCommand were just used. There are security concerns about allowing these methods to be public. If desired, these methods can be changed to internal use only by re-compiling Konsole.

This warning will only show once for this Konsole instance.

")); show_disallow_certain_dbus_methods_message = false; } #endif _emulation->sendText(text); } // Only D-Bus calls this function void Session::runCommand(const QString& command) const { sendText(command + QLatin1Char('\n')); } void Session::sendMouseEvent(int buttons, int column, int line, int eventType) { _emulation->sendMouseEvent(buttons, column, line, eventType); } void Session::done(int exitCode, QProcess::ExitStatus exitStatus) { // This slot should be triggered only one time disconnect(_shellProcess, QOverload::of(&Konsole::Pty::finished), this, &Konsole::Session::done); if (!_autoClose) { _userTitle = i18nc("@info:shell This session is done", "Finished"); emit sessionAttributeChanged(); return; } if (_closePerUserRequest) { emit finished(); return; } QString message; if (exitCode != 0) { if (exitStatus != QProcess::NormalExit) { message = i18n("Program '%1' crashed.", _program); } else { message = i18n("Program '%1' exited with status %2.", _program, exitCode); } //FIXME: See comments in Session::silenceTimerDone() KNotification::event(QStringLiteral("Finished"), message , QPixmap(), QApplication::activeWindow(), KNotification::CloseWhenWidgetActivated); } if (exitStatus != QProcess::NormalExit) { // this seeming duplicated line is for the case when exitCode is 0 message = i18n("Program '%1' crashed.", _program); terminalWarning(message); } else { emit finished(); } } Emulation* Session::emulation() const { return _emulation; } QString Session::keyBindings() const { return _emulation->keyBindings(); } QStringList Session::environment() const { return _environment; } void Session::setEnvironment(const QStringList& environment) { if (isReadOnly()) { return; } _environment = environment; } void Session::addEnvironmentEntry(const QString& entry) { _environment << entry; } int Session::sessionId() const { return _sessionId; } void Session::setKeyBindings(const QString& name) { _emulation->setKeyBindings(name); } void Session::setTitle(TitleRole role , const QString& newTitle) { if (title(role) != newTitle) { if (role == NameRole) { _nameTitle = newTitle; } else if (role == DisplayedTitleRole) { _displayTitle = newTitle; } emit sessionAttributeChanged(); } } QString Session::title(TitleRole role) const { if (role == NameRole) { return _nameTitle; } else if (role == DisplayedTitleRole) { return _displayTitle; } else { return QString(); } } ProcessInfo* Session::getProcessInfo() { ProcessInfo* process = nullptr; if (isForegroundProcessActive() && updateForegroundProcessInfo()) { process = _foregroundProcessInfo; } else { updateSessionProcessInfo(); process = _sessionProcessInfo; } return process; } void Session::updateSessionProcessInfo() { Q_ASSERT(_shellProcess); bool ok; // The checking for pid changing looks stupid, but it is needed // at the moment to workaround the problem that processId() might // return 0 if ((_sessionProcessInfo == nullptr) || (processId() != 0 && processId() != _sessionProcessInfo->pid(&ok))) { delete _sessionProcessInfo; _sessionProcessInfo = ProcessInfo::newInstance(processId()); _sessionProcessInfo->setUserHomeDir(); } _sessionProcessInfo->update(); } bool Session::updateForegroundProcessInfo() { Q_ASSERT(_shellProcess); const int foregroundPid = _shellProcess->foregroundProcessGroup(); if (foregroundPid != _foregroundPid) { delete _foregroundProcessInfo; _foregroundProcessInfo = ProcessInfo::newInstance(foregroundPid); _foregroundPid = foregroundPid; } if (_foregroundProcessInfo != nullptr) { _foregroundProcessInfo->update(); return _foregroundProcessInfo->isValid(); } else { return false; } } bool Session::isRemote() { ProcessInfo* process = getProcessInfo(); bool ok = false; return (process->name(&ok) == QLatin1String("ssh") && ok); } QString Session::getDynamicTitle() { ProcessInfo* process = getProcessInfo(); // format tab titles using process info bool ok = false; if (process->name(&ok) == QLatin1String("ssh") && ok) { SSHProcessInfo sshInfo(*process); return sshInfo.format(tabTitleFormat(Session::RemoteTabTitle)); } /* * Parses an input string, looking for markers beginning with a '%' * character and returns a string with the markers replaced * with information from this process description. *
* The markers recognized are: *
    *
  • %B - User's Bourne prompt sigil ($, or # for superuser).
  • *
  • %u - Name of the user which owns the process.
  • *
  • %n - Replaced with the name of the process.
  • *
  • %d - Replaced with the last part of the path name of the * process' current working directory. * * (eg. if the current directory is '/home/bob' then * 'bob' would be returned) *
  • *
  • %D - Replaced with the current working directory of the process.
  • *
*/ QString title = tabTitleFormat(Session::LocalTabTitle); // search for and replace known marker int UID = process->userId(&ok); if(!ok) { title.replace(QLatin1String("%B"), QStringLiteral("-")); } else { //title.replace(QLatin1String("%I"), QString::number(UID)); if (UID == 0) { title.replace(QLatin1String("%B"), QStringLiteral("#")); } else { title.replace(QLatin1String("%B"), QStringLiteral("$")); } } title.replace(QLatin1String("%u"), process->userName()); title.replace(QLatin1String("%h"), Konsole::ProcessInfo::localHost()); title.replace(QLatin1String("%n"), process->name(&ok)); QString dir = _reportedWorkingUrl.toLocalFile(); ok = true; if (dir.isEmpty()) { // update current directory from process updateWorkingDirectory(); dir = process->currentDir(&ok); } if(!ok) { title.replace(QLatin1String("%d"), QStringLiteral("-")); title.replace(QLatin1String("%D"), QStringLiteral("-")); } else { // allow for shortname to have the ~ as homeDir const QString homeDir = process->userHomeDir(); if (!homeDir.isEmpty()) { if (dir.startsWith(homeDir)) { dir.remove(0, homeDir.length()); dir.prepend(QLatin1Char('~')); } } title.replace(QLatin1String("%D"), dir); title.replace(QLatin1String("%d"), process->formatShortDir(dir)); } return title; } QUrl Session::getUrl() { if (_reportedWorkingUrl.isValid()) { return _reportedWorkingUrl; } QString path; updateSessionProcessInfo(); if (_sessionProcessInfo->isValid()) { bool ok = false; // check if foreground process is bookmark-able if (isForegroundProcessActive() && _foregroundProcessInfo->isValid()) { // for remote connections, save the user and host // bright ideas to get the directory at the other end are welcome :) if (_foregroundProcessInfo->name(&ok) == QLatin1String("ssh") && ok) { SSHProcessInfo sshInfo(*_foregroundProcessInfo); QUrl url; url.setScheme(QStringLiteral("ssh")); url.setUserName(sshInfo.userName()); url.setHost(sshInfo.host()); const QString port = sshInfo.port(); if (!port.isEmpty() && port != QLatin1String("22")) { url.setPort(port.toInt()); } return url; } else { path = _foregroundProcessInfo->currentDir(&ok); if (!ok) { path.clear(); } } } else { // otherwise use the current working directory of the shell process path = _sessionProcessInfo->currentDir(&ok); if (!ok) { path.clear(); } } } return QUrl::fromLocalFile(path); } void Session::setIconName(const QString& iconName) { if (iconName != _iconName) { _iconName = iconName; emit sessionAttributeChanged(); } } void Session::setIconText(const QString& iconText) { _iconText = iconText; } QString Session::iconName() const { - return isReadOnly() ? QStringLiteral("object-locked") : _iconName; + return _iconName; } QString Session::iconText() const { return _iconText; } void Session::setHistoryType(const HistoryType& hType) { _emulation->setHistory(hType); } const HistoryType& Session::historyType() const { return _emulation->history(); } void Session::clearHistory() { _emulation->clearHistory(); } QStringList Session::arguments() const { return _arguments; } QString Session::program() const { return _program; } bool Session::isMonitorActivity() const { return _monitorActivity; } bool Session::isMonitorSilence() const { return _monitorSilence; } void Session::setMonitorActivity(bool monitor) { if (_monitorActivity == monitor) { return; } _monitorActivity = monitor; _notifiedActivity = false; // This timer is meaningful only after activity has been notified _activityTimer->stop(); setPendingNotification(Notification::Activity, false); } void Session::setMonitorSilence(bool monitor) { if (_monitorSilence == monitor) { return; } _monitorSilence = monitor; if (_monitorSilence) { _silenceTimer->start(_silenceSeconds * 1000); } else { _silenceTimer->stop(); } setPendingNotification(Notification::Silence, false); } void Session::setMonitorSilenceSeconds(int seconds) { _silenceSeconds = seconds; if (_monitorSilence) { _silenceTimer->start(_silenceSeconds * 1000); } } void Session::setAddToUtmp(bool add) { _addToUtmp = add; } void Session::setAutoClose(bool close) { _autoClose = close; } bool Session::autoClose() const { return _autoClose; } void Session::setFlowControlEnabled(bool enabled) { if (isReadOnly()) { return; } _flowControlEnabled = enabled; if (_shellProcess != nullptr) { _shellProcess->setFlowControlEnabled(_flowControlEnabled); } emit flowControlEnabledChanged(enabled); } bool Session::flowControlEnabled() const { if (_shellProcess != nullptr) { return _shellProcess->flowControlEnabled(); } else { return _flowControlEnabled; } } void Session::fireZModemDownloadDetected() { if (!_zmodemBusy) { QTimer::singleShot(10, this, &Konsole::Session::zmodemDownloadDetected); _zmodemBusy = true; } } void Session::fireZModemUploadDetected() { if (!_zmodemBusy) { QTimer::singleShot(10, this, &Konsole::Session::zmodemUploadDetected); } } void Session::cancelZModem() { _shellProcess->sendData(QByteArrayLiteral("\030\030\030\030")); // Abort _zmodemBusy = false; } void Session::startZModem(const QString& zmodem, const QString& dir, const QStringList& list) { _zmodemBusy = true; _zmodemProc = new KProcess(); _zmodemProc->setOutputChannelMode(KProcess::SeparateChannels); *_zmodemProc << zmodem << QStringLiteral("-v") << QStringLiteral("-e") << list; if (!dir.isEmpty()) { _zmodemProc->setWorkingDirectory(dir); } connect(_zmodemProc, &KProcess::readyReadStandardOutput, this, &Konsole::Session::zmodemReadAndSendBlock); connect(_zmodemProc, &KProcess::readyReadStandardError, this, &Konsole::Session::zmodemReadStatus); connect(_zmodemProc, QOverload::of(&KProcess::finished), this, &Konsole::Session::zmodemFinished); _zmodemProc->start(); disconnect(_shellProcess, &Konsole::Pty::receivedData, this, &Konsole::Session::onReceiveBlock); connect(_shellProcess, &Konsole::Pty::receivedData, this, &Konsole::Session::zmodemReceiveBlock); _zmodemProgress = new ZModemDialog(QApplication::activeWindow(), false, i18n("ZModem Progress")); connect(_zmodemProgress, &Konsole::ZModemDialog::zmodemCancel, this, &Konsole::Session::zmodemFinished); _zmodemProgress->show(); } void Session::zmodemReadAndSendBlock() { _zmodemProc->setReadChannel(QProcess::StandardOutput); QByteArray data = _zmodemProc->read(ZMODEM_BUFFER_SIZE); while (data.count() != 0) { _shellProcess->sendData(data); data = _zmodemProc->read(ZMODEM_BUFFER_SIZE); } } void Session::zmodemReadStatus() { _zmodemProc->setReadChannel(QProcess::StandardError); QByteArray msg = _zmodemProc->readAll(); while (!msg.isEmpty()) { int i = msg.indexOf('\015'); int j = msg.indexOf('\012'); QByteArray txt; if ((i != -1) && ((j == -1) || (i < j))) { msg = msg.mid(i + 1); } else if (j != -1) { txt = msg.left(j); msg = msg.mid(j + 1); } else { txt = msg; msg.truncate(0); } if (!txt.isEmpty()) { _zmodemProgress->addText(QString::fromLocal8Bit(txt)); } } } void Session::zmodemReceiveBlock(const char* data, int len) { static int steps = 0; QByteArray bytes(data, len); _zmodemProc->write(bytes); // Provide some feedback to dialog if (steps > 100) { _zmodemProgress->addProgressText(QStringLiteral(".")); steps = 0; } steps++; } void Session::zmodemFinished() { /* zmodemFinished() is called by QProcess's finished() and ZModemDialog's user1Clicked(). Therefore, an invocation by user1Clicked() will recursively invoke this function again when the KProcess is deleted! */ if (_zmodemProc != nullptr) { KProcess* process = _zmodemProc; _zmodemProc = nullptr; // Set _zmodemProc to 0 avoid recursive invocations! _zmodemBusy = false; delete process; // Now, the KProcess may be disposed safely. disconnect(_shellProcess, &Konsole::Pty::receivedData, this , &Konsole::Session::zmodemReceiveBlock); connect(_shellProcess, &Konsole::Pty::receivedData, this, &Konsole::Session::onReceiveBlock); _shellProcess->sendData(QByteArrayLiteral("\030\030\030\030")); // Abort _shellProcess->sendData(QByteArrayLiteral("\001\013\n")); // Try to get prompt back _zmodemProgress->transferDone(); } } void Session::onReceiveBlock(const char* buf, int len) { handleActivity(); _emulation->receiveData(buf, len); } QSize Session::size() { return _emulation->imageSize(); } void Session::setSize(const QSize& size) { if ((size.width() <= 1) || (size.height() <= 1)) { return; } emit resizeRequest(size); } QSize Session::preferredSize() const { return _preferredSize; } void Session::setPreferredSize(const QSize& size) { _preferredSize = size; } int Session::processId() const { return _shellProcess->pid(); } void Session::setTitle(int role , const QString& title) { switch (role) { case 0: setTitle(Session::NameRole, title); break; case 1: setTitle(Session::DisplayedTitleRole, title); // without these, that title will be overridden by the expansion of // title format shortly after, which will confuses users. _localTabTitleFormat = title; _remoteTabTitleFormat = title; break; } } QString Session::title(int role) const { switch (role) { case 0: return title(Session::NameRole); case 1: return title(Session::DisplayedTitleRole); default: return QString(); } } void Session::setTabTitleFormat(int context , const QString& format) { switch (context) { case 0: setTabTitleFormat(Session::LocalTabTitle, format); break; case 1: setTabTitleFormat(Session::RemoteTabTitle, format); break; } } QString Session::tabTitleFormat(int context) const { switch (context) { case 0: return tabTitleFormat(Session::LocalTabTitle); case 1: return tabTitleFormat(Session::RemoteTabTitle); default: return QString(); } } void Session::setHistorySize(int lines) { if (isReadOnly()) { return; } if (lines < 0) { setHistoryType(HistoryTypeFile()); } else if (lines == 0) { setHistoryType(HistoryTypeNone()); } else { setHistoryType(CompactHistoryType(lines)); } } int Session::historySize() const { const HistoryType& currentHistory = historyType(); if (currentHistory.isEnabled()) { if (currentHistory.isUnlimited()) { return -1; } else { return currentHistory.maximumLineCount(); } } else { return 0; } } QString Session::profile() { return SessionManager::instance()->sessionProfile(this)->name(); } void Session::setProfile(const QString &profileName) { const QList profiles = ProfileManager::instance()->allProfiles(); for (const Profile::Ptr &profile : profiles) { if (profile->name() == profileName) { SessionManager::instance()->setSessionProfile(this, profile); } } } int Session::foregroundProcessId() { int pid; bool ok = false; pid = getProcessInfo()->pid(&ok); if (!ok) { pid = -1; } return pid; } bool Session::isForegroundProcessActive() { // foreground process info is always updated after this return (_shellProcess->pid() != _shellProcess->foregroundProcessGroup()); } QString Session::foregroundProcessName() { QString name; if (updateForegroundProcessInfo()) { bool ok = false; name = _foregroundProcessInfo->name(&ok); if (!ok) { name.clear(); } } return name; } void Session::saveSession(KConfigGroup& group) { group.writePathEntry("WorkingDir", currentWorkingDirectory()); group.writeEntry("LocalTab", tabTitleFormat(LocalTabTitle)); group.writeEntry("RemoteTab", tabTitleFormat(RemoteTabTitle)); group.writeEntry("SessionGuid", _uniqueIdentifier.toString()); group.writeEntry("Encoding", QString::fromUtf8(codec())); } void Session::restoreSession(KConfigGroup& group) { QString value; value = group.readPathEntry("WorkingDir", QString()); if (!value.isEmpty()) { setInitialWorkingDirectory(value); } value = group.readEntry("LocalTab"); if (!value.isEmpty()) { setTabTitleFormat(LocalTabTitle, value); } value = group.readEntry("RemoteTab"); if (!value.isEmpty()) { setTabTitleFormat(RemoteTabTitle, value); } value = group.readEntry("SessionGuid"); if (!value.isEmpty()) { _uniqueIdentifier = QUuid(value); } value = group.readEntry("Encoding"); if (!value.isEmpty()) { setCodec(value.toUtf8()); } } QString Session::validDirectory(const QString& dir) const { QString validDir = dir; if (validDir.isEmpty()) { validDir = QDir::currentPath(); } const QFileInfo fi(validDir); if (!fi.exists() || !fi.isDir()) { validDir = QDir::homePath(); } return validDir; } void Session::setPendingNotification(Session::Notification notification, bool enable) { if(enable != _activeNotifications.testFlag(notification)) { _activeNotifications.setFlag(notification, enable); emit notificationsChanged(notification, enable); } } void Session::handleActivity() { // TODO: should this hardcoded interval be user configurable? const int activityMaskInSeconds = 15; bool hasFocus = false; // Don't notify if the terminal is active const auto &displays = _views; for (const TerminalDisplay *display: displays) { if (display->hasFocus()) { hasFocus = true; break; } } if (_monitorActivity && !_notifiedActivity) { KNotification::event(hasFocus ? QStringLiteral("Activity") : QStringLiteral("ActivityHidden"), i18n("Activity in session '%1'", _nameTitle), QPixmap(), QApplication::activeWindow(), KNotification::CloseWhenWidgetActivated); // mask activity notification for a while to avoid flooding _notifiedActivity = true; _activityTimer->start(activityMaskInSeconds * 1000); } // reset the counter for monitoring continuous silence since there is activity if (_monitorSilence) { _silenceTimer->start(_silenceSeconds * 1000); } if (_monitorActivity) { setPendingNotification(Notification::Activity); } } bool Session::isReadOnly() const { return _readOnly; } void Session::setReadOnly(bool readOnly) { if (_readOnly != readOnly) { _readOnly = readOnly; // Needed to update the tab icons and all // attached views. emit readOnlyChanged(); } } SessionGroup::SessionGroup(QObject* parent) : QObject(parent), _masterMode(0) { } SessionGroup::~SessionGroup() = default; int SessionGroup::masterMode() const { return _masterMode; } QList SessionGroup::sessions() const { return _sessions.keys(); } bool SessionGroup::masterStatus(Session* session) const { return _sessions[session]; } void SessionGroup::addSession(Session* session) { connect(session, &Konsole::Session::finished, this, &Konsole::SessionGroup::sessionFinished); _sessions.insert(session, false); } void SessionGroup::removeSession(Session* session) { disconnect(session, &Konsole::Session::finished, this, &Konsole::SessionGroup::sessionFinished); setMasterStatus(session, false); _sessions.remove(session); } void SessionGroup::sessionFinished() { auto* session = qobject_cast(sender()); Q_ASSERT(session); removeSession(session); } void SessionGroup::setMasterMode(int mode) { _masterMode = mode; } QList SessionGroup::masters() const { return _sessions.keys(true); } void SessionGroup::setMasterStatus(Session* session , bool master) { const bool wasMaster = _sessions[session]; if (wasMaster == master) { // No status change -> nothing to do. return; } _sessions[session] = master; if (master) { connect(session->emulation(), &Konsole::Emulation::sendData, this, &Konsole::SessionGroup::forwardData); } else { disconnect(session->emulation(), &Konsole::Emulation::sendData, this, &Konsole::SessionGroup::forwardData); } } void SessionGroup::forwardData(const QByteArray& data) { static bool _inForwardData = false; if (_inForwardData) { // Avoid recursive calls among session groups! // A recursive call happens when a master in group A calls forwardData() // in group B. If one of the destination sessions in group B is also a // master of a group including the master session of group A, this would // again call forwardData() in group A, and so on. return; } _inForwardData = true; const QList sessionsKeys = _sessions.keys(); for (Session *other : sessionsKeys) { if (!_sessions[other]) { other->emulation()->sendString(data); } } _inForwardData = false; } diff --git a/src/SessionController.cpp b/src/SessionController.cpp index 4a6ab0d4..77d11ef6 100644 --- a/src/SessionController.cpp +++ b/src/SessionController.cpp @@ -1,1810 +1,1773 @@ /* Copyright 2006-2008 by Robert Knight Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "SessionController.h" #include "ProfileManager.h" #include "konsoledebug.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Konsole #include "EditProfileDialog.h" #include "CopyInputDialog.h" #include "Emulation.h" #include "Filter.h" #include "History.h" #include "HistorySizeDialog.h" #include "IncrementalSearchBar.h" #include "RenameTabDialog.h" #include "ScreenWindow.h" #include "Session.h" #include "ProfileList.h" #include "TerminalDisplay.h" #include "SessionManager.h" #include "Enumeration.h" #include "PrintOptions.h" #include "SaveHistoryTask.h" #include "SearchHistoryTask.h" // For Unix signal names #include using namespace Konsole; -// TODO - Replace the icon choices below when suitable icons for silence and -// activity are available -Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _activityIcon, (QIcon::fromTheme(QLatin1String("dialog-information")))) -Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _silenceIcon, (QIcon::fromTheme(QLatin1String("dialog-information")))) -Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _bellIcon, (QIcon::fromTheme(QLatin1String("preferences-desktop-notification-bell")))) -Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _broadcastIcon, (QIcon::fromTheme(QLatin1String("emblem-important")))) - QSet SessionController::_allControllers; int SessionController::_lastControllerId; SessionController::SessionController(Session* session , TerminalDisplay* view, QObject* parent) : ViewProperties(parent) , KXMLGUIClient() , _session(session) , _view(view) , _copyToGroup(nullptr) , _profileList(nullptr) , _sessionIcon(QIcon()) , _sessionIconName(QString()) , _previousState(-1) , _searchFilter(nullptr) , _urlFilter(nullptr) , _fileFilter(nullptr) , _copyInputToAllTabsAction(nullptr) , _findAction(nullptr) , _findNextAction(nullptr) , _findPreviousAction(nullptr) , _interactionTimer(nullptr) , _searchStartLine(0) , _prevSearchResultLine(0) , _codecAction(nullptr) , _switchProfileMenu(nullptr) , _webSearchMenu(nullptr) , _listenForScreenWindowUpdates(false) , _preventClose(false) - , _keepIconUntilInteraction(false) , _selectedText(QString()) , _showMenuAction(nullptr) , _bookmarkValidProgramsToClear(QStringList()) , _isSearchBarEnabled(false) , _editProfileDialog(nullptr) , _searchBar(view->searchBar()) { Q_ASSERT(session); Q_ASSERT(view); // handle user interface related to session (menus etc.) if (isKonsolePart()) { setComponentName(QStringLiteral("konsole"), i18n("Konsole")); setXMLFile(QStringLiteral("partui.rc")); setupCommonActions(); } else { setXMLFile(QStringLiteral("sessionui.rc")); setupCommonActions(); setupExtraActions(); } actionCollection()->addAssociatedWidget(view); foreach(QAction * action, actionCollection()->actions()) { action->setShortcutContext(Qt::WidgetWithChildrenShortcut); } setIdentifier(++_lastControllerId); sessionAttributeChanged(); connect(_view, &TerminalDisplay::compositeFocusChanged, this, &SessionController::viewFocusChangeHandler); _view->setSessionController(this); // install filter on the view to highlight URLs and files updateFilterList(SessionManager::instance()->sessionProfile(_session)); // listen for changes in session, we might need to change the enabled filters connect(ProfileManager::instance(), &Konsole::ProfileManager::profileChanged, this, &Konsole::SessionController::updateFilterList); // listen for session resize requests connect(_session.data(), &Konsole::Session::resizeRequest, this, &Konsole::SessionController::sessionResizeRequest); // listen for popup menu requests connect(_view.data(), &Konsole::TerminalDisplay::configureRequest, this, &Konsole::SessionController::showDisplayContextMenu); // move view to newest output when keystrokes occur connect(_view.data(), &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::SessionController::trackOutput); // listen to activity / silence notifications from session connect(_session.data(), &Konsole::Session::notificationsChanged, this, &Konsole::SessionController::sessionNotificationsChanged); // listen to title and icon changes connect(_session.data(), &Konsole::Session::sessionAttributeChanged, this, &Konsole::SessionController::sessionAttributeChanged); connect(_session.data(), &Konsole::Session::readOnlyChanged, this, &Konsole::SessionController::sessionReadOnlyChanged); connect(this, &Konsole::SessionController::tabRenamedByUser, _session, &Konsole::Session::tabTitleSetByUser); connect(_session.data() , &Konsole::Session::currentDirectoryChanged , this , &Konsole::SessionController::currentDirectoryChanged); // listen for color changes connect(_session.data(), &Konsole::Session::changeBackgroundColorRequest, _view.data(), &Konsole::TerminalDisplay::setBackgroundColor); connect(_session.data(), &Konsole::Session::changeForegroundColorRequest, _view.data(), &Konsole::TerminalDisplay::setForegroundColor); // update the title when the session starts connect(_session.data(), &Konsole::Session::started, this, &Konsole::SessionController::snapshot); // listen for output changes to set activity flag connect(_session->emulation(), &Konsole::Emulation::outputChanged, this, &Konsole::SessionController::fireActivity); // listen for detection of ZModem transfer connect(_session.data(), &Konsole::Session::zmodemDownloadDetected, this, &Konsole::SessionController::zmodemDownload); connect(_session.data(), &Konsole::Session::zmodemUploadDetected, this, &Konsole::SessionController::zmodemUpload); // listen for flow control status changes connect(_session.data(), &Konsole::Session::flowControlEnabledChanged, _view.data(), &Konsole::TerminalDisplay::setFlowControlWarningEnabled); _view->setFlowControlWarningEnabled(_session->flowControlEnabled()); // take a snapshot of the session state every so often when // user activity occurs // // the timer is owned by the session so that it will be destroyed along // with the session _interactionTimer = new QTimer(_session); _interactionTimer->setSingleShot(true); _interactionTimer->setInterval(500); connect(_interactionTimer, &QTimer::timeout, this, &Konsole::SessionController::snapshot); connect(_view.data(), &Konsole::TerminalDisplay::compositeFocusChanged, this, [this](bool focused) { if (focused) { interactionHandler(); }}); connect(_view.data(), &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::SessionController::interactionHandler); // take a snapshot of the session state periodically in the background auto backgroundTimer = new QTimer(_session); backgroundTimer->setSingleShot(false); backgroundTimer->setInterval(2000); connect(backgroundTimer, &QTimer::timeout, this, &Konsole::SessionController::snapshot); backgroundTimer->start(); // xterm '11;?' request connect(_session.data(), &Konsole::Session::getBackgroundColor, this, &Konsole::SessionController::sendBackgroundColor); _allControllers.insert(this); // A list of programs that accept Ctrl+C to clear command line used // before outputting bookmark. _bookmarkValidProgramsToClear << QStringLiteral("bash") << QStringLiteral("fish") << QStringLiteral("sh"); _bookmarkValidProgramsToClear << QStringLiteral("tcsh") << QStringLiteral("zsh"); setupSearchBar(); _searchBar->setVisible(_isSearchBarEnabled); } SessionController::~SessionController() { _allControllers.remove(this); if (!_editProfileDialog.isNull()) { delete _editProfileDialog.data(); } } void SessionController::trackOutput(QKeyEvent* event) { Q_ASSERT(_view->screenWindow()); // Qt has broken something, so we can't rely on just checking if certain // keys are passed as modifiers anymore. const int key = event->key(); const bool shouldNotTriggerScroll = key == Qt::Key_Super_L || key == Qt::Key_Super_R || key == Qt::Key_Hyper_L || key == Qt::Key_Hyper_R || key == Qt::Key_Shift || key == Qt::Key_Control || key == Qt::Key_Meta || key == Qt::Key_Alt || key == Qt::Key_AltGr || key == Qt::Key_CapsLock || key == Qt::Key_NumLock || key == Qt::Key_ScrollLock; // Only jump to the bottom if the user actually typed something in, // not if the user e. g. just pressed a modifier. if (event->text().isEmpty() && ((event->modifiers() != 0u) || shouldNotTriggerScroll)) { return; } _view->screenWindow()->setTrackOutput(true); } void SessionController::viewFocusChangeHandler(bool focused) { if (focused) { // notify the world that the view associated with this session has been focused // used by the view manager to update the title of the MainWindow widget containing the view emit viewFocused(this); // when the view is focused, set bell events from the associated session to be delivered // by the focused view // first, disconnect any other views which are listening for bell signals from the session disconnect(_session.data(), &Konsole::Session::bellRequest, nullptr, nullptr); // second, connect the newly focused view to listen for the session's bell signal connect(_session.data(), &Konsole::Session::bellRequest, _view.data(), &Konsole::TerminalDisplay::bell); if ((_copyInputToAllTabsAction != nullptr) && _copyInputToAllTabsAction->isChecked()) { // A session with "Copy To All Tabs" has come into focus: // Ensure that newly created sessions are included in _copyToGroup! copyInputToAllTabs(); } } } void SessionController::interactionHandler() { - // This flag is used to make sure those special icons indicating interest - // events (activity/silence/bell?) remain in the tab until user interaction - // happens. Otherwise, those special icons will quickly be replaced by - // normal icon when ::snapshot() is triggered - _keepIconUntilInteraction = false; _interactionTimer->start(); } void SessionController::snapshot() { Q_ASSERT(!_session.isNull()); QString title = _session->getDynamicTitle(); title = title.simplified(); // Visualize that the session is broadcasting to others if ((_copyToGroup != nullptr) && _copyToGroup->sessions().count() > 1) { title.append(QLatin1Char('*')); } // use the fallback title if needed if (title.isEmpty()) { title = _session->title(Session::NameRole); } // apply new title _session->setTitle(Session::DisplayedTitleRole, title); // do not forget icon updateSessionIcon(); } QString SessionController::currentDir() const { return _session->currentWorkingDirectory(); } QUrl SessionController::url() const { return _session->getUrl(); } void SessionController::rename() { renameSession(); } void SessionController::openUrl(const QUrl& url) { // Clear shell's command line if (!_session->isForegroundProcessActive() && _bookmarkValidProgramsToClear.contains(_session->foregroundProcessName())) { _session->sendTextToTerminal(QChar(0x03), QLatin1Char('\n')); // Ctrl+C } // handle local paths if (url.isLocalFile()) { QString path = url.toLocalFile(); _session->sendTextToTerminal(QStringLiteral("cd ") + KShell::quoteArg(path), QLatin1Char('\r')); } else if (url.scheme().isEmpty()) { // QUrl couldn't parse what the user entered into the URL field // so just dump it to the shell QString command = url.toDisplayString(); if (!command.isEmpty()) { _session->sendTextToTerminal(command, QLatin1Char('\r')); } } else if (url.scheme() == QLatin1String("ssh")) { QString sshCommand = QStringLiteral("ssh "); if (url.port() > -1) { sshCommand += QStringLiteral("-p %1 ").arg(url.port()); } if (!url.userName().isEmpty()) { sshCommand += (url.userName() + QLatin1Char('@')); } if (!url.host().isEmpty()) { sshCommand += url.host(); } _session->sendTextToTerminal(sshCommand, QLatin1Char('\r')); } else if (url.scheme() == QLatin1String("telnet")) { QString telnetCommand = QStringLiteral("telnet "); if (!url.userName().isEmpty()) { telnetCommand += QStringLiteral("-l %1 ").arg(url.userName()); } if (!url.host().isEmpty()) { telnetCommand += (url.host() + QLatin1Char(' ')); } if (url.port() > -1) { telnetCommand += QString::number(url.port()); } _session->sendTextToTerminal(telnetCommand, QLatin1Char('\r')); } else { //TODO Implement handling for other Url types KMessageBox::sorry(_view->window(), i18n("Konsole does not know how to open the bookmark: ") + url.toDisplayString()); qCDebug(KonsoleDebug) << "Unable to open bookmark at url" << url << ", I do not know" << " how to handle the protocol " << url.scheme(); } } void SessionController::setupPrimaryScreenSpecificActions(bool use) { KActionCollection* collection = actionCollection(); QAction* clearAction = collection->action(QStringLiteral("clear-history")); QAction* resetAction = collection->action(QStringLiteral("clear-history-and-reset")); QAction* selectAllAction = collection->action(QStringLiteral("select-all")); QAction* selectLineAction = collection->action(QStringLiteral("select-line")); // these actions are meaningful only when primary screen is used. clearAction->setEnabled(use); resetAction->setEnabled(use); selectAllAction->setEnabled(use); selectLineAction->setEnabled(use); } void SessionController::selectionChanged(const QString& selectedText) { _selectedText = selectedText; updateCopyAction(selectedText); } void SessionController::updateCopyAction(const QString& selectedText) { QAction* copyAction = actionCollection()->action(QStringLiteral("edit_copy")); // copy action is meaningful only when some text is selected. copyAction->setEnabled(!selectedText.isEmpty()); } void SessionController::updateWebSearchMenu() { // reset _webSearchMenu->setVisible(false); _webSearchMenu->menu()->clear(); if (_selectedText.isEmpty()) { return; } QString searchText = _selectedText; searchText = searchText.replace(QLatin1Char('\n'), QLatin1Char(' ')).replace(QLatin1Char('\r'), QLatin1Char(' ')).simplified(); if (searchText.isEmpty()) { return; } KUriFilterData filterData(searchText); filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly); if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) { const QStringList searchProviders = filterData.preferredSearchProviders(); if (!searchProviders.isEmpty()) { _webSearchMenu->setText(i18n("Search for '%1' with", KStringHandler::rsqueeze(searchText, 16))); QAction* action = nullptr; foreach(const QString& searchProvider, searchProviders) { action = new QAction(searchProvider, _webSearchMenu); action->setIcon(QIcon::fromTheme(filterData.iconNameForPreferredSearchProvider(searchProvider))); action->setData(filterData.queryForPreferredSearchProvider(searchProvider)); connect(action, &QAction::triggered, this, &Konsole::SessionController::handleWebShortcutAction); _webSearchMenu->addAction(action); } _webSearchMenu->addSeparator(); action = new QAction(i18n("Configure Web Shortcuts..."), _webSearchMenu); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); connect(action, &QAction::triggered, this, &Konsole::SessionController::configureWebShortcuts); _webSearchMenu->addAction(action); _webSearchMenu->setVisible(true); } } } void SessionController::handleWebShortcutAction() { auto * action = qobject_cast(sender()); if (action == nullptr) { return; } KUriFilterData filterData(action->data().toString()); if (KUriFilter::self()->filterUri(filterData, QStringList() << QStringLiteral("kurisearchfilter"))) { const QUrl& url = filterData.uri(); new KRun(url, QApplication::activeWindow()); } } void SessionController::configureWebShortcuts() { KToolInvocation::kdeinitExec(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts")); } void SessionController::sendSignal(QAction* action) { const auto signal = action->data().toInt(); _session->sendSignal(signal); } void SessionController::sendBackgroundColor() { const QColor c = _view->getBackgroundColor(); _session->reportBackgroundColor(c); } void SessionController::toggleReadOnly() { auto *action = qobject_cast(sender()); if (action != nullptr) { bool readonly = !isReadOnly(); _session->setReadOnly(readonly); } } void SessionController::removeSearchFilter() { if (_searchFilter == nullptr) { return; } _view->filterChain()->removeFilter(_searchFilter); delete _searchFilter; _searchFilter = nullptr; } void SessionController::setupSearchBar() { connect(_searchBar.data(), &Konsole::IncrementalSearchBar::unhandledMovementKeyPressed, this, &Konsole::SessionController::movementKeyFromSearchBarReceived); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::closeClicked, this, &Konsole::SessionController::searchClosed); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchFromClicked, this, &Konsole::SessionController::searchFrom); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::findNextClicked, this, &Konsole::SessionController::findNextInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::findPreviousClicked, this, &Konsole::SessionController::findPreviousInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::highlightMatchesToggled , this , &Konsole::SessionController::highlightMatches); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::matchCaseToggled, this, &Konsole::SessionController::changeSearchMatch); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::matchRegExpToggled, this, &Konsole::SessionController::changeSearchMatch); } void SessionController::setShowMenuAction(QAction* action) { _showMenuAction = action; } void SessionController::setupCommonActions() { KActionCollection* collection = actionCollection(); // Close Session QAction* action = collection->addAction(QStringLiteral("close-session"), this, SLOT(closeSession())); action->setText(i18n("&Close Session")); action->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_W); // Open Browser action = collection->addAction(QStringLiteral("open-browser"), this, SLOT(openBrowser())); action->setText(i18n("Open File Manager")); action->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager"))); // Copy and Paste action = KStandardAction::copy(this, SLOT(copy()), collection); #ifdef Q_OS_MACOS // Don't use the Konsole::ACCEL const here, we really want the Command key (Qt::META) // TODO: check what happens if we leave it to Qt to assign the default? collection->setDefaultShortcut(action, Qt::META + Qt::Key_C); #else collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_C); #endif // disabled at first, since nothing has been selected now action->setEnabled(false); action = KStandardAction::paste(this, SLOT(paste()), collection); QList pasteShortcut; #ifdef Q_OS_MACOS pasteShortcut.append(QKeySequence(Qt::META + Qt::Key_V)); // No Insert key on Mac keyboards #else pasteShortcut.append(QKeySequence(Konsole::ACCEL + Qt::SHIFT + Qt::Key_V)); pasteShortcut.append(QKeySequence(Qt::SHIFT + Qt::Key_Insert)); #endif collection->setDefaultShortcuts(action, pasteShortcut); action = collection->addAction(QStringLiteral("paste-selection"), this, SLOT(pasteFromX11Selection())); action->setText(i18n("Paste Selection")); #ifdef Q_OS_MACOS collection->setDefaultShortcut(action, Qt::META + Qt::SHIFT + Qt::Key_V); #else collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Insert); #endif _webSearchMenu = new KActionMenu(i18n("Web Search"), this); _webSearchMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts"))); _webSearchMenu->setVisible(false); collection->addAction(QStringLiteral("web-search"), _webSearchMenu); action = collection->addAction(QStringLiteral("select-all"), this, SLOT(selectAll())); action->setText(i18n("&Select All")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all"))); action = collection->addAction(QStringLiteral("select-line"), this, SLOT(selectLine())); action->setText(i18n("Select &Line")); action = KStandardAction::saveAs(this, SLOT(saveHistory()), collection); action->setText(i18n("Save Output &As...")); #ifdef Q_OS_MACOS action->setShortcut(QKeySequence(Qt::META + Qt::Key_S)); #endif action = KStandardAction::print(this, SLOT(print_screen()), collection); action->setText(i18n("&Print Screen...")); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_P); action = collection->addAction(QStringLiteral("adjust-history"), this, SLOT(showHistoryOptions())); action->setText(i18n("Adjust Scrollback...")); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); action = collection->addAction(QStringLiteral("clear-history"), this, SLOT(clearHistory())); action->setText(i18n("Clear Scrollback")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); action = collection->addAction(QStringLiteral("clear-history-and-reset"), this, SLOT(clearHistoryAndReset())); action->setText(i18n("Clear Scrollback and Reset")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_K); // Profile Options action = collection->addAction(QStringLiteral("edit-current-profile"), this, SLOT(editCurrentProfile())); action->setText(i18n("Edit Current Profile...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); _switchProfileMenu = new KActionMenu(i18n("Switch Profile"), this); collection->addAction(QStringLiteral("switch-profile"), _switchProfileMenu); connect(_switchProfileMenu->menu(), &QMenu::aboutToShow, this, &Konsole::SessionController::prepareSwitchProfileMenu); // History _findAction = KStandardAction::find(this, SLOT(searchBarEvent()), collection); _findNextAction = KStandardAction::findNext(this, SLOT(findNextInHistory()), collection); _findNextAction->setEnabled(false); _findPreviousAction = KStandardAction::findPrev(this, SLOT(findPreviousInHistory()), collection); _findPreviousAction->setEnabled(false); #ifdef Q_OS_MACOS collection->setDefaultShortcut(_findAction, Qt::META + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::META + Qt::Key_G); collection->setDefaultShortcut(_findPreviousAction, Qt::META + Qt::SHIFT + Qt::Key_G); #else collection->setDefaultShortcut(_findAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::Key_F3); collection->setDefaultShortcut(_findPreviousAction, Qt::SHIFT + Qt::Key_F3); #endif // Character Encoding _codecAction = new KCodecAction(i18n("Set &Encoding"), this); _codecAction->setIcon(QIcon::fromTheme(QStringLiteral("character-set"))); collection->addAction(QStringLiteral("set-encoding"), _codecAction); connect(_codecAction->menu(), &QMenu::aboutToShow, this, &Konsole::SessionController::updateCodecAction); connect(_codecAction, QOverload::of(&KCodecAction::triggered), this, &Konsole::SessionController::changeCodec); // Read-only action = collection->addAction(QStringLiteral("view-readonly"), this, SLOT(toggleReadOnly())); action->setText(i18nc("@item:inmenu A read only (locked) session", "Read-only")); action->setCheckable(true); updateReadOnlyActionStates(); } void SessionController::setupExtraActions() { KActionCollection* collection = actionCollection(); // Rename Session QAction* action = collection->addAction(QStringLiteral("rename-session"), this, SLOT(renameSession())); action->setText(i18n("&Rename Tab...")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::ALT + Qt::Key_S); // Copy input to ==> all tabs auto* copyInputToAllTabsAction = collection->add(QStringLiteral("copy-input-to-all-tabs")); copyInputToAllTabsAction->setText(i18n("&All Tabs in Current Window")); copyInputToAllTabsAction->setData(CopyInputToAllTabsMode); // this action is also used in other place, so remember it _copyInputToAllTabsAction = copyInputToAllTabsAction; // Copy input to ==> selected tabs auto* copyInputToSelectedTabsAction = collection->add(QStringLiteral("copy-input-to-selected-tabs")); copyInputToSelectedTabsAction->setText(i18n("&Select Tabs...")); collection->setDefaultShortcut(copyInputToSelectedTabsAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Period); copyInputToSelectedTabsAction->setData(CopyInputToSelectedTabsMode); // Copy input to ==> none auto* copyInputToNoneAction = collection->add(QStringLiteral("copy-input-to-none")); copyInputToNoneAction->setText(i18nc("@action:inmenu Do not select any tabs", "&None")); collection->setDefaultShortcut(copyInputToNoneAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Slash); copyInputToNoneAction->setData(CopyInputToNoneMode); copyInputToNoneAction->setChecked(true); // the default state // The "Copy Input To" submenu // The above three choices are represented as combo boxes auto* copyInputActions = collection->add(QStringLiteral("copy-input-to")); copyInputActions->setText(i18n("Copy Input To")); copyInputActions->addAction(copyInputToAllTabsAction); copyInputActions->addAction(copyInputToSelectedTabsAction); copyInputActions->addAction(copyInputToNoneAction); connect(copyInputActions, QOverload::of(&KSelectAction::triggered), this, &Konsole::SessionController::copyInputActionsTriggered); action = collection->addAction(QStringLiteral("zmodem-upload"), this, SLOT(zmodemUpload())); action->setText(i18n("&ZModem Upload...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::ALT + Qt::Key_U); // Monitor KToggleAction* toggleAction = new KToggleAction(i18n("Monitor for &Activity"), this); collection->setDefaultShortcut(toggleAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_A); action = collection->addAction(QStringLiteral("monitor-activity"), toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorActivity); toggleAction = new KToggleAction(i18n("Monitor for &Silence"), this); collection->setDefaultShortcut(toggleAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_I); action = collection->addAction(QStringLiteral("monitor-silence"), toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorSilence); // Text Size action = collection->addAction(QStringLiteral("enlarge-font"), this, SLOT(increaseFontSize())); action->setText(i18n("Enlarge Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-more"))); QList enlargeFontShortcut; enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL + Qt::Key_Plus)); enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL + Qt::Key_Equal)); collection->setDefaultShortcuts(action, enlargeFontShortcut); action = collection->addAction(QStringLiteral("shrink-font"), this, SLOT(decreaseFontSize())); action->setText(i18n("Shrink Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-less"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::Key_Minus); action = collection->addAction(QStringLiteral("reset-font-size"), this, SLOT(resetFontSize())); action->setText(i18n("Reset Font Size")); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::ALT + Qt::Key_0); // Send signal auto* sendSignalActions = collection->add(QStringLiteral("send-signal")); sendSignalActions->setText(i18n("Send Signal")); connect(sendSignalActions, QOverload::of(&KSelectAction::triggered), this, &Konsole::SessionController::sendSignal); action = collection->addAction(QStringLiteral("sigstop-signal")); action->setText(i18n("&Suspend Task") + QStringLiteral(" (STOP)")); action->setData(SIGSTOP); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigcont-signal")); action->setText(i18n("&Continue Task") + QStringLiteral(" (CONT)")); action->setData(SIGCONT); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sighup-signal")); action->setText(i18n("&Hangup") + QStringLiteral(" (HUP)")); action->setData(SIGHUP); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigint-signal")); action->setText(i18n("&Interrupt Task") + QStringLiteral(" (INT)")); action->setData(SIGINT); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigterm-signal")); action->setText(i18n("&Terminate Task") + QStringLiteral(" (TERM)")); action->setData(SIGTERM); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigkill-signal")); action->setText(i18n("&Kill Task") + QStringLiteral(" (KILL)")); action->setData(SIGKILL); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigusr1-signal")); action->setText(i18n("User Signal &1") + QStringLiteral(" (USR1)")); action->setData(SIGUSR1); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigusr2-signal")); action->setText(i18n("User Signal &2") + QStringLiteral(" (USR2)")); action->setData(SIGUSR2); sendSignalActions->addAction(action); } void SessionController::switchProfile(const Profile::Ptr &profile) { SessionManager::instance()->setSessionProfile(_session, profile); updateFilterList(profile); } void SessionController::prepareSwitchProfileMenu() { if (_switchProfileMenu->menu()->isEmpty()) { _profileList = new ProfileList(false, this); connect(_profileList, &Konsole::ProfileList::profileSelected, this, &Konsole::SessionController::switchProfile); } _switchProfileMenu->menu()->clear(); _switchProfileMenu->menu()->addActions(_profileList->actions()); } void SessionController::updateCodecAction() { _codecAction->setCurrentCodec(QString::fromUtf8(_session->codec())); } void SessionController::changeCodec(QTextCodec* codec) { _session->setCodec(codec); } EditProfileDialog* SessionController::profileDialogPointer() { return _editProfileDialog.data(); } void SessionController::editCurrentProfile() { // Searching for Edit profile dialog opened with the same profile const QList allSessionsControllers = _allControllers.values(); foreach (SessionController* session, allSessionsControllers) { if ((session->profileDialogPointer() != nullptr) && session->profileDialogPointer()->isVisible() && session->profileDialogPointer()->lookupProfile() == SessionManager::instance()->sessionProfile(_session)) { session->profileDialogPointer()->close(); } } // NOTE bug311270: For to prevent the crash, the profile must be reset. if (!_editProfileDialog.isNull()) { // exists but not visible delete _editProfileDialog.data(); } _editProfileDialog = new EditProfileDialog(QApplication::activeWindow()); _editProfileDialog.data()->setProfile(SessionManager::instance()->sessionProfile(_session)); _editProfileDialog.data()->show(); } void SessionController::renameSession() { const QString &sessionLocalTabTitleFormat = _session->tabTitleFormat(Session::LocalTabTitle); const QString &sessionRemoteTabTitleFormat = _session->tabTitleFormat(Session::RemoteTabTitle); QScopedPointer dialog(new RenameTabDialog(QApplication::activeWindow())); dialog->setTabTitleText(sessionLocalTabTitleFormat); dialog->setRemoteTabTitleText(sessionRemoteTabTitleFormat); if (_session->isRemote()) { dialog->focusRemoteTabTitleText(); } else { dialog->focusTabTitleText(); } QPointer guard(_session); int result = dialog->exec(); if (guard.isNull()) { return; } if (result != 0) { const QString &tabTitle = dialog->tabTitleText(); const QString &remoteTabTitle = dialog->remoteTabTitleText(); if (tabTitle != sessionLocalTabTitleFormat) { _session->setTabTitleFormat(Session::LocalTabTitle, tabTitle); emit tabRenamedByUser(true); // trigger an update of the tab text snapshot(); } if(remoteTabTitle != sessionRemoteTabTitleFormat) { _session->setTabTitleFormat(Session::RemoteTabTitle, remoteTabTitle); emit tabRenamedByUser(true); snapshot(); } } } // This is called upon Menu->Close Sesssion and right-click on tab->Close Tab bool SessionController::confirmClose() const { if (_session->isForegroundProcessActive()) { QString title = _session->foregroundProcessName(); // hard coded for now. In future make it possible for the user to specify which programs // are ignored when considering whether to display a confirmation QStringList ignoreList; ignoreList << QString::fromUtf8(qgetenv("SHELL")).section(QLatin1Char('/'), -1); if (ignoreList.contains(title)) { return true; } QString question; if (title.isEmpty()) { question = i18n("A program is currently running in this session." " Are you sure you want to close it?"); } else { question = i18n("The program '%1' is currently running in this session." " Are you sure you want to close it?", title); } int result = KMessageBox::warningYesNo(_view->window(), question, i18n("Confirm Close"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("CloseSingleTab")); return result == KMessageBox::Yes; } return true; } bool SessionController::confirmForceClose() const { if (_session->isRunning()) { QString title = _session->program(); // hard coded for now. In future make it possible for the user to specify which programs // are ignored when considering whether to display a confirmation QStringList ignoreList; ignoreList << QString::fromUtf8(qgetenv("SHELL")).section(QLatin1Char('/'), -1); if (ignoreList.contains(title)) { return true; } QString question; if (title.isEmpty()) { question = i18n("A program in this session would not die." " Are you sure you want to kill it by force?"); } else { question = i18n("The program '%1' is in this session would not die." " Are you sure you want to kill it by force?", title); } int result = KMessageBox::warningYesNo(_view->window(), question, i18n("Confirm Close")); return result == KMessageBox::Yes; } return true; } void SessionController::closeSession() { if (_preventClose) { return; } if (confirmClose()) { if (_session->closeInNormalWay()) { return; } else if (confirmForceClose()) { if (_session->closeInForceWay()) { return; } else { qCDebug(KonsoleDebug) << "Konsole failed to close a session in any way."; } } } } // Trying to open a remote Url may produce unexpected results. // Therefore, if a remote url, open the user's home path. // TODO consider: 1) disable menu upon remote session // 2) transform url to get the desired result (ssh -> sftp, etc) void SessionController::openBrowser() { const QUrl currentUrl = url(); if (currentUrl.isLocalFile()) { new KRun(currentUrl, QApplication::activeWindow(), true); } else { new KRun(QUrl::fromLocalFile(QDir::homePath()), QApplication::activeWindow(), true); } } void SessionController::copy() { _view->copyToClipboard(); } void SessionController::paste() { _view->pasteFromClipboard(); } void SessionController::pasteFromX11Selection() { _view->pasteFromX11Selection(); } void SessionController::selectAll() { _view->selectAll(); } void SessionController::selectLine() { _view->selectCurrentLine(); } static const KXmlGuiWindow* findWindow(const QObject* object) { // Walk up the QObject hierarchy to find a KXmlGuiWindow. while (object != nullptr) { const auto* window = qobject_cast(object); if (window != nullptr) { return(window); } object = object->parent(); } return(nullptr); } static bool hasTerminalDisplayInSameWindow(const Session* session, const KXmlGuiWindow* window) { // Iterate all TerminalDisplays of this Session ... foreach(const TerminalDisplay* terminalDisplay, session->views()) { // ... and check whether a TerminalDisplay has the same // window as given in the parameter if (window == findWindow(terminalDisplay)) { return(true); } } return(false); } void SessionController::copyInputActionsTriggered(QAction* action) { const auto mode = action->data().toInt(); switch (mode) { case CopyInputToAllTabsMode: copyInputToAllTabs(); break; case CopyInputToSelectedTabsMode: copyInputToSelectedTabs(); break; case CopyInputToNoneMode: copyInputToNone(); break; default: Q_ASSERT(false); } } void SessionController::copyInputToAllTabs() { if (_copyToGroup == nullptr) { _copyToGroup = new SessionGroup(this); } // Find our window ... const KXmlGuiWindow* myWindow = findWindow(_view); QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (auto session : group) { // First, ensure that the session is removed // (necessary to avoid duplicates on addSession()!) _copyToGroup->removeSession(session); // Add current session if it is displayed our window if (hasTerminalDisplayInSameWindow(session, myWindow)) { _copyToGroup->addSession(session); } } _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); snapshot(); } void SessionController::copyInputToSelectedTabs() { if (_copyToGroup == nullptr) { _copyToGroup = new SessionGroup(this); _copyToGroup->addSession(_session); _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); } QPointer dialog = new CopyInputDialog(_view); dialog->setMasterSession(_session); QSet currentGroup = QSet::fromList(_copyToGroup->sessions()); currentGroup.remove(_session); dialog->setChosenSessions(currentGroup); QPointer guard(_session); int result = dialog->exec(); if (guard.isNull()) { return; } if (result == QDialog::Accepted) { QSet newGroup = dialog->chosenSessions(); newGroup.remove(_session); QSet completeGroup = newGroup | currentGroup; foreach(Session * session, completeGroup) { if (newGroup.contains(session) && !currentGroup.contains(session)) { _copyToGroup->addSession(session); } else if (!newGroup.contains(session) && currentGroup.contains(session)) { _copyToGroup->removeSession(session); } } _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); snapshot(); } } void SessionController::copyInputToNone() { if (_copyToGroup == nullptr) { // No 'Copy To' is active return; } QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (auto iterator : group) { Session* session = iterator; if (session != _session) { _copyToGroup->removeSession(iterator); } } delete _copyToGroup; _copyToGroup = nullptr; snapshot(); } void SessionController::searchClosed() { _isSearchBarEnabled = false; searchHistory(false); } void SessionController::updateFilterList(Profile::Ptr profile) { if (profile != SessionManager::instance()->sessionProfile(_session)) { return; } bool underlineFiles = profile->underlineFilesEnabled(); if (!underlineFiles && (_fileFilter != nullptr)) { _view->filterChain()->removeFilter(_fileFilter); delete _fileFilter; _fileFilter = nullptr; } else if (underlineFiles && (_fileFilter == nullptr)) { _fileFilter = new FileFilter(_session); _view->filterChain()->addFilter(_fileFilter); } bool underlineLinks = profile->underlineLinksEnabled(); if (!underlineLinks && (_urlFilter != nullptr)) { _view->filterChain()->removeFilter(_urlFilter); delete _urlFilter; _urlFilter = nullptr; } else if (underlineLinks && (_urlFilter == nullptr)) { _urlFilter = new UrlFilter(); _view->filterChain()->addFilter(_urlFilter); } } void SessionController::setSearchStartToWindowCurrentLine() { setSearchStartTo(-1); } void SessionController::setSearchStartTo(int line) { _searchStartLine = line; _prevSearchResultLine = line; } void SessionController::listenForScreenWindowUpdates() { if (_listenForScreenWindowUpdates) { return; } connect(_view->screenWindow(), &Konsole::ScreenWindow::outputChanged, this, &Konsole::SessionController::updateSearchFilter); connect(_view->screenWindow(), &Konsole::ScreenWindow::scrolled, this, &Konsole::SessionController::updateSearchFilter); connect(_view->screenWindow(), &Konsole::ScreenWindow::currentResultLineChanged, _view.data(), QOverload<>::of(&Konsole::TerminalDisplay::update)); _listenForScreenWindowUpdates = true; } void SessionController::updateSearchFilter() { if ((_searchFilter != nullptr) && (!_searchBar.isNull())) { _view->processFilters(); } } void SessionController::searchBarEvent() { QString selectedText = _view->screenWindow()->selectedText(Screen::PreserveLineBreaks | Screen::TrimLeadingWhitespace | Screen::TrimTrailingWhitespace); if (!selectedText.isEmpty()) { _searchBar->setSearchText(selectedText); } if (_searchBar->isVisible()) { _searchBar->focusLineEdit(); } else { searchHistory(true); _isSearchBarEnabled = true; } } void SessionController::enableSearchBar(bool showSearchBar) { if (_searchBar.isNull()) { return; } if (showSearchBar && !_searchBar->isVisible()) { setSearchStartToWindowCurrentLine(); } _searchBar->setVisible(showSearchBar); if (showSearchBar) { connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchChanged, this, &Konsole::SessionController::searchTextChanged); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchReturnPressed, this, &Konsole::SessionController::findPreviousInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchShiftPlusReturnPressed, this, &Konsole::SessionController::findNextInHistory); } else { disconnect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchChanged, this, &Konsole::SessionController::searchTextChanged); disconnect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchReturnPressed, this, &Konsole::SessionController::findPreviousInHistory); disconnect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchShiftPlusReturnPressed, this, &Konsole::SessionController::findNextInHistory); if ((!_view.isNull()) && (_view->screenWindow() != nullptr)) { _view->screenWindow()->setCurrentResultLine(-1); } } } bool SessionController::reverseSearchChecked() const { Q_ASSERT(_searchBar); QBitArray options = _searchBar->optionsChecked(); return options.at(IncrementalSearchBar::ReverseSearch); } QRegularExpression SessionController::regexpFromSearchBarOptions() const { QBitArray options = _searchBar->optionsChecked(); QString text(_searchBar->searchText()); QRegularExpression regExp; if (options.at(IncrementalSearchBar::RegExp)) { regExp.setPattern(text); } else { regExp.setPattern(QRegularExpression::escape(text)); } if (!options.at(IncrementalSearchBar::MatchCase)) { regExp.setPatternOptions(QRegularExpression::CaseInsensitiveOption); } return regExp; } // searchHistory() may be called either as a result of clicking a menu item or // as a result of changing the search bar widget void SessionController::searchHistory(bool showSearchBar) { enableSearchBar(showSearchBar); if (!_searchBar.isNull()) { if (showSearchBar) { removeSearchFilter(); listenForScreenWindowUpdates(); _searchFilter = new RegExpFilter(); _searchFilter->setRegExp(regexpFromSearchBarOptions()); _view->filterChain()->addFilter(_searchFilter); _view->processFilters(); setFindNextPrevEnabled(true); } else { setFindNextPrevEnabled(false); removeSearchFilter(); _view->setFocus(Qt::ActiveWindowFocusReason); } } } void SessionController::setFindNextPrevEnabled(bool enabled) { _findNextAction->setEnabled(enabled); _findPreviousAction->setEnabled(enabled); } void SessionController::searchTextChanged(const QString& text) { Q_ASSERT(_view->screenWindow()); if (_searchText == text) { return; } _searchText = text; if (text.isEmpty()) { _view->screenWindow()->clearSelection(); _view->screenWindow()->scrollTo(_searchStartLine); } // update search. this is called even when the text is // empty to clear the view's filters beginSearch(text , reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::searchCompleted(bool success) { _prevSearchResultLine = _view->screenWindow()->currentResultLine(); if (!_searchBar.isNull()) { _searchBar->setFoundMatch(success); } } void SessionController::beginSearch(const QString& text, Enum::SearchDirection direction) { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); QRegularExpression regExp = regexpFromSearchBarOptions(); _searchFilter->setRegExp(regExp); if (_searchStartLine < 0 || _searchStartLine > _view->screenWindow()->lineCount()) { if (direction == Enum::ForwardsSearch) { setSearchStartTo(_view->screenWindow()->currentLine()); } else { setSearchStartTo(_view->screenWindow()->currentLine() + _view->screenWindow()->windowLines()); } } if (!regExp.pattern().isEmpty()) { _view->screenWindow()->setCurrentResultLine(-1); auto task = new SearchHistoryTask(this); connect(task, &Konsole::SearchHistoryTask::completed, this, &Konsole::SessionController::searchCompleted); task->setRegExp(regExp); task->setSearchDirection(direction); task->setAutoDelete(true); task->setStartLine(_searchStartLine); task->addScreenWindow(_session , _view->screenWindow()); task->execute(); } else if (text.isEmpty()) { searchCompleted(false); } _view->processFilters(); } void SessionController::highlightMatches(bool highlight) { if (highlight) { _view->filterChain()->addFilter(_searchFilter); _view->processFilters(); } else { _view->filterChain()->removeFilter(_searchFilter); } _view->update(); } void SessionController::searchFrom() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); if (reverseSearchChecked()) { setSearchStartTo(_view->screenWindow()->lineCount()); } else { setSearchStartTo(0); } beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::findNextInHistory() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); setSearchStartTo(_prevSearchResultLine); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::findPreviousInHistory() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); setSearchStartTo(_prevSearchResultLine); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::ForwardsSearch : Enum::BackwardsSearch); } void SessionController::changeSearchMatch() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); // reset Selection for new case match _view->screenWindow()->clearSelection(); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::showHistoryOptions() { QScopedPointer dialog(new HistorySizeDialog(QApplication::activeWindow())); const HistoryType& currentHistory = _session->historyType(); if (currentHistory.isEnabled()) { if (currentHistory.isUnlimited()) { dialog->setMode(Enum::UnlimitedHistory); } else { dialog->setMode(Enum::FixedSizeHistory); dialog->setLineCount(currentHistory.maximumLineCount()); } } else { dialog->setMode(Enum::NoHistory); } QPointer guard(_session); int result = dialog->exec(); if (guard.isNull()) { return; } if (result != 0) { scrollBackOptionsChanged(dialog->mode(), dialog->lineCount()); } } void SessionController::sessionResizeRequest(const QSize& size) { ////qDebug() << "View resize requested to " << size; _view->setSize(size.width(), size.height()); } void SessionController::scrollBackOptionsChanged(int mode, int lines) { switch (mode) { case Enum::NoHistory: _session->setHistoryType(HistoryTypeNone()); break; case Enum::FixedSizeHistory: _session->setHistoryType(CompactHistoryType(lines)); break; case Enum::UnlimitedHistory: _session->setHistoryType(HistoryTypeFile()); break; } } void SessionController::print_screen() { QPrinter printer; QPointer dialog = new QPrintDialog(&printer, _view); auto options = new PrintOptions(); dialog->setOptionTabs(QList() << options); dialog->setWindowTitle(i18n("Print Shell")); connect(dialog.data(), QOverload<>::of(&QPrintDialog::accepted), options, &Konsole::PrintOptions::saveSettings); if (dialog->exec() != QDialog::Accepted) { return; } QPainter painter; painter.begin(&printer); KConfigGroup configGroup(KSharedConfig::openConfig(), "PrintOptions"); if (configGroup.readEntry("ScaleOutput", true)) { double scale = qMin(printer.pageRect().width() / static_cast(_view->width()), printer.pageRect().height() / static_cast(_view->height())); painter.scale(scale, scale); } _view->printContent(painter, configGroup.readEntry("PrinterFriendly", true)); } void SessionController::saveHistory() { SessionTask* task = new SaveHistoryTask(this); task->setAutoDelete(true); task->addSession(_session); task->execute(); } void SessionController::clearHistory() { _session->clearHistory(); _view->updateImage(); // To reset view scrollbar _view->repaint(); } void SessionController::clearHistoryAndReset() { Profile::Ptr profile = SessionManager::instance()->sessionProfile(_session); QByteArray name = profile->defaultEncoding().toUtf8(); Emulation* emulation = _session->emulation(); emulation->reset(); _session->refresh(); _session->setCodec(QTextCodec::codecForName(name)); clearHistory(); } void SessionController::increaseFontSize() { _view->increaseFontSize(); } void SessionController::decreaseFontSize() { _view->decreaseFontSize(); } void SessionController::resetFontSize() { _view->resetFontSize(); } void SessionController::monitorActivity(bool monitor) { _session->setMonitorActivity(monitor); } void SessionController::monitorSilence(bool monitor) { _session->setMonitorSilence(monitor); } void SessionController::updateSessionIcon() { // If the default profile icon is being used, don't put it on the tab // Only show the icon if the user specifically chose one if (_session->iconName() == QLatin1String("utilities-terminal")) { _sessionIconName = QString(); } else { _sessionIconName = _session->iconName(); } _sessionIcon = QIcon::fromTheme(_sessionIconName); - // Visualize that the session is broadcasting to others - if ((_copyToGroup != nullptr) && _copyToGroup->sessions().count() > 1) { - // Master Mode: set different icon, to warn the user to be careful - setIcon(*_broadcastIcon); - } else { - if (!_keepIconUntilInteraction) { - // Not in Master Mode: use normal icon - setIcon(_sessionIcon); - } - } + setIcon(_sessionIcon); } void SessionController::updateReadOnlyActionStates() { bool readonly = isReadOnly(); QAction *readonlyAction = actionCollection()->action(QStringLiteral("view-readonly")); Q_ASSERT(readonlyAction != nullptr); readonlyAction->setIcon(QIcon::fromTheme(readonly ? QStringLiteral("object-locked") : QStringLiteral("object-unlocked"))); readonlyAction->setChecked(readonly); auto updateActionState = [this, readonly](const QString &name) { QAction *action = actionCollection()->action(name); if (action != nullptr) { action->setEnabled(!readonly); } }; updateActionState(QStringLiteral("edit_paste")); updateActionState(QStringLiteral("clear-history")); updateActionState(QStringLiteral("clear-history-and-reset")); updateActionState(QStringLiteral("edit-current-profile")); updateActionState(QStringLiteral("switch-profile")); updateActionState(QStringLiteral("adjust-history")); updateActionState(QStringLiteral("send-signal")); updateActionState(QStringLiteral("zmodem-upload")); _codecAction->setEnabled(!readonly); // Without the timer, when detaching a tab while the message widget is visible, // the size of the terminal becomes really small... QTimer::singleShot(0, this, [this, readonly]() { _view->updateReadOnlyState(readonly); }); } bool SessionController::isReadOnly() const { if (!_session.isNull()) { return _session->isReadOnly(); } else { return false; } } bool SessionController::isCopyInputActive() const { if ((_copyToGroup != nullptr) && _copyToGroup->sessions().count() > 1) { return true; } return false; } void SessionController::sessionAttributeChanged() { if (_sessionIconName != _session->iconName()) { updateSessionIcon(); } QString title = _session->title(Session::DisplayedTitleRole); // special handling for the "%w" marker which is replaced with the // window title set by the shell title.replace(QLatin1String("%w"), _session->userTitle()); // special handling for the "%#" marker which is replaced with the // number of the shell title.replace(QLatin1String("%#"), QString::number(_session->sessionId())); if (title.isEmpty()) { title = _session->title(Session::NameRole); } setTitle(title); emit rawTitleChanged(); } void SessionController::sessionReadOnlyChanged() { - // Trigger icon update - sessionAttributeChanged(); - updateReadOnlyActionStates(); // Update all views foreach (TerminalDisplay* view, session()->views()) { if (view != _view.data()) { view->updateReadOnlyState(isReadOnly()); } } } void SessionController::showDisplayContextMenu(const QPoint& position) { // needed to make sure the popup menu is available, even if a hosting // application did not merge our GUI. if (factory() == nullptr) { if (clientBuilder() == nullptr) { setClientBuilder(new KXMLGUIBuilder(_view)); } auto factory = new KXMLGUIFactory(clientBuilder(), this); factory->addClient(this); ////qDebug() << "Created xmlgui factory" << factory; } QPointer popup = qobject_cast(factory()->container(QStringLiteral("session-popup-menu"), this)); if (!popup.isNull()) { updateReadOnlyActionStates(); // prepend content-specific actions such as "Open Link", "Copy Email Address" etc. QList contentActions = _view->filterActions(position); auto contentSeparator = new QAction(popup); contentSeparator->setSeparator(true); contentActions << contentSeparator; popup->insertActions(popup->actions().value(0, nullptr), contentActions); // always update this submenu before showing the context menu, // because the available search services might have changed // since the context menu is shown last time updateWebSearchMenu(); _preventClose = true; if (_showMenuAction != nullptr) { if ( _showMenuAction->isChecked() ) { popup->removeAction( _showMenuAction); } else { popup->insertAction(_switchProfileMenu, _showMenuAction); } } QAction* chosen = popup->exec(_view->mapToGlobal(position)); // check for validity of the pointer to the popup menu if (!popup.isNull()) { delete contentSeparator; } _preventClose = false; if ((chosen != nullptr) && chosen->objectName() == QLatin1String("close-session")) { chosen->trigger(); } } else { qCDebug(KonsoleDebug) << "Unable to display popup menu for session" << _session->title(Session::NameRole) << ", no GUI factory available to build the popup."; } } void SessionController::movementKeyFromSearchBarReceived(QKeyEvent *event) { QCoreApplication::sendEvent(_view, event); setSearchStartToWindowCurrentLine(); } void SessionController::sessionNotificationsChanged(Session::Notification notification, bool enabled) { - if (notification == Session::Notification::Activity && enabled) { - setIcon(*_activityIcon); - _keepIconUntilInteraction = true; - } else if (notification == Session::Notification::Silence && enabled) { - setIcon(*_silenceIcon); - _keepIconUntilInteraction = true; - } else if (notification == Session::Notification::Bell && enabled) { - setIcon(*_bellIcon); - _keepIconUntilInteraction = true; - } else { - updateSessionIcon(); - } emit notificationChanged(this, notification, enabled); } void SessionController::zmodemDownload() { QString zmodem = QStandardPaths::findExecutable(QStringLiteral("rz")); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable(QStringLiteral("lrz")); } if (!zmodem.isEmpty()) { const QString path = QFileDialog::getExistingDirectory(_view, i18n("Save ZModem Download to..."), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (!path.isEmpty()) { _session->startZModem(zmodem, path, QStringList()); return; } } else { KMessageBox::error(_view, i18n("

A ZModem file transfer attempt has been detected, " "but no suitable ZModem software was found on this system.

" "

You may wish to install the 'rzsz' or 'lrzsz' package.

")); } _session->cancelZModem(); } void SessionController::zmodemUpload() { if (_session->isZModemBusy()) { KMessageBox::sorry(_view, i18n("

The current session already has a ZModem file transfer in progress.

")); return; } QString zmodem = QStandardPaths::findExecutable(QStringLiteral("sz")); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable(QStringLiteral("lsz")); } if (zmodem.isEmpty()) { KMessageBox::sorry(_view, i18n("

No suitable ZModem software was found on this system.

" "

You may wish to install the 'rzsz' or 'lrzsz' package.

")); return; } QStringList files = QFileDialog::getOpenFileNames(_view, i18n("Select Files for ZModem Upload"), QDir::homePath()); if (!files.isEmpty()) { _session->startZModem(zmodem, QString(), files); } } bool SessionController::isKonsolePart() const { // Check to see if we are being called from Konsole or a KPart return !(qApp->applicationName() == QLatin1String("konsole")); } QString SessionController::userTitle() const { if (!_session.isNull()) { return _session->userTitle(); } else { return QString(); } } diff --git a/src/SessionController.h b/src/SessionController.h index d91b66b5..70cd82b9 100644 --- a/src/SessionController.h +++ b/src/SessionController.h @@ -1,366 +1,364 @@ /* Copyright 2006-2008 by Robert Knight Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef SESSIONCONTROLLER_H #define SESSIONCONTROLLER_H // Qt #include #include #include #include // KDE #include // Konsole #include "ViewProperties.h" #include "Profile.h" #include "Enumeration.h" #include "Session.h" class QAction; class QTextCodec; class QKeyEvent; class QTimer; class QUrl; class KCodecAction; class QAction; class KActionMenu; namespace Konsole { class Session; class SessionGroup; class ScreenWindow; class TerminalDisplay; class IncrementalSearchBar; class ProfileList; class RegExpFilter; class UrlFilter; class FileFilter; class EditProfileDialog; using SessionPtr = QPointer; /** * Provides the menu actions to manipulate a single terminal session and view pair. * The actions provided by this class are defined in the sessionui.rc XML file. * * SessionController monitors the session and provides access to basic information * about the session such as title(), icon() and currentDir(). SessionController * provides notifications of activity in the session via the activity() signal. * * When the controlled view receives the focus, the focused() signal is emitted * with a pointer to the controller. This can be used by main application window * which contains the view to plug the controller's actions into the menu when * the view is focused. */ class KONSOLEPRIVATE_EXPORT SessionController : public ViewProperties, public KXMLGUIClient { Q_OBJECT public: enum CopyInputToEnum { /** Copy keyboard input to all the other tabs in current window */ CopyInputToAllTabsMode = 0, /** Copy keyboard input to user selected tabs in current window */ CopyInputToSelectedTabsMode = 1, /** Do not copy keyboard input to other tabs */ CopyInputToNoneMode = 2 }; /** * Constructs a new SessionController which operates on @p session and @p view. */ SessionController(Session *session, TerminalDisplay *view, QObject *parent); ~SessionController() Q_DECL_OVERRIDE; /** Returns the session associated with this controller */ QPointer session() { return _session; } /** Returns the view associated with this controller */ QPointer view() { return _view; } /** * Returns the "window title" of the associated session. */ QString userTitle() const; /** * Returns true if the controller is valid. * A valid controller is one which has a non-null session() and view(). * * Equivalent to "!session().isNull() && !view().isNull()" */ bool isValid() const; /** Set the start line from which the next search will be done **/ void setSearchStartTo(int line); /** set start line to the first or last line (depending on the reverse search * setting) in the terminal display **/ void setSearchStartToWindowCurrentLine(); /** * Sets the action displayed in the session's context menu to hide or * show the menu bar. */ void setShowMenuAction(QAction *action); EditProfileDialog *profileDialogPointer(); // reimplemented QUrl url() const Q_DECL_OVERRIDE; QString currentDir() const Q_DECL_OVERRIDE; void rename() Q_DECL_OVERRIDE; bool confirmClose() const Q_DECL_OVERRIDE; virtual bool confirmForceClose() const; /** Returns the set of all controllers that exist. */ static QSet allControllers() { return _allControllers; } /* Returns true if called within a KPart; false if called within Konsole. */ bool isKonsolePart() const; bool isReadOnly() const; bool isCopyInputActive() const; Q_SIGNALS: /** * Emitted when the view associated with the controller is focused. * This can be used by other classes to plug the controller's actions into a window's * menus. */ void viewFocused(SessionController *controller); void rawTitleChanged(); /** * Emitted when the current working directory of the session associated with * the controller is changed. */ void currentDirectoryChanged(const QString &dir); /** * Emitted when the user changes the tab title. */ void tabRenamedByUser(bool renamed) const; public Q_SLOTS: /** * Issues a command to the session to navigate to the specified URL. * This may not succeed if the foreground program does not understand * the command sent to it ( 'cd path' for local URLs ) or is not * responding to input. * * openUrl() currently supports urls for local paths and those * using the 'ssh' protocol ( eg. "ssh://joebloggs@hostname" ) */ void openUrl(const QUrl &url); /** * update actions which are meaningful only when primary screen is in use. */ void setupPrimaryScreenSpecificActions(bool use); /** * update actions which are closely related with the selected text. */ void selectionChanged(const QString &selectedText); /** * close the associated session. This might involve user interaction for * confirmation. */ void closeSession(); /** Increase font size */ void increaseFontSize(); /** Decrease font size */ void decreaseFontSize(); /** Reset font size */ void resetFontSize(); /** Close the incremental search */ void searchClosed(); // called when the user clicks on the private Q_SLOTS: // menu item handlers void openBrowser(); void copy(); void paste(); void selectAll(); void selectLine(); void pasteFromX11Selection(); // shortcut only void copyInputActionsTriggered(QAction *action); void copyInputToAllTabs(); void copyInputToSelectedTabs(); void copyInputToNone(); void editCurrentProfile(); void changeCodec(QTextCodec *codec); void enableSearchBar(bool showSearchBar); void searchHistory(bool showSearchBar); void searchBarEvent(); void searchFrom(); void findNextInHistory(); void findPreviousInHistory(); void changeSearchMatch(); void print_screen(); void saveHistory(); void showHistoryOptions(); void clearHistory(); void clearHistoryAndReset(); void monitorActivity(bool monitor); void monitorSilence(bool monitor); void renameSession(); void switchProfile(const Profile::Ptr &profile); void handleWebShortcutAction(); void configureWebShortcuts(); void sendSignal(QAction *action); void sendBackgroundColor(); void toggleReadOnly(); // other void setupSearchBar(); void prepareSwitchProfileMenu(); void updateCodecAction(); void showDisplayContextMenu(const QPoint &position); void movementKeyFromSearchBarReceived(QKeyEvent *event); void sessionNotificationsChanged(Session::Notification notification, bool enabled); void sessionAttributeChanged(); void sessionReadOnlyChanged(); void searchTextChanged(const QString &text); void searchCompleted(bool success); void updateFilterList(Profile::Ptr profile); // Called when the profile has changed, so we might need to change the list of filters void viewFocusChangeHandler(bool focused); void interactionHandler(); void snapshot(); // called periodically as the user types // to take a snapshot of the state of the // foreground process in the terminal void highlightMatches(bool highlight); void scrollBackOptionsChanged(int mode, int lines); void sessionResizeRequest(const QSize &size); void trackOutput(QKeyEvent *event); // move view to end of current output // when a key press occurs in the // display area void updateSearchFilter(); void zmodemDownload(); void zmodemUpload(); // update actions related with selected text void updateCopyAction(const QString &selectedText); void updateWebSearchMenu(); private: Q_DISABLE_COPY(SessionController) // begins the search // text - pattern to search for // direction - value from SearchHistoryTask::SearchDirection enum to specify // the search direction void beginSearch(const QString &text, Enum::SearchDirection direction); QRegularExpression regexpFromSearchBarOptions() const; bool reverseSearchChecked() const; void setupCommonActions(); void setupExtraActions(); void removeSearchFilter(); // remove and delete the current search filter if set void setFindNextPrevEnabled(bool enabled); void listenForScreenWindowUpdates(); private: void updateSessionIcon(); void updateReadOnlyActionStates(); QPointer _session; QPointer _view; SessionGroup *_copyToGroup; ProfileList *_profileList; QIcon _sessionIcon; QString _sessionIconName; int _previousState; RegExpFilter *_searchFilter; UrlFilter *_urlFilter; FileFilter *_fileFilter; QAction *_copyInputToAllTabsAction; QAction *_findAction; QAction *_findNextAction; QAction *_findPreviousAction; QTimer *_interactionTimer; int _searchStartLine; int _prevSearchResultLine; KCodecAction *_codecAction; KActionMenu *_switchProfileMenu; KActionMenu *_webSearchMenu; bool _listenForScreenWindowUpdates; bool _preventClose; - bool _keepIconUntilInteraction; - QString _selectedText; QAction *_showMenuAction; static QSet _allControllers; static int _lastControllerId; QStringList _bookmarkValidProgramsToClear; bool _isSearchBarEnabled; QPointer _editProfileDialog; QString _searchText; QPointer _searchBar; }; inline bool SessionController::isValid() const { return !_session.isNull() && !_view.isNull(); } } #endif //SESSIONCONTROLLER_H