diff --git a/src/Emulation.h b/src/Emulation.h index a89d627b..bccfc797 100644 --- a/src/Emulation.h +++ b/src/Emulation.h @@ -1,525 +1,520 @@ /* This file is part of Konsole, an X terminal. Copyright 2007-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle 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 EMULATION_H #define EMULATION_H // Qt #include #include #include // Konsole #include "Enumeration.h" #include "konsoleprivate_export.h" class QKeyEvent; namespace Konsole { class KeyboardTranslator; class HistoryType; class Screen; class ScreenWindow; class TerminalCharacterDecoder; /** * This enum describes the available states which * the terminal emulation may be set to. * * These are the values used by Emulation::stateChanged() */ enum { /** The emulation is currently receiving user input. */ NOTIFYNORMAL = 0, /** * The terminal program has triggered a bell event * to get the user's attention. */ NOTIFYBELL = 1, /** * The emulation is currently receiving data from its * terminal input. */ NOTIFYACTIVITY = 2, // unused here? NOTIFYSILENCE = 3 }; /** * Base class for terminal emulation back-ends. * * The back-end is responsible for decoding an incoming character stream and * producing an output image of characters. * * When input from the terminal is received, the receiveData() slot should be called with * the data which has arrived. The emulation will process the data and update the * screen image accordingly. The codec used to decode the incoming character stream * into the unicode characters used internally can be specified using setCodec() * * The size of the screen image can be specified by calling setImageSize() with the * desired number of lines and columns. When new lines are added, old content * is moved into a history store, which can be set by calling setHistory(). * * The screen image can be accessed by creating a ScreenWindow onto this emulation * by calling createWindow(). Screen windows provide access to a section of the * output. Each screen window covers the same number of lines and columns as the * image size returned by imageSize(). The screen window can be moved up and down * and provides transparent access to both the current on-screen image and the * previous output. The screen windows emit an outputChanged signal * when the section of the image they are looking at changes. * Graphical views can then render the contents of a screen window, listening for notifications * of output changes from the screen window which they are associated with and updating * accordingly. * * The emulation also is also responsible for converting input from the connected views such * as keypresses and mouse activity into a character string which can be sent * to the terminal program. Key presses can be processed by calling the sendKeyEvent() slot, * while mouse events can be processed using the sendMouseEvent() slot. When the character * stream has been produced, the emulation will emit a sendData() signal with a pointer * to the character buffer. This data should be fed to the standard input of the terminal * process. The translation of key presses into an output character stream is performed * using a lookup in a set of key bindings which map key sequences to output * character sequences. The name of the key bindings set used can be specified using * setKeyBindings() * * The emulation maintains certain state information which changes depending on the * input received. The emulation can be reset back to its starting state by calling * reset(). * * The emulation also maintains an activity state, which specifies whether * terminal is currently active ( when data is received ), normal * ( when the terminal is idle or receiving user input ) or trying * to alert the user ( also known as a "Bell" event ). The stateSet() signal * is emitted whenever the activity state is set. This can be used to determine * how long the emulation has been active/idle for and also respond to * a 'bell' event in different ways. */ class KONSOLEPRIVATE_EXPORT Emulation : public QObject { Q_OBJECT public: /** Constructs a new terminal emulation */ Emulation(); ~Emulation() Q_DECL_OVERRIDE; /** * Creates a new window onto the output from this emulation. The contents * of the window are then rendered by views which are set to use this window using the * TerminalDisplay::setScreenWindow() method. */ ScreenWindow *createWindow(); /** Returns the size of the screen image which the emulation produces */ QSize imageSize() const; /** * Returns the total number of lines, including those stored in the history. */ int lineCount() const; /** * Sets the history store used by this emulation. When new lines * are added to the output, older lines at the top of the screen are transferred to a history * store. * * The number of lines which are kept and the storage location depend on the * type of store. */ void setHistory(const HistoryType &); /** Returns the history store used by this emulation. See setHistory() */ const HistoryType &history() const; /** Clears the history scroll. */ void clearHistory(); /** * Copies the output history from @p startLine to @p endLine * into @p stream, using @p decoder to convert the terminal * characters into text. * * @param decoder A decoder which converts lines of terminal characters with * appearance attributes into output text. PlainTextDecoder is the most commonly * used decoder. * @param startLine Index of first line to copy * @param endLine Index of last line to copy */ virtual void writeToStream(TerminalCharacterDecoder *decoder, int startLine, int endLine); /** Returns the codec used to decode incoming characters. See setCodec() */ const QTextCodec *codec() const { return _codec; } /** Sets the codec used to decode incoming characters. */ void setCodec(const QTextCodec *); /** * Convenience method. * Returns true if the current codec used to decode incoming * characters is UTF-8 */ bool utf8() const { Q_ASSERT(_codec); return _codec->mibEnum() == 106; } /** Returns the special character used for erasing character. */ virtual char eraseChar() const; /** * Sets the key bindings used to key events * ( received through sendKeyEvent() ) into character * streams to send to the terminal. */ void setKeyBindings(const QString &name); /** * Returns the name of the emulation's current key bindings. * See setKeyBindings() */ QString keyBindings() const; /** * Copies the current image into the history and clears the screen. */ virtual void clearEntireScreen() = 0; /** Resets the state of the terminal. */ virtual void reset() = 0; /** * Returns true if the active terminal program is interested in Mouse * Tracking events. * * The programRequestsMouseTracking() signal is emitted when a program * indicates it's interested in Mouse Tracking events. * * See MODE_Mouse100{0,1,2,3} in Vt102Emulation. */ bool programUsesMouseTracking() const; bool programBracketedPasteMode() const; public Q_SLOTS: /** Change the size of the emulation's image */ virtual void setImageSize(int lines, int columns); /** * Interprets a sequence of characters and sends the result to the terminal. * This is equivalent to calling sendKeyEvent() for each character in @p text in succession. */ virtual void sendText(const QString &text) = 0; /** * Interprets a key press event and emits the sendData() signal with * the resulting character stream. */ virtual void sendKeyEvent(QKeyEvent *); /** * Converts information about a mouse event into an xterm-compatible escape * sequence and emits the character sequence via sendData() */ virtual void sendMouseEvent(int buttons, int column, int line, int eventType); /** * Sends a string of characters to the foreground terminal process. * * @param string The characters to send. */ virtual void sendString(const QByteArray &string) = 0; /** * Processes an incoming stream of characters. receiveData() decodes the incoming * character buffer using the current codec(), and then calls receiveChar() for * each unicode character in the resulting buffer. * * receiveData() also starts a timer which causes the outputChanged() signal * to be emitted when it expires. The timer allows multiple updates in quick * succession to be buffered into a single outputChanged() signal emission. * * @param text A string of characters received from the terminal program. * @param length The length of @p text */ void receiveData(const char *text, int length); /** - * Sends information about the focus lost event to the terminal. + * Sends information about the focus event to the terminal. */ - virtual void focusLost() = 0; - - /** - * Sends information about the focus gained event to the terminal. - */ - virtual void focusGained() = 0; + virtual void focusChanged(bool focused) = 0; Q_SIGNALS: /** * Emitted when a buffer of data is ready to send to the * standard input of the terminal. * * @param data The buffer of data ready to be sent */ void sendData(const QByteArray &data); /** * Requests that the pty used by the terminal process * be set to UTF 8 mode. * * Refer to the IUTF8 entry in termios(3) for more information. */ void useUtf8Request(bool); /** * Emitted when the activity state of the emulation is set. * * @param state The new activity state, one of NOTIFYNORMAL, NOTIFYACTIVITY * or NOTIFYBELL */ void stateSet(int state); /** * Emitted when the special sequence indicating the request for data * transmission through ZModem protocol is detected. */ void zmodemDownloadDetected(); void zmodemUploadDetected(); /** * Requests that the color of the text used * to represent the tabs associated with this * emulation be changed. This is a Konsole-specific * extension from pre-KDE 4 times. * * TODO: Document how the parameter works. */ void changeTabTextColorRequest(int color); /** * This is emitted when the program (typically editors and other full-screen * applications, ones that take up the whole terminal display), running in * the terminal indicates whether or not it is interested in Mouse Tracking * events. This is an XTerm extension, for more information have a look at: * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking * * @param usesMouseTracking This will be true if the program is interested * in Mouse Tracking events or false otherwise. */ void programRequestsMouseTracking(bool usesMouseTracking); void enableAlternateScrolling(bool enable); void programBracketedPasteModeChanged(bool bracketedPasteMode); /** * Emitted when the contents of the screen image change. * The emulation buffers the updates from successive image changes, * and only emits outputChanged() at sensible intervals when * there is a lot of terminal activity. * * Normally there is no need for objects other than the screen windows * created with createWindow() to listen for this signal. * * ScreenWindow objects created using createWindow() will emit their * own outputChanged() signal in response to this signal. */ void outputChanged(); /** * Emitted when the program running in the terminal wishes to update * certain session attributes. This allows terminal programs to customize * certain aspects of the terminal emulation display. * * This signal is emitted when the escape sequence "\033]ARG;VALUE\007" * is received in an input string, where ARG is a number specifying * what should be changed and VALUE is a string specifying the new value. * * @param attribute Specifies which session attribute to change: *
    *
  • 0 - Set window icon text and session title to @p newValue
  • *
  • 1 - Set window icon text to @p newValue
  • *
  • 2 - Set session title to @p newValue
  • *
  • 11 - Set the session's default background color to @p newValue, * where @p newValue can be an HTML-style string ("#RRGGBB") or a * named color (e.g. 'red', 'blue'). For more details see: * https://doc.qt.io/qt-5/qcolor.html#setNamedColor *
  • *
  • 31 - Supposedly treats @p newValue as a URL and opens it (NOT * IMPLEMENTED) *
  • *
  • 32 - Sets the icon associated with the session. @p newValue * is the name of the icon to use, which can be the name of any * icon in the current KDE icon theme (eg: 'konsole', 'kate', * 'folder_home') *
  • *
* @param newValue Specifies the new value of the session attribute */ void sessionAttributeChanged(int attribute, const QString &newValue); /** * Emitted when the terminal emulator's size has changed */ void imageSizeChanged(int lineCount, int columnCount); /** * Emitted when the setImageSize() is called on this emulation for * the first time. */ void imageSizeInitialized(); /** * Emitted after receiving the escape sequence which asks to change * the terminal emulator's size */ void imageResizeRequest(const QSize &sizz); /** * Emitted when the terminal program requests to change various properties * of the terminal display. * * A profile change command occurs when a special escape sequence, followed * by a string containing a series of name and value pairs is received. * This string can be parsed using a ProfileCommandParser instance. * * @param text A string expected to contain a series of key and value pairs in * the form: name=value;name2=value2 ... */ void profileChangeCommandReceived(const QString &text); /** * Emitted when a flow control key combination ( Ctrl+S or Ctrl+Q ) is pressed. * @param suspendKeyPressed True if Ctrl+S was pressed to suspend output or Ctrl+Q to * resume output. */ void flowControlKeyPressed(bool suspendKeyPressed); /** * Emitted when the active screen is switched, to indicate whether the primary * screen is in use. */ void primaryScreenInUse(bool use); /** * Emitted when the text selection is changed */ void selectionChanged(const QString &text); /** * Emitted when terminal code requiring terminal's response received. */ void sessionAttributeRequest(int id); /** * Emitted when Set Cursor Style (DECSCUSR) escape sequences are sent * to the terminal. * @p shape cursor shape * @p isBlinking if true, the cursor will be set to blink */ void setCursorStyleRequest(Enum::CursorShapeEnum shape = Enum::BlockCursor, bool isBlinking = false); /** * Emitted when reset() is called to reset the cursor style to the * current profile cursor shape and blinking settings. */ void resetCursorStyleRequest(); protected: virtual void setMode(int mode) = 0; virtual void resetMode(int mode) = 0; /** * Processes an incoming character. See receiveData() * @p c A unicode character code. */ virtual void receiveChar(uint c); /** * Sets the active screen. The terminal has two screens, primary and alternate. * The primary screen is used by default. When certain interactive programs such * as Vim are run, they trigger a switch to the alternate screen. * * @param index 0 to switch to the primary screen, or 1 to switch to the alternate screen */ void setScreen(int index); enum EmulationCodec { LocaleCodec = 0, Utf8Codec = 1 }; void setCodec(EmulationCodec codec); QList _windows; Screen *_currentScreen; // pointer to the screen which is currently active, // this is one of the elements in the screen[] array Screen *_screen[2]; // 0 = primary screen ( used by most programs, including the shell // scrollbars are enabled in this mode ) // 1 = alternate ( used by vi , emacs etc. // scrollbars are not enabled in this mode ) //decodes an incoming C-style character stream into a unicode QString using //the current text codec. (this allows for rendering of non-ASCII characters in text files etc.) const QTextCodec *_codec; QTextDecoder *_decoder; const KeyboardTranslator *_keyTranslator; // the keyboard layout protected Q_SLOTS: /** * Schedules an update of attached views. * Repeated calls to bufferedUpdate() in close succession will result in only a single update, * much like the Qt buffered update of widgets. */ void bufferedUpdate(); // used to emit the primaryScreenInUse(bool) signal void checkScreenInUse(); // used to emit the selectionChanged(QString) signal void checkSelectedText(); private Q_SLOTS: // triggered by timer, causes the emulation to send an updated screen image to each // view void showBulk(); void setUsesMouseTracking(bool usesMouseTracking); void bracketedPasteModeChanged(bool bracketedPasteMode); private: Q_DISABLE_COPY(Emulation) bool _usesMouseTracking; bool _bracketedPasteMode; QTimer _bulkTimer1; QTimer _bulkTimer2; bool _imageSizeInitialized; }; } #endif // ifndef EMULATION_H diff --git a/src/Session.cpp b/src/Session.cpp index e4d6a716..bb628fbb 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -1,1795 +1,1794 @@ /* 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::stateSet, this, &Konsole::Session::activityStateSet); 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::focusLost, _emulation, &Konsole::Emulation::focusLost); - connect(widget, &Konsole::TerminalDisplay::focusGained, _emulation, &Konsole::Emulation::focusGained); + 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); } 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) { emit stateChanged(NOTIFYNORMAL); 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); emit stateChanged(NOTIFYSILENCE); } void Session::activityTimerDone() { _notifiedActivity = 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 "<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 (state == NOTIFYACTIVITY && !_monitorActivity) { state = NOTIFYNORMAL; } if (state == NOTIFYSILENCE && !_monitorSilence) { state = NOTIFYNORMAL; } emit stateChanged(state); } void Session::onViewSizeChange(int /*height*/, int /*width*/) { updateTerminalSize(); } void Session::updateTerminalSize() { int minLines = -1; int minColumns = -1; // minimum number of lines and columns that views require for // their size to be taken into consideration ( to avoid problems // with new view widgets which haven't yet been set to their correct size ) const int VIEW_LINES_THRESHOLD = 2; const int VIEW_COLUMNS_THRESHOLD = 2; //select largest number of lines and columns that will fit in all visible views for (TerminalDisplay *view : qAsConst(_views)) { if (!view->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; } 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(); activityStateSet(NOTIFYNORMAL); } void Session::setMonitorSilence(bool monitor) { if (_monitorSilence == monitor) { return; } _monitorSilence = monitor; if (_monitorSilence) { _silenceTimer->start(_silenceSeconds * 1000); } else { _silenceTimer->stop(); } activityStateSet(NOTIFYNORMAL); } 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) { _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; } 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 b67e6b07..b91d7793 100644 --- a/src/SessionController.cpp +++ b/src/SessionController.cpp @@ -1,1805 +1,1807 @@ /* 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(); - view->installEventFilter(this); - view->setSessionController(this); + 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::stateChanged, this, &Konsole::SessionController::sessionStateChanged); // 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::focusGained, this, &Konsole::SessionController::interactionHandler); - connect(_view.data(), &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::SessionController::interactionHandler); + 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); } } -bool SessionController::eventFilter(QObject* watched , QEvent* event) -{ - if (event->type() == QEvent::FocusIn && watched == _view) { - // 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 focused(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(); - } - } - return Konsole::ViewProperties::eventFilter(watched, event); -} - 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); } } } 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; } } 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::sessionStateChanged(int state) { if (state == _previousState) { return; } if (state == NOTIFYACTIVITY) { setIcon(*_activityIcon); _keepIconUntilInteraction = true; } else if (state == NOTIFYSILENCE) { setIcon(*_silenceIcon); _keepIconUntilInteraction = true; } else if (state == NOTIFYBELL) { setIcon(*_bellIcon); _keepIconUntilInteraction = true; } else if (state == NOTIFYNORMAL) { updateSessionIcon(); } _previousState = state; } 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 ca03abdf..df49275f 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" 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; - // Reimplemented to watch for events happening to the view - bool eventFilter(QObject *watched, QEvent *event) Q_DECL_OVERRIDE; - /** 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; 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 focused(SessionController *controller); + 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 sessionStateChanged(int state); 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 diff --git a/src/TerminalDisplay.cpp b/src/TerminalDisplay.cpp index 2f001370..fe0f4bd3 100644 --- a/src/TerminalDisplay.cpp +++ b/src/TerminalDisplay.cpp @@ -1,4024 +1,4075 @@ /* This file is part of Konsole, a terminal emulator for KDE. Copyright 2006-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle 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 "TerminalDisplay.h" // Config #include "config-konsole.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include // Konsole #include "Filter.h" #include "konsoledebug.h" #include "TerminalCharacterDecoder.h" #include "Screen.h" #include "SessionController.h" #include "ExtendedCharTable.h" #include "TerminalDisplayAccessible.h" #include "SessionManager.h" #include "Session.h" #include "WindowSystemInfo.h" #include "IncrementalSearchBar.h" #include "Profile.h" #include "ViewManager.h" // for colorSchemeForProfile. // TODO: Rewrite this. #include "LineBlockCharacters.h" using namespace Konsole; #define REPCHAR "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "abcdefgjijklmnopqrstuvwxyz" \ "0123456789./+@" // we use this to force QPainter to display text in LTR mode // more information can be found in: https://unicode.org/reports/tr9/ const QChar LTR_OVERRIDE_CHAR(0x202D); inline int TerminalDisplay::loc(int x, int y) const { Q_ASSERT(y >= 0 && y < _lines); Q_ASSERT(x >= 0 && x < _columns); x = qBound(0, x, _columns - 1); y = qBound(0, y, _lines - 1); return y * _columns + x; } /* ------------------------------------------------------------------------- */ /* */ /* Colors */ /* */ /* ------------------------------------------------------------------------- */ /* Note that we use ANSI color order (bgr), while IBMPC color order is (rgb) Code 0 1 2 3 4 5 6 7 ----------- ------- ------- ------- ------- ------- ------- ------- ------- ANSI (bgr) Black Red Green Yellow Blue Magenta Cyan White IBMPC (rgb) Black Blue Green Cyan Red Magenta Yellow White */ ScreenWindow* TerminalDisplay::screenWindow() const { return _screenWindow; } void TerminalDisplay::setScreenWindow(ScreenWindow* window) { // disconnect existing screen window if any if (!_screenWindow.isNull()) { disconnect(_screenWindow , nullptr , this , nullptr); } _screenWindow = window; if (!_screenWindow.isNull()) { connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateLineProperties); connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateImage); connect(_screenWindow.data() , &Konsole::ScreenWindow::currentResultLineChanged , this , &Konsole::TerminalDisplay::updateImage); connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, [this]() { _filterUpdateRequired = true; }); connect(_screenWindow.data(), &Konsole::ScreenWindow::scrolled, this, [this]() { _filterUpdateRequired = true; }); _screenWindow->setWindowLines(_lines); } } const ColorEntry* TerminalDisplay::colorTable() const { return _colorTable; } void TerminalDisplay::onColorsChanged() { // Mostly just fix the scrollbar // this is a workaround to add some readability to old themes like Fusion // changing the light value for button a bit makes themes like fusion, windows and oxygen way more readable and pleasing QPalette p = QApplication::palette(); QColor buttonTextColor = _colorTable[DEFAULT_FORE_COLOR]; QColor backgroundColor = _colorTable[DEFAULT_BACK_COLOR]; backgroundColor.setAlphaF(_opacity); QColor buttonColor = backgroundColor.toHsv(); if (buttonColor.valueF() < 0.5) { buttonColor = buttonColor.lighter(); } else { buttonColor = buttonColor.darker(); } p.setColor(QPalette::Button, buttonColor); p.setColor(QPalette::Window, backgroundColor); p.setColor(QPalette::Base, backgroundColor); p.setColor(QPalette::WindowText, buttonTextColor); p.setColor(QPalette::ButtonText, buttonTextColor); setPalette(p); _scrollBar->setPalette(p); update(); } void TerminalDisplay::setBackgroundColor(const QColor& color) { _colorTable[DEFAULT_BACK_COLOR] = color; onColorsChanged(); } QColor TerminalDisplay::getBackgroundColor() const { return _colorTable[DEFAULT_BACK_COLOR]; } void TerminalDisplay::setForegroundColor(const QColor& color) { _colorTable[DEFAULT_FORE_COLOR] = color; onColorsChanged(); } void TerminalDisplay::setColorTable(const ColorEntry table[]) { for (int i = 0; i < TABLE_COLORS; i++) { _colorTable[i] = table[i]; } setBackgroundColor(_colorTable[DEFAULT_BACK_COLOR]); onColorsChanged(); } /* ------------------------------------------------------------------------- */ /* */ /* Font */ /* */ /* ------------------------------------------------------------------------- */ static inline bool isLineCharString(const QString& string) { if (string.length() == 0) { return false; } return LineBlockCharacters::canDraw(string.at(0).unicode()); } void TerminalDisplay::fontChange(const QFont&) { QFontMetrics fm(font()); _fontHeight = fm.height() + _lineSpacing; Q_ASSERT(_fontHeight > 0); /* TODO: When changing the three deprecated width() below * consider the info in * https://phabricator.kde.org/D23144 comments * horizontalAdvance() was added in Qt 5.11 (which should be the * minimum for 20.04 or 20.08 KDE Applications release) */ // waba TerminalDisplay 1.123: // "Base character width on widest ASCII character. This prevents too wide // characters in the presence of double wide (e.g. Japanese) characters." // Get the width from representative normal width characters _fontWidth = qRound((static_cast(fm.width(QStringLiteral(REPCHAR))) / static_cast(qstrlen(REPCHAR)))); _fixedFont = true; const int fw = fm.width(QLatin1Char(REPCHAR[0])); for (unsigned int i = 1; i < qstrlen(REPCHAR); i++) { if (fw != fm.width(QLatin1Char(REPCHAR[i]))) { _fixedFont = false; break; } } if (_fontWidth < 1) { _fontWidth = 1; } _fontAscent = fm.ascent(); emit changedFontMetricSignal(_fontHeight, _fontWidth); propagateSize(); update(); } void TerminalDisplay::setVTFont(const QFont& f) { QFont newFont(f); int strategy = 0; // hint that text should be drawn with- or without anti-aliasing. // depending on the user's font configuration, this may not be respected strategy |= _antialiasText ? QFont::PreferAntialias : QFont::NoAntialias; // Konsole cannot handle non-integer font metrics strategy |= QFont::ForceIntegerMetrics; // In case the provided font doesn't have some specific characters it should // fall back to a Monospace fonts. newFont.setStyleHint(QFont::TypeWriter, QFont::StyleStrategy(strategy)); // Try to check that a good font has been loaded. // For some fonts, ForceIntegerMetrics causes height() == 0 which // will cause Konsole to crash later. QFontMetrics fontMetrics2(newFont); if (fontMetrics2.height() < 1) { qCDebug(KonsoleDebug)<<"The font "<(fontInfo.styleHint()), fontInfo.weight(), static_cast(fontInfo.style()), static_cast(fontInfo.underline()), static_cast(fontInfo.strikeOut()), // Intentional newFont use - fixedPitch is bugged, see comment above static_cast(newFont.fixedPitch()), static_cast(fontInfo.rawMode())); qCDebug(KonsoleDebug) << "The font to use in the terminal can not be matched exactly on your system."; qCDebug(KonsoleDebug) << " Selected: " << newFont.toString(); qCDebug(KonsoleDebug) << " System : " << nonMatching; } QWidget::setFont(newFont); fontChange(newFont); } void TerminalDisplay::increaseFontSize() { QFont font = getVTFont(); font.setPointSizeF(font.pointSizeF() + 1); setVTFont(font); } void TerminalDisplay::decreaseFontSize() { const qreal MinimumFontSize = 6; QFont font = getVTFont(); font.setPointSizeF(qMax(font.pointSizeF() - 1, MinimumFontSize)); setVTFont(font); } void TerminalDisplay::resetFontSize() { const qreal MinimumFontSize = 6; QFont font = getVTFont(); Profile::Ptr currentProfile = SessionManager::instance()->sessionProfile(_sessionController->session()); const qreal defaultFontSize = currentProfile->font().pointSizeF(); font.setPointSizeF(qMax(defaultFontSize, MinimumFontSize)); setVTFont(font); } uint TerminalDisplay::lineSpacing() const { return _lineSpacing; } void TerminalDisplay::setLineSpacing(uint i) { _lineSpacing = i; fontChange(font()); // Trigger an update. } /* ------------------------------------------------------------------------- */ /* */ /* Accessibility */ /* */ /* ------------------------------------------------------------------------- */ namespace Konsole { #ifndef QT_NO_ACCESSIBILITY /** * This function installs the factory function which lets Qt instantiate the QAccessibleInterface * for the TerminalDisplay. */ QAccessibleInterface* accessibleInterfaceFactory(const QString &key, QObject *object) { Q_UNUSED(key) if (auto *display = qobject_cast(object)) { return new TerminalDisplayAccessible(display); } return nullptr; } #endif } /* ------------------------------------------------------------------------- */ /* */ /* Constructor / Destructor */ /* */ /* ------------------------------------------------------------------------- */ TerminalDisplay::TerminalDisplay(QWidget* parent) : QWidget(parent) , _screenWindow(nullptr) , _bellMasked(false) , _verticalLayout(new QVBoxLayout(this)) , _fixedFont(true) , _fontHeight(1) , _fontWidth(1) , _fontAscent(1) , _boldIntense(true) , _lines(1) , _columns(1) , _usedLines(1) , _usedColumns(1) , _contentRect(QRect()) , _image(nullptr) , _imageSize(0) , _lineProperties(QVector()) , _randomSeed(0) , _resizing(false) , _showTerminalSizeHint(true) , _bidiEnabled(false) , _usesMouseTracking(false) , _alternateScrolling(true) , _bracketedPasteMode(false) , _iPntSel(QPoint()) , _pntSel(QPoint()) , _tripleSelBegin(QPoint()) , _actSel(0) , _wordSelectionMode(false) , _lineSelectionMode(false) , _preserveLineBreaks(true) , _columnSelectionMode(false) , _autoCopySelectedText(false) , _copyTextAsHTML(true) , _middleClickPasteMode(Enum::PasteFromX11Selection) , _scrollBar(nullptr) , _scrollbarLocation(Enum::ScrollBarRight) , _scrollFullPage(false) , _wordCharacters(QStringLiteral(":@-./_~")) , _bellMode(Enum::NotifyBell) , _allowBlinkingText(true) , _allowBlinkingCursor(false) , _textBlinking(false) , _cursorBlinking(false) , _hasTextBlinker(false) , _urlHintsModifiers(Qt::NoModifier) , _showUrlHint(false) , _reverseUrlHints(false) , _openLinksByDirectClick(false) , _ctrlRequiredForDrag(true) , _dropUrlsAsText(false) , _tripleClickMode(Enum::SelectWholeLine) , _possibleTripleClick(false) , _resizeWidget(nullptr) , _resizeTimer(nullptr) , _flowControlWarningEnabled(false) , _outputSuspendedMessageWidget(nullptr) , _lineSpacing(0) , _size(QSize()) , _blendColor(qRgba(0, 0, 0, 0xff)) , _wallpaper(nullptr) , _filterChain(new TerminalImageFilterChain()) , _mouseOverHotspotArea(QRegion()) , _filterUpdateRequired(true) , _cursorShape(Enum::BlockCursor) , _cursorColor(QColor()) , _antialiasText(true) , _useFontLineCharacters(false) , _printerFriendly(false) , _sessionController(nullptr) , _trimLeadingSpaces(false) , _trimTrailingSpaces(false) , _mouseWheelZoom(false) , _margin(1) , _centerContents(false) , _readOnlyMessageWidget(nullptr) , _readOnly(false) , _opacity(1.0) , _dimWhenInactive(false) , _scrollWheelState(ScrollState()) , _searchBar(new IncrementalSearchBar(this)) , _headerBar(new TerminalHeaderBar(this)) , _searchResultRect(QRect()) , _drawOverlay(false) { // terminal applications are not designed with Right-To-Left in mind, // so the layout is forced to Left-To-Right setLayoutDirection(Qt::LeftToRight); _contentRect = QRect(_margin, _margin, 1, 1); // create scroll bar for scrolling output up and down _scrollBar = new QScrollBar(this); _scrollBar->setAutoFillBackground(false); // set the scroll bar's slider to occupy the whole area of the scroll bar initially setScroll(0, 0); _scrollBar->setCursor(Qt::ArrowCursor); _headerBar->setCursor(Qt::ArrowCursor); connect(_headerBar, &TerminalHeaderBar::requestToggleExpansion, this, &Konsole::TerminalDisplay::requestToggleExpansion); connect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged); connect(_scrollBar, &QScrollBar::sliderMoved, this, &Konsole::TerminalDisplay::viewScrolledByUser); // setup timers for blinking text _blinkTextTimer = new QTimer(this); _blinkTextTimer->setInterval(TEXT_BLINK_DELAY); connect(_blinkTextTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkTextEvent); // setup timers for blinking cursor _blinkCursorTimer = new QTimer(this); _blinkCursorTimer->setInterval(QApplication::cursorFlashTime() / 2); connect(_blinkCursorTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkCursorEvent); // hide mouse cursor on keystroke or idle KCursor::setAutoHideCursor(this, true); setMouseTracking(true); setUsesMouseTracking(false); setBracketedPasteMode(false); setColorTable(ColorScheme::defaultTable); // Enable drag and drop support setAcceptDrops(true); _dragInfo.state = diNone; setFocusPolicy(Qt::WheelFocus); // enable input method support setAttribute(Qt::WA_InputMethodEnabled, true); // this is an important optimization, it tells Qt // that TerminalDisplay will handle repainting its entire area. setAttribute(Qt::WA_OpaquePaintEvent); // Add the stretch item once, the KMessageWidgets are inserted at index 0. _verticalLayout->addWidget(_headerBar); _verticalLayout->addStretch(); _verticalLayout->setSpacing(0); _verticalLayout->setContentsMargins(0, 0, 0, 0); setLayout(_verticalLayout); new AutoScrollHandler(this); + + // Keep this last + auto focusWatcher = new CompositeWidgetFocusWatcher(this, this); + connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged, + this, [this](bool focused) {_hasCompositeFocus = focused;}); + connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged, + this, &TerminalDisplay::compositeFocusChanged); + connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged, + _headerBar, &TerminalHeaderBar::setFocusIndicatorState); + #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(Konsole::accessibleInterfaceFactory); #endif } TerminalDisplay::~TerminalDisplay() { disconnect(_blinkTextTimer); disconnect(_blinkCursorTimer); delete _readOnlyMessageWidget; delete _outputSuspendedMessageWidget; delete[] _image; delete _filterChain; _readOnlyMessageWidget = nullptr; _outputSuspendedMessageWidget = nullptr; } void TerminalDisplay::hideDragTarget() { _drawOverlay = false; update(); } void TerminalDisplay::showDragTarget(const QPoint& cursorPos) { using EdgeDistance = std::pair; auto closerToEdge = std::min( { {cursorPos.x(), Qt::LeftEdge}, {cursorPos.y(), Qt::TopEdge}, {width() - cursorPos.x(), Qt::RightEdge}, {height() - cursorPos.y(), Qt::BottomEdge} }, [](const EdgeDistance& left, const EdgeDistance& right) -> bool { return left.first < right.first; } ); if (_overlayEdge == closerToEdge.second) { return; } _overlayEdge = closerToEdge.second; _drawOverlay = true; update(); } /* ------------------------------------------------------------------------- */ /* */ /* Display Operations */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::drawLineCharString(QPainter& painter, int x, int y, const QString& str, const Character* attributes) { // only turn on anti-aliasing during this short time for the "text" // for the normal text we have TextAntialiasing on demand on // otherwise we have rendering artifacts // set https://bugreports.qt.io/browse/QTBUG-66036 painter.setRenderHint(QPainter::Antialiasing, _antialiasText); const bool useBoldPen = (attributes->rendition & RE_BOLD) != 0 && _boldIntense; QRect cellRect = {x, y, _fontWidth, _fontHeight}; for (int i = 0 ; i < str.length(); i++) { LineBlockCharacters::draw(painter, cellRect.translated(i * _fontWidth, 0), str[i], useBoldPen); } painter.setRenderHint(QPainter::Antialiasing, false); } void TerminalDisplay::setKeyboardCursorShape(Enum::CursorShapeEnum shape) { _cursorShape = shape; } Enum::CursorShapeEnum TerminalDisplay::keyboardCursorShape() const { return _cursorShape; } void TerminalDisplay::setCursorStyle(Enum::CursorShapeEnum shape, bool isBlinking) { setKeyboardCursorShape(shape); setBlinkingCursorEnabled(isBlinking); // when the cursor shape and blinking state are changed via the // Set Cursor Style (DECSCUSR) escape sequences in vim, and if the // cursor isn't set to blink, the cursor shape doesn't actually // change until the cursor is moved by the user; calling update() // makes the cursor shape get updated sooner. if (!isBlinking) { update(); } } void TerminalDisplay::resetCursorStyle() { Q_ASSERT(_sessionController != nullptr); Q_ASSERT(!_sessionController->session().isNull()); Profile::Ptr currentProfile = SessionManager::instance()->sessionProfile(_sessionController->session()); if (currentProfile != nullptr) { auto shape = static_cast(currentProfile->property(Profile::CursorShape)); setKeyboardCursorShape(shape); setBlinkingCursorEnabled(currentProfile->blinkingCursorEnabled()); } } void TerminalDisplay::setKeyboardCursorColor(const QColor& color) { _cursorColor = color; } QColor TerminalDisplay::keyboardCursorColor() const { return _cursorColor; } void TerminalDisplay::setOpacity(qreal opacity) { QColor color(_blendColor); color.setAlphaF(opacity); _opacity = opacity; _blendColor = color.rgba(); onColorsChanged(); } void TerminalDisplay::setWallpaper(const ColorSchemeWallpaper::Ptr &p) { _wallpaper = p; } void TerminalDisplay::drawBackground(QPainter& painter, const QRect& rect, const QColor& backgroundColor, bool useOpacitySetting) { // the area of the widget showing the contents of the terminal display is drawn // using the background color from the color scheme set with setColorTable() // // the area of the widget behind the scroll-bar is drawn using the background // brush from the scroll-bar's palette, to give the effect of the scroll-bar // being outside of the terminal display and visual consistency with other KDE // applications. if (useOpacitySetting && !_wallpaper->isNull() && _wallpaper->draw(painter, rect, _opacity)) { } else if (qAlpha(_blendColor) < 0xff && useOpacitySetting) { #if defined(Q_OS_MACOS) // TODO - On MacOS, using CompositionMode doesn't work. Altering the // transparency in the color scheme alters the brightness. painter.fillRect(rect, backgroundColor); #else QColor color(backgroundColor); color.setAlpha(qAlpha(_blendColor)); const QPainter::CompositionMode originalMode = painter.compositionMode(); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(rect, color); painter.setCompositionMode(originalMode); #endif } else { painter.fillRect(rect, backgroundColor); } } void TerminalDisplay::drawCursor(QPainter& painter, const QRect& rect, const QColor& foregroundColor, const QColor& /*backgroundColor*/, bool& invertCharacterColor) { // don't draw cursor which is currently blinking if (_cursorBlinking) { return; } // shift rectangle top down one pixel to leave some space // between top and bottom QRectF cursorRect = rect.adjusted(0, 1, 0, 0); QColor cursorColor = _cursorColor.isValid() ? _cursorColor : foregroundColor; painter.setPen(cursorColor); if (_cursorShape == Enum::BlockCursor) { // draw the cursor outline, adjusting the area so that // it is draw entirely inside 'rect' int penWidth = qMax(1, painter.pen().width()); painter.drawRect(cursorRect.adjusted(int(penWidth / 2) + 0.5, int(penWidth / 2) + 0.5, - int(penWidth / 2) - penWidth % 2 + 0.5, - int(penWidth / 2) - penWidth % 2 + 0.5)); // draw the cursor body only when the widget has focus if (hasFocus()) { painter.fillRect(cursorRect, cursorColor); if (!_cursorColor.isValid()) { // invert the color used to draw the text to ensure that the character at // the cursor position is readable invertCharacterColor = true; } } } else if (_cursorShape == Enum::UnderlineCursor) { QLineF line(cursorRect.left() + 0.5, cursorRect.bottom() - 0.5, cursorRect.right() - 0.5, cursorRect.bottom() - 0.5); painter.drawLine(line); } else if (_cursorShape == Enum::IBeamCursor) { QLineF line(cursorRect.left() + 0.5, cursorRect.top() + 0.5, cursorRect.left() + 0.5, cursorRect.bottom() - 0.5); painter.drawLine(line); } } void TerminalDisplay::drawCharacters(QPainter& painter, const QRect& rect, const QString& text, const Character* style, bool invertCharacterColor) { // don't draw text which is currently blinking if (_textBlinking && ((style->rendition & RE_BLINK) != 0)) { return; } // don't draw concealed characters if ((style->rendition & RE_CONCEAL) != 0) { return; } static constexpr int MaxFontWeight = 99; // https://doc.qt.io/qt-5/qfont.html#Weight-enum const int normalWeight = font().weight(); // +26 makes "bold" from "normal", "normal" from "light", etc. It is 26 instead of not 25 to prefer // bolder weight when 25 falls in the middle between two weights. See QFont::Weight const int boldWeight = qMin(normalWeight + 26, MaxFontWeight); const auto isBold = [boldWeight](const QFont &font) { return font.weight() >= boldWeight; }; const bool useBold = (((style->rendition & RE_BOLD) != 0) && _boldIntense); const bool useUnderline = ((style->rendition & RE_UNDERLINE) != 0) || font().underline(); const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || font().italic(); const bool useStrikeOut = ((style->rendition & RE_STRIKEOUT) != 0) || font().strikeOut(); const bool useOverline = ((style->rendition & RE_OVERLINE) != 0) || font().overline(); QFont currentFont = painter.font(); if (isBold(currentFont) != useBold || currentFont.underline() != useUnderline || currentFont.italic() != useItalic || currentFont.strikeOut() != useStrikeOut || currentFont.overline() != useOverline) { currentFont.setWeight(useBold ? boldWeight : normalWeight); currentFont.setUnderline(useUnderline); currentFont.setItalic(useItalic); currentFont.setStrikeOut(useStrikeOut); currentFont.setOverline(useOverline); painter.setFont(currentFont); } // setup pen const CharacterColor& textColor = (invertCharacterColor ? style->backgroundColor : style->foregroundColor); const QColor color = textColor.color(_colorTable); QPen pen = painter.pen(); if (pen.color() != color) { pen.setColor(color); painter.setPen(color); } const bool origClipping = painter.hasClipping(); const auto origClipRegion = painter.clipRegion(); painter.setClipRect(rect); // draw text if (isLineCharString(text) && !_useFontLineCharacters) { drawLineCharString(painter, rect.x(), rect.y(), text, style); } else { // Force using LTR as the document layout for the terminal area, because // there is no use cases for RTL emulator and RTL terminal application. // // This still allows RTL characters to be rendered in the RTL way. painter.setLayoutDirection(Qt::LeftToRight); if (_bidiEnabled) { painter.drawText(rect.x(), rect.y() + _fontAscent + _lineSpacing, text); } else { painter.drawText(rect.x(), rect.y() + _fontAscent + _lineSpacing, LTR_OVERRIDE_CHAR + text); } } painter.setClipRegion(origClipRegion); painter.setClipping(origClipping); } void TerminalDisplay::drawTextFragment(QPainter& painter , const QRect& rect, const QString& text, const Character* style) { // setup painter const QColor foregroundColor = style->foregroundColor.color(_colorTable); const QColor backgroundColor = style->backgroundColor.color(_colorTable); // draw background if different from the display's background color if (backgroundColor != getBackgroundColor()) { drawBackground(painter, rect, backgroundColor, false /* do not use transparency */); } // draw cursor shape if the current character is the cursor // this may alter the foreground and background colors bool invertCharacterColor = false; if ((style->rendition & RE_CURSOR) != 0) { drawCursor(painter, rect, foregroundColor, backgroundColor, invertCharacterColor); } // draw text drawCharacters(painter, rect, text, style, invertCharacterColor); } void TerminalDisplay::drawPrinterFriendlyTextFragment(QPainter& painter, const QRect& rect, const QString& text, const Character* style) { // Set the colors used to draw to black foreground and white // background for printer friendly output when printing Character print_style = *style; print_style.foregroundColor = CharacterColor(COLOR_SPACE_RGB, 0x00000000); print_style.backgroundColor = CharacterColor(COLOR_SPACE_RGB, 0xFFFFFFFF); // draw text drawCharacters(painter, rect, text, &print_style, false); } void TerminalDisplay::setRandomSeed(uint randomSeed) { _randomSeed = randomSeed; } uint TerminalDisplay::randomSeed() const { return _randomSeed; } // scrolls the image by 'lines', down if lines > 0 or up otherwise. // // the terminal emulation keeps track of the scrolling of the character // image as it receives input, and when the view is updated, it calls scrollImage() // with the final scroll amount. this improves performance because scrolling the // display is much cheaper than re-rendering all the text for the // part of the image which has moved up or down. // Instead only new lines have to be drawn void TerminalDisplay::scrollImage(int lines , const QRect& screenWindowRegion) { // return if there is nothing to do if ((lines == 0) || (_image == nullptr)) { return; } // if the flow control warning is enabled this will interfere with the // scrolling optimizations and cause artifacts. the simple solution here // is to just disable the optimization whilst it is visible if ((_outputSuspendedMessageWidget != nullptr) && _outputSuspendedMessageWidget->isVisible()) { return; } if ((_readOnlyMessageWidget != nullptr) && _readOnlyMessageWidget->isVisible()) { return; } // constrain the region to the display // the bottom of the region is capped to the number of lines in the display's // internal image - 2, so that the height of 'region' is strictly less // than the height of the internal image. QRect region = screenWindowRegion; region.setBottom(qMin(region.bottom(), _lines - 2)); // return if there is nothing to do if (!region.isValid() || (region.top() + abs(lines)) >= region.bottom() || _lines <= region.height()) { return; } // hide terminal size label to prevent it being scrolled if ((_resizeWidget != nullptr) && _resizeWidget->isVisible()) { _resizeWidget->hide(); } // Note: With Qt 4.4 the left edge of the scrolled area must be at 0 // to get the correct (newly exposed) part of the widget repainted. // // The right edge must be before the left edge of the scroll bar to // avoid triggering a repaint of the entire widget, the distance is // given by SCROLLBAR_CONTENT_GAP // // Set the QT_FLUSH_PAINT environment variable to '1' before starting the // application to monitor repainting. // const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->width(); const int SCROLLBAR_CONTENT_GAP = 1; QRect scrollRect; if (_scrollbarLocation == Enum::ScrollBarLeft) { scrollRect.setLeft(scrollBarWidth + SCROLLBAR_CONTENT_GAP); scrollRect.setRight(width()); } else { scrollRect.setLeft(0); scrollRect.setRight(width() - scrollBarWidth - SCROLLBAR_CONTENT_GAP); } void* firstCharPos = &_image[ region.top() * _columns ]; void* lastCharPos = &_image[(region.top() + abs(lines)) * _columns ]; const int top = _contentRect.top() + (region.top() * _fontHeight); const int linesToMove = region.height() - abs(lines); const int bytesToMove = linesToMove * _columns * sizeof(Character); Q_ASSERT(linesToMove > 0); Q_ASSERT(bytesToMove > 0); scrollRect.setTop( lines > 0 ? top : top + abs(lines) * _fontHeight); scrollRect.setHeight(linesToMove * _fontHeight); if (!scrollRect.isValid() || scrollRect.isEmpty()) { return; } //scroll internal image if (lines > 0) { // check that the memory areas that we are going to move are valid Q_ASSERT((char*)lastCharPos + bytesToMove < (char*)(_image + (_lines * _columns))); Q_ASSERT((lines * _columns) < _imageSize); //scroll internal image down memmove(firstCharPos , lastCharPos , bytesToMove); } else { // check that the memory areas that we are going to move are valid Q_ASSERT((char*)firstCharPos + bytesToMove < (char*)(_image + (_lines * _columns))); //scroll internal image up memmove(lastCharPos , firstCharPos , bytesToMove); } //scroll the display vertically to match internal _image scroll(0 , _fontHeight * (-lines) , scrollRect); } QRegion TerminalDisplay::hotSpotRegion() const { QRegion region; for (const Filter::HotSpot *hotSpot : _filterChain->hotSpots()) { QRect r; r.setLeft(hotSpot->startColumn()); r.setTop(hotSpot->startLine()); if (hotSpot->startLine() == hotSpot->endLine()) { r.setRight(hotSpot->endColumn()); r.setBottom(hotSpot->endLine()); region |= imageToWidget(r); } else { r.setRight(_columns); r.setBottom(hotSpot->startLine()); region |= imageToWidget(r); r.setLeft(0); for (int line = hotSpot->startLine() + 1 ; line < hotSpot->endLine(); line++) { r.moveTop(line); region |= imageToWidget(r); } r.moveTop(hotSpot->endLine()); r.setRight(hotSpot->endColumn()); region |= imageToWidget(r); } } return region; } void TerminalDisplay::processFilters() { if (_screenWindow.isNull()) { return; } if (!_filterUpdateRequired) { return; } QRegion preUpdateHotSpots = hotSpotRegion(); // use _screenWindow->getImage() here rather than _image because // other classes may call processFilters() when this display's // ScreenWindow emits a scrolled() signal - which will happen before // updateImage() is called on the display and therefore _image is // out of date at this point _filterChain->setImage(_screenWindow->getImage(), _screenWindow->windowLines(), _screenWindow->windowColumns(), _screenWindow->getLineProperties()); _filterChain->process(); QRegion postUpdateHotSpots = hotSpotRegion(); update(preUpdateHotSpots | postUpdateHotSpots); _filterUpdateRequired = false; } void TerminalDisplay::updateImage() { if (_screenWindow.isNull()) { return; } // optimization - scroll the existing image where possible and // avoid expensive text drawing for parts of the image that // can simply be moved up or down // disable this shortcut for transparent konsole with scaled pixels, otherwise we get rendering artifacts, see BUG 350651 if (!(WindowSystemInfo::HAVE_TRANSPARENCY && (qApp->devicePixelRatio() > 1.0)) && _wallpaper->isNull() && !_searchBar->isVisible()) { scrollImage(_screenWindow->scrollCount() , _screenWindow->scrollRegion()); } if (_image == nullptr) { // Create _image. // The emitted changedContentSizeSignal also leads to getImage being recreated, so do this first. updateImageSize(); } Character* const newimg = _screenWindow->getImage(); const int lines = _screenWindow->windowLines(); const int columns = _screenWindow->windowColumns(); setScroll(_screenWindow->currentLine() , _screenWindow->lineCount()); Q_ASSERT(_usedLines <= _lines); Q_ASSERT(_usedColumns <= _columns); int y, x, len; const QPoint tL = contentsRect().topLeft(); const int tLx = tL.x(); const int tLy = tL.y(); _hasTextBlinker = false; CharacterColor cf; // undefined const int linesToUpdate = qMin(_lines, qMax(0, lines)); const int columnsToUpdate = qMin(_columns, qMax(0, columns)); auto dirtyMask = new char[columnsToUpdate + 2]; QRegion dirtyRegion; // debugging variable, this records the number of lines that are found to // be 'dirty' ( ie. have changed from the old _image to the new _image ) and // which therefore need to be repainted int dirtyLineCount = 0; for (y = 0; y < linesToUpdate; ++y) { const Character* currentLine = &_image[y * _columns]; const Character* const newLine = &newimg[y * columns]; bool updateLine = false; // The dirty mask indicates which characters need repainting. We also // mark surrounding neighbors dirty, in case the character exceeds // its cell boundaries memset(dirtyMask, 0, columnsToUpdate + 2); for (x = 0 ; x < columnsToUpdate ; ++x) { if (newLine[x] != currentLine[x]) { dirtyMask[x] = 1; } } if (!_resizing) { // not while _resizing, we're expecting a paintEvent for (x = 0; x < columnsToUpdate; ++x) { _hasTextBlinker |= (newLine[x].rendition & RE_BLINK); // Start drawing if this character or the next one differs. // We also take the next one into account to handle the situation // where characters exceed their cell width. if (dirtyMask[x] != 0) { if (newLine[x + 0].character == 0u) { continue; } const bool lineDraw = LineBlockCharacters::canDraw(newLine[x + 0].character); const bool doubleWidth = (x + 1 == columnsToUpdate) ? false : (newLine[x + 1].character == 0); const RenditionFlags cr = newLine[x].rendition; const CharacterColor clipboard = newLine[x].backgroundColor; if (newLine[x].foregroundColor != cf) { cf = newLine[x].foregroundColor; } const int lln = columnsToUpdate - x; for (len = 1; len < lln; ++len) { const Character& ch = newLine[x + len]; if (ch.character == 0u) { continue; // Skip trailing part of multi-col chars. } const bool nextIsDoubleWidth = (x + len + 1 == columnsToUpdate) ? false : (newLine[x + len + 1].character == 0); if (ch.foregroundColor != cf || ch.backgroundColor != clipboard || (ch.rendition & ~RE_EXTENDED_CHAR) != (cr & ~RE_EXTENDED_CHAR) || (dirtyMask[x + len] == 0) || LineBlockCharacters::canDraw(ch.character) != lineDraw || nextIsDoubleWidth != doubleWidth) { break; } } const bool saveFixedFont = _fixedFont; if (lineDraw) { _fixedFont = false; } if (doubleWidth) { _fixedFont = false; } updateLine = true; _fixedFont = saveFixedFont; x += len - 1; } } } //both the top and bottom halves of double height _lines must always be redrawn //although both top and bottom halves contain the same characters, only //the top one is actually //drawn. if (_lineProperties.count() > y) { updateLine |= (_lineProperties[y] & LINE_DOUBLEHEIGHT); } // if the characters on the line are different in the old and the new _image // then this line must be repainted. if (updateLine) { dirtyLineCount++; // add the area occupied by this line to the region which needs to be // repainted QRect dirtyRect = QRect(_contentRect.left() + tLx , _contentRect.top() + tLy + _fontHeight * y , _fontWidth * columnsToUpdate , _fontHeight); dirtyRegion |= dirtyRect; } // replace the line of characters in the old _image with the // current line of the new _image memcpy((void*)currentLine, (const void*)newLine, columnsToUpdate * sizeof(Character)); } // if the new _image is smaller than the previous _image, then ensure that the area // outside the new _image is cleared if (linesToUpdate < _usedLines) { dirtyRegion |= QRect(_contentRect.left() + tLx , _contentRect.top() + tLy + _fontHeight * linesToUpdate , _fontWidth * _columns , _fontHeight * (_usedLines - linesToUpdate)); } _usedLines = linesToUpdate; if (columnsToUpdate < _usedColumns) { dirtyRegion |= QRect(_contentRect.left() + tLx + columnsToUpdate * _fontWidth , _contentRect.top() + tLy , _fontWidth * (_usedColumns - columnsToUpdate) , _fontHeight * _lines); } _usedColumns = columnsToUpdate; dirtyRegion |= _inputMethodData.previousPreeditRect; if ((_screenWindow->currentResultLine() != -1) && (_screenWindow->scrollCount() != 0)) { // De-highlight previous result region dirtyRegion |= _searchResultRect; // Highlight new result region dirtyRegion |= QRect(0, _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _fontHeight, _columns * _fontWidth, _fontHeight); } _screenWindow->resetScrollCount(); // update the parts of the display which have changed update(dirtyRegion); if (_allowBlinkingText && _hasTextBlinker && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!_hasTextBlinker && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } delete[] dirtyMask; #ifndef QT_NO_ACCESSIBILITY QAccessibleEvent dataChangeEvent(this, QAccessible::VisibleDataChanged); QAccessible::updateAccessibility(&dataChangeEvent); QAccessibleTextCursorEvent cursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX()); QAccessible::updateAccessibility(&cursorEvent); #endif } void TerminalDisplay::showResizeNotification() { if (_showTerminalSizeHint && isVisible()) { if (_resizeWidget == nullptr) { _resizeWidget = new QLabel(i18n("Size: XXX x XXX"), this); _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().boundingRect(i18n("Size: XXX x XXX")).width()); _resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height()); _resizeWidget->setAlignment(Qt::AlignCenter); _resizeWidget->setStyleSheet(QStringLiteral("background-color:palette(window);border-style:solid;border-width:1px;border-color:palette(dark)")); _resizeTimer = new QTimer(this); _resizeTimer->setInterval(SIZE_HINT_DURATION); _resizeTimer->setSingleShot(true); connect(_resizeTimer, &QTimer::timeout, _resizeWidget, &QLabel::hide); } QString sizeStr = i18n("Size: %1 x %2", _columns, _lines); _resizeWidget->setText(sizeStr); _resizeWidget->move((width() - _resizeWidget->width()) / 2, (height() - _resizeWidget->height()) / 2 + 20); _resizeWidget->show(); _resizeTimer->start(); } } void TerminalDisplay::paintEvent(QPaintEvent* pe) { QPainter paint(this); // Determine which characters should be repainted (1 region unit = 1 character) QRegion dirtyImageRegion; const QRegion region = pe->region() & contentsRect(); for (const QRect &rect : region) { dirtyImageRegion += widgetToImage(rect); drawBackground(paint, rect, getBackgroundColor(), true /* use opacity setting */); } // only turn on text anti-aliasing, never turn on normal antialiasing // set https://bugreports.qt.io/browse/QTBUG-66036 paint.setRenderHint(QPainter::TextAntialiasing, _antialiasText); for (const QRect &rect : qAsConst(dirtyImageRegion)) { drawContents(paint, rect); } drawCurrentResultRect(paint); drawInputMethodPreeditString(paint, preeditRect()); paintFilters(paint); const bool drawDimmed = _dimWhenInactive && !hasFocus(); const QColor dimColor(0, 0, 0, 128); for (const QRect &rect : region) { if (drawDimmed) { paint.fillRect(rect, dimColor); } } if (_drawOverlay) { const auto y = _headerBar->isVisible() ? _headerBar->height() : 0; const auto rect = _overlayEdge == Qt::LeftEdge ? QRect(0, y, width() / 2, height()) : _overlayEdge == Qt::TopEdge ? QRect(0, y, width(), height() / 2) : _overlayEdge == Qt::RightEdge ? QRect(width() - width() / 2, y, width() / 2, height()) : QRect(0, height() - height() / 2, width(), height() / 2); paint.setRenderHint(QPainter::Antialiasing); paint.setPen(Qt::NoPen); paint.setBrush(QColor(100,100,100, 127)); paint.drawRect(rect); } } void TerminalDisplay::printContent(QPainter& painter, bool friendly) { // Reinitialize the font with the printers paint device so the font // measurement calculations will be done correctly QFont savedFont = getVTFont(); QFont font(savedFont, painter.device()); painter.setFont(font); setVTFont(font); QRect rect(0, 0, _usedColumns, _usedLines); _printerFriendly = friendly; if (!friendly) { drawBackground(painter, rect, getBackgroundColor(), true /* use opacity setting */); } drawContents(painter, rect); _printerFriendly = false; setVTFont(savedFont); } QPoint TerminalDisplay::cursorPosition() const { if (!_screenWindow.isNull()) { return _screenWindow->cursorPosition(); } else { return {0, 0}; } } inline bool TerminalDisplay::isCursorOnDisplay() const { return cursorPosition().x() < _columns && cursorPosition().y() < _lines; } FilterChain* TerminalDisplay::filterChain() const { return _filterChain; } void TerminalDisplay::paintFilters(QPainter& painter) { if (_filterUpdateRequired) { return; } // get color of character under mouse and use it to draw // lines for filters QPoint cursorPos = mapFromGlobal(QCursor::pos()); int cursorLine; int cursorColumn; getCharacterPosition(cursorPos, cursorLine, cursorColumn, false); Character cursorCharacter = _image[loc(qMin(cursorColumn, _columns - 1), cursorLine)]; painter.setPen(QPen(cursorCharacter.foregroundColor.color(_colorTable))); // iterate over hotspots identified by the display's currently active filters // and draw appropriate visuals to indicate the presence of the hotspot QList spots = _filterChain->hotSpots(); int urlNumber, urlNumInc; if (_reverseUrlHints) { urlNumber = spots.size() + 1; urlNumInc = -1; } else { urlNumber = 0; urlNumInc = 1; } foreach(Filter::HotSpot* spot, spots) { urlNumber += urlNumInc; QRegion region; if (spot->type() == Filter::HotSpot::Link) { QRect r; if (spot->startLine() == spot->endLine()) { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } else { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) { r.setCoords(0 * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } r.setCoords(0 * _fontWidth + _contentRect.left(), spot->endLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } if (_showUrlHint && urlNumber < 10) { // Position at the beginning of the URL QRect hintRect(*region.begin()); hintRect.setWidth(r.height()); painter.fillRect(hintRect, QColor(0, 0, 0, 128)); painter.setPen(Qt::white); painter.drawRect(hintRect.adjusted(0, 0, -1, -1)); painter.drawText(hintRect, Qt::AlignCenter, QString::number(urlNumber)); } } for (int line = spot->startLine() ; line <= spot->endLine() ; line++) { int startColumn = 0; int endColumn = _columns - 1; // TODO use number of _columns which are actually // occupied on this line rather than the width of the // display in _columns // Check image size so _image[] is valid (see makeImage) if (endColumn >= _columns || line >= _lines) { break; } // ignore whitespace at the end of the lines while (_image[loc(endColumn, line)].isSpace() && endColumn > 0) { endColumn--; } // increment here because the column which we want to set 'endColumn' to // is the first whitespace character at the end of the line endColumn++; if (line == spot->startLine()) { startColumn = spot->startColumn(); } if (line == spot->endLine()) { endColumn = spot->endColumn(); } // TODO: resolve this comment with the new margin/center code // subtract one pixel from // the right and bottom so that // we do not overdraw adjacent // hotspots // // subtracting one pixel from all sides also prevents an edge case where // moving the mouse outside a link could still leave it underlined // because the check below for the position of the cursor // finds it on the border of the target area QRect r; r.setCoords(startColumn * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), endColumn * _fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); // Underline link hotspots if (spot->type() == Filter::HotSpot::Link) { QFontMetrics metrics(font()); // find the baseline (which is the invisible line that the characters in the font sit on, // with some having tails dangling below) const int baseline = r.bottom() - metrics.descent(); // find the position of the underline below that const int underlinePos = baseline + metrics.underlinePos(); if (_showUrlHint || region.contains(mapFromGlobal(QCursor::pos()))) { painter.drawLine(r.left() , underlinePos , r.right() , underlinePos); } // Marker hotspots simply have a transparent rectangular shape // drawn on top of them } else if (spot->type() == Filter::HotSpot::Marker) { //TODO - Do not use a hardcoded color for this const bool isCurrentResultLine = (_screenWindow->currentResultLine() == (spot->startLine() + _screenWindow->currentLine())); QColor color = isCurrentResultLine ? QColor(255, 255, 0, 120) : QColor(255, 0, 0, 120); painter.fillRect(r, color); } } } } inline static bool isRtl(const Character &chr) { uint c = 0; if ((chr.rendition & RE_EXTENDED_CHAR) == 0) { c = chr.character; } else { ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(chr.character, extendedCharLength); if (chars != nullptr) { c = chars[0]; } } switch(QChar::direction(c)) { case QChar::DirR: case QChar::DirAL: case QChar::DirRLE: case QChar::DirRLI: case QChar::DirRLO: return true; default: return false; } } void TerminalDisplay::drawContents(QPainter& paint, const QRect& rect) { const int numberOfColumns = _usedColumns; QVector univec; univec.reserve(numberOfColumns); for (int y = rect.y(); y <= rect.bottom(); y++) { int x = rect.x(); if ((_image[loc(rect.x(), y)].character == 0u) && (x != 0)) { x--; // Search for start of multi-column character } for (; x <= rect.right(); x++) { int len = 1; int p = 0; // reset our buffer to the number of columns int bufferSize = numberOfColumns; univec.resize(bufferSize); uint *disstrU = univec.data(); // is this a single character or a sequence of characters ? if ((_image[loc(x, y)].rendition & RE_EXTENDED_CHAR) != 0) { // sequence of characters ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(_image[loc(x, y)].character, extendedCharLength); if (chars != nullptr) { Q_ASSERT(extendedCharLength > 1); bufferSize += extendedCharLength - 1; univec.resize(bufferSize); disstrU = univec.data(); for (int index = 0 ; index < extendedCharLength ; index++) { Q_ASSERT(p < bufferSize); disstrU[p++] = chars[index]; } } } else { // single character const uint c = _image[loc(x, y)].character; if (c != 0u) { Q_ASSERT(p < bufferSize); disstrU[p++] = c; } } const bool lineDraw = LineBlockCharacters::canDraw(_image[loc(x, y)].character); const bool doubleWidth = (_image[qMin(loc(x, y) + 1, _imageSize - 1)].character == 0); const CharacterColor currentForeground = _image[loc(x, y)].foregroundColor; const CharacterColor currentBackground = _image[loc(x, y)].backgroundColor; const RenditionFlags currentRendition = _image[loc(x, y)].rendition; const bool rtl = isRtl(_image[loc(x, y)]); const auto isInsideDrawArea = [&](int column) { return column <= rect.right(); }; const auto hasSameColors = [&](int column) { return _image[loc(column, y)].foregroundColor == currentForeground && _image[loc(column, y)].backgroundColor == currentBackground; }; const auto hasSameRendition = [&](int column) { return (_image[loc(column, y)].rendition & ~RE_EXTENDED_CHAR) == (currentRendition & ~RE_EXTENDED_CHAR); }; const auto hasSameWidth = [&](int column) { const int characterLoc = qMin(loc(column, y) + 1, _imageSize - 1); return (_image[characterLoc].character == 0) == doubleWidth; }; const auto canBeGrouped = [&](int column) { return _image[loc(column, y)].character <= 0x7e || rtl; }; if (canBeGrouped(x)) { while (isInsideDrawArea(x + len) && hasSameColors(x + len) && hasSameRendition(x + len) && hasSameWidth(x + len) && canBeGrouped(x + len)) { const uint c = _image[loc(x + len, y)].character; if ((_image[loc(x + len, y)].rendition & RE_EXTENDED_CHAR) != 0) { // sequence of characters ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(c, extendedCharLength); if (chars != nullptr) { Q_ASSERT(extendedCharLength > 1); bufferSize += extendedCharLength - 1; univec.resize(bufferSize); disstrU = univec.data(); for (int index = 0 ; index < extendedCharLength ; index++) { Q_ASSERT(p < bufferSize); disstrU[p++] = chars[index]; } } } else { // single character if (c != 0u) { Q_ASSERT(p < bufferSize); disstrU[p++] = c; } } if (doubleWidth) { // assert((_image[loc(x+len,y)+1].character == 0)), see above if condition len++; // Skip trailing part of multi-column character } len++; } } else { // Group spaces following any non-wide character with the character. This allows for // rendering ambiguous characters with wide glyphs without clipping them. while (!doubleWidth && isInsideDrawArea(x + len) && _image[loc(x + len, y)].character == ' ' && hasSameColors(x + len) && hasSameRendition(x + len)) { // disstrU intentionally not modified - trailing spaces are meaningless len++; } } if ((x + len < _usedColumns) && (_image[loc(x + len, y)].character == 0u)) { len++; // Adjust for trailing part of multi-column character } const bool save__fixedFont = _fixedFont; if (lineDraw) { _fixedFont = false; } if (doubleWidth) { _fixedFont = false; } univec.resize(p); // Create a text scaling matrix for double width and double height lines. QMatrix textScale; if (y < _lineProperties.size()) { if ((_lineProperties[y] & LINE_DOUBLEWIDTH) != 0) { textScale.scale(2, 1); } if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) { textScale.scale(1, 2); } } //Apply text scaling matrix. // TODO: setWorldMatrix is obsolete, change to setWorldTransform paint.setWorldMatrix(textScale, true); //calculate the area in which the text will be drawn QRect textArea = QRect(_contentRect.left() + contentsRect().left() + _fontWidth * x, _contentRect.top() + contentsRect().top() + _fontHeight * y, _fontWidth * len, _fontHeight); //move the calculated area to take account of scaling applied to the painter. //the position of the area from the origin (0,0) is scaled //by the opposite of whatever //transformation has been applied to the painter. this ensures that //painting does actually start from textArea.topLeft() //(instead of textArea.topLeft() * painter-scale) textArea.moveTopLeft(textScale.inverted().map(textArea.topLeft())); QString unistr = QString::fromUcs4(univec.data(), univec.length()); //paint text fragment if (_printerFriendly) { drawPrinterFriendlyTextFragment(paint, textArea, unistr, &_image[loc(x, y)]); } else { drawTextFragment(paint, textArea, unistr, &_image[loc(x, y)]); } _fixedFont = save__fixedFont; //reset back to single-width, single-height _lines // TODO: setWorldMatrix is obsolete, change to setWorldTransform paint.setWorldMatrix(textScale.inverted(), true); if (y < _lineProperties.size() - 1) { //double-height _lines are represented by two adjacent _lines //containing the same characters //both _lines will have the LINE_DOUBLEHEIGHT attribute. //If the current line has the LINE_DOUBLEHEIGHT attribute, //we can therefore skip the next line if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) { y++; } } x += len - 1; } } } void TerminalDisplay::drawCurrentResultRect(QPainter& painter) { if(_screenWindow->currentResultLine() == -1) { return; } _searchResultRect.setRect(0, _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _fontHeight, _columns * _fontWidth, _fontHeight); painter.fillRect(_searchResultRect, QColor(0, 0, 255, 80)); } QRect TerminalDisplay::imageToWidget(const QRect& imageArea) const { QRect result; result.setLeft(_contentRect.left() + _fontWidth * imageArea.left()); result.setTop(_contentRect.top() + _fontHeight * imageArea.top()); result.setWidth(_fontWidth * imageArea.width()); result.setHeight(_fontHeight * imageArea.height()); return result; } QRect TerminalDisplay::widgetToImage(const QRect &widgetArea) const { QRect result; result.setLeft( qMin(_usedColumns - 1, qMax(0, (widgetArea.left() - contentsRect().left() - _contentRect.left()) / _fontWidth ))); result.setTop( qMin(_usedLines - 1, qMax(0, (widgetArea.top() - contentsRect().top() - _contentRect.top() ) / _fontHeight))); result.setRight( qMin(_usedColumns - 1, qMax(0, (widgetArea.right() - contentsRect().left() - _contentRect.left()) / _fontWidth ))); result.setBottom(qMin(_usedLines - 1, qMax(0, (widgetArea.bottom() - contentsRect().top() - _contentRect.top() ) / _fontHeight))); return result; } /* ------------------------------------------------------------------------- */ /* */ /* Blinking Text & Cursor */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setBlinkingCursorEnabled(bool blink) { _allowBlinkingCursor = blink; if (blink && !_blinkCursorTimer->isActive()) { _blinkCursorTimer->start(); } if (!blink && _blinkCursorTimer->isActive()) { _blinkCursorTimer->stop(); if (_cursorBlinking) { // if cursor is blinking(hidden), blink it again to make it show _cursorBlinking = false; updateCursor(); } Q_ASSERT(!_cursorBlinking); } } void TerminalDisplay::setBlinkingTextEnabled(bool blink) { _allowBlinkingText = blink; if (blink && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!blink && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } } void TerminalDisplay::focusOutEvent(QFocusEvent*) { // trigger a repaint of the cursor so that it is both: // // * visible (in case it was hidden during blinking) // * drawn in a focused out state _cursorBlinking = false; updateCursor(); // suppress further cursor blinking _blinkCursorTimer->stop(); Q_ASSERT(!_cursorBlinking); // if text is blinking (hidden), blink it again to make it shown if (_textBlinking) { blinkTextEvent(); } // suppress further text blinking _blinkTextTimer->stop(); Q_ASSERT(!_textBlinking); _showUrlHint = false; - _headerBar->terminalFocusOut(); - emit focusLost(); } void TerminalDisplay::focusInEvent(QFocusEvent*) { if (_allowBlinkingCursor) { _blinkCursorTimer->start(); } updateCursor(); if (_allowBlinkingText && _hasTextBlinker) { _blinkTextTimer->start(); } - _headerBar->terminalFocusIn(); - emit focusGained(); } void TerminalDisplay::blinkTextEvent() { Q_ASSERT(_allowBlinkingText); _textBlinking = !_textBlinking; // TODO: Optimize to only repaint the areas of the widget where there is // blinking text rather than repainting the whole widget. - _headerBar->terminalFocusOut(); update(); } void TerminalDisplay::blinkCursorEvent() { Q_ASSERT(_allowBlinkingCursor); _cursorBlinking = !_cursorBlinking; updateCursor(); } void TerminalDisplay::updateCursor() { if (!isCursorOnDisplay()){ return; } const int cursorLocation = loc(cursorPosition().x(), cursorPosition().y()); Q_ASSERT(cursorLocation < _imageSize); int charWidth = _image[cursorLocation].width(); QRect cursorRect = imageToWidget(QRect(cursorPosition(), QSize(charWidth, 1))); update(cursorRect); } /* ------------------------------------------------------------------------- */ /* */ /* Geometry & Resizing */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) if (contentsRect().isValid()) { // NOTE: This calls setTabText() in TabbedViewContainer::updateTitle(), // which might update the widget size again. New resizeEvent // won't be called, do not rely on new sizes before this call. updateImageSize(); updateImage(); } const auto scrollBarWidth = _scrollbarLocation != Enum::ScrollBarHidden ? _scrollBar->width() : 0; const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0; const auto x = width() - scrollBarWidth - _searchBar->width(); const auto y = headerHeight; _searchBar->move(x, y); } void TerminalDisplay::propagateSize() { if (_image != nullptr) { updateImageSize(); } } void TerminalDisplay::updateImageSize() { Character* oldImage = _image; const int oldLines = _lines; const int oldColumns = _columns; makeImage(); if (oldImage != nullptr) { // copy the old image to reduce flicker int lines = qMin(oldLines, _lines); int columns = qMin(oldColumns, _columns); for (int line = 0; line < lines; line++) { memcpy((void*)&_image[_columns * line], (void*)&oldImage[oldColumns * line], columns * sizeof(Character)); } delete[] oldImage; } if (!_screenWindow.isNull()) { _screenWindow->setWindowLines(_lines); } _resizing = (oldLines != _lines) || (oldColumns != _columns); if (_resizing) { showResizeNotification(); emit changedContentSizeSignal(_contentRect.height(), _contentRect.width()); // expose resizeEvent } _resizing = false; } void TerminalDisplay::makeImage() { _wallpaper->load(); calcGeometry(); // confirm that array will be of non-zero size, since the painting code // assumes a non-zero array length Q_ASSERT(_lines > 0 && _columns > 0); Q_ASSERT(_usedLines <= _lines && _usedColumns <= _columns); _imageSize = _lines * _columns; _image = new Character[_imageSize]; clearImage(); } void TerminalDisplay::clearImage() { for (int i = 0; i < _imageSize; ++i) { _image[i] = Screen::DefaultChar; } } void TerminalDisplay::calcGeometry() { const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0; _scrollBar->resize( _scrollBar->sizeHint().width(), // width contentsRect().height() - headerHeight // height ); _contentRect = contentsRect().adjusted(_margin, _margin, -_margin, -_margin); switch (_scrollbarLocation) { case Enum::ScrollBarHidden : break; case Enum::ScrollBarLeft : _contentRect.setLeft(_contentRect.left() + _scrollBar->width()); _scrollBar->move(contentsRect().left(), contentsRect().top() + headerHeight); break; case Enum::ScrollBarRight: _contentRect.setRight(_contentRect.right() - _scrollBar->width()); _scrollBar->move(contentsRect().left() + contentsRect().width() - _scrollBar->width(), contentsRect().top() + headerHeight); break; } _contentRect.setTop(_contentRect.top() + headerHeight); // ensure that display is always at least one column wide _columns = qMax(1, _contentRect.width() / _fontWidth); _usedColumns = qMin(_usedColumns, _columns); // ensure that display is always at least one line high _lines = qMax(1, _contentRect.height() / _fontHeight); _usedLines = qMin(_usedLines, _lines); if(_centerContents) { QSize unusedPixels = _contentRect.size() - QSize(_columns * _fontWidth, _lines * _fontHeight); _contentRect.adjust(unusedPixels.width() / 2, unusedPixels.height() / 2, 0, 0); } } // calculate the needed size, this must be synced with calcGeometry() void TerminalDisplay::setSize(int columns, int lines) { const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->sizeHint().width(); const int horizontalMargin = _margin * 2; const int verticalMargin = _margin * 2; QSize newSize = QSize(horizontalMargin + scrollBarWidth + (columns * _fontWidth) , verticalMargin + (lines * _fontHeight)); if (newSize != size()) { _size = newSize; updateGeometry(); } } QSize TerminalDisplay::sizeHint() const { return _size; } //showEvent and hideEvent are reimplemented here so that it appears to other classes that the //display has been resized when the display is hidden or shown. // //TODO: Perhaps it would be better to have separate signals for show and hide instead of using //the same signal as the one for a content size change void TerminalDisplay::showEvent(QShowEvent*) { propagateSize(); emit changedContentSizeSignal(_contentRect.height(), _contentRect.width()); } void TerminalDisplay::hideEvent(QHideEvent*) { emit changedContentSizeSignal(_contentRect.height(), _contentRect.width()); } void TerminalDisplay::setMargin(int margin) { if (margin < 0) { margin = 0; } _margin = margin; updateImageSize(); } void TerminalDisplay::setCenterContents(bool enable) { _centerContents = enable; calcGeometry(); update(); } /* ------------------------------------------------------------------------- */ /* */ /* Scrollbar */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setScrollBarPosition(Enum::ScrollBarPositionEnum position) { if (_scrollbarLocation == position) { return; } _scrollBar->setHidden(position == Enum::ScrollBarHidden); _scrollbarLocation = position; propagateSize(); update(); } void TerminalDisplay::scrollBarPositionChanged(int) { if (_screenWindow.isNull()) { return; } _screenWindow->scrollTo(_scrollBar->value()); // if the thumb has been moved to the bottom of the _scrollBar then set // the display to automatically track new output, // that is, scroll down automatically // to how new _lines as they are added const bool atEndOfOutput = (_scrollBar->value() == _scrollBar->maximum()); _screenWindow->setTrackOutput(atEndOfOutput); updateImage(); } void TerminalDisplay::setScroll(int cursor, int slines) { // update _scrollBar if the range or value has changed, // otherwise return // // setting the range or value of a _scrollBar will always trigger // a repaint, so it should be avoided if it is not necessary if (_scrollBar->minimum() == 0 && _scrollBar->maximum() == (slines - _lines) && _scrollBar->value() == cursor) { return; } disconnect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged); _scrollBar->setRange(0, slines - _lines); _scrollBar->setSingleStep(1); _scrollBar->setPageStep(_lines); _scrollBar->setValue(cursor); connect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged); } void TerminalDisplay::setScrollFullPage(bool fullPage) { _scrollFullPage = fullPage; } bool TerminalDisplay::scrollFullPage() const { return _scrollFullPage; } /* ------------------------------------------------------------------------- */ /* */ /* Mouse */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::mousePressEvent(QMouseEvent* ev) { if (_possibleTripleClick && (ev->button() == Qt::LeftButton)) { mouseTripleClickEvent(ev); return; } if (!contentsRect().contains(ev->pos())) { return; } if (_screenWindow.isNull()) { return; } // Ignore clicks on the message widget if (_readOnlyMessageWidget != nullptr) { if (_readOnlyMessageWidget->isVisible() && _readOnlyMessageWidget->frameGeometry().contains(ev->pos())) { return; } } if (_outputSuspendedMessageWidget != nullptr) { if (_outputSuspendedMessageWidget->isVisible() && _outputSuspendedMessageWidget->frameGeometry().contains(ev->pos())) { return; } } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); QPoint pos = QPoint(charColumn, charLine); if (ev->button() == Qt::LeftButton) { // request the software keyboard, if any if (qApp->autoSipEnabled()) { auto behavior = QStyle::RequestSoftwareInputPanel( style()->styleHint(QStyle::SH_RequestSoftwareInputPanel)); if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) { QEvent event(QEvent::RequestSoftwareInputPanel); QApplication::sendEvent(this, &event); } } if (!ev->modifiers()) { _lineSelectionMode = false; _wordSelectionMode = false; } // The user clicked inside selected text bool selected = _screenWindow->isSelected(pos.x(), pos.y()); // Drag only when the Control key is held if ((!_ctrlRequiredForDrag || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && selected) { _dragInfo.state = diPending; _dragInfo.start = ev->pos(); } else { // No reason to ever start a drag event _dragInfo.state = diNone; _preserveLineBreaks = !(((ev->modifiers() & Qt::ControlModifier) != 0u) && !(ev->modifiers() & Qt::AltModifier)); _columnSelectionMode = ((ev->modifiers() & Qt::AltModifier) != 0u) && ((ev->modifiers() & Qt::ControlModifier) != 0u); // There are a couple of use cases when selecting text : // Normal buffer or Alternate buffer when not using Mouse Tracking: // select text or extendSelection or columnSelection or columnSelection + extendSelection // // Alternate buffer when using Mouse Tracking and with Shift pressed: // select text or columnSelection if (!_usesMouseTracking && ((ev->modifiers() == Qt::ShiftModifier) || (((ev->modifiers() & Qt::ShiftModifier) != 0u) && _columnSelectionMode))) { extendSelection(ev->pos()); } else if ((!_usesMouseTracking && !((ev->modifiers() & Qt::ShiftModifier))) || (_usesMouseTracking && ((ev->modifiers() & Qt::ShiftModifier) != 0u))) { _screenWindow->clearSelection(); pos.ry() += _scrollBar->value(); _iPntSel = _pntSel = pos; _actSel = 1; // left mouse button pressed but nothing selected yet. } else if (_usesMouseTracking && !_readOnly) { emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u))) { Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) { QObject action; action.setObjectName(QStringLiteral("open-action")); spot->activate(&action); } } } } else if (ev->button() == Qt::MidButton) { processMidButtonClick(ev); } else if (ev->button() == Qt::RightButton) { if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) { emit configureRequest(ev->pos()); } else { if(!_readOnly) { emit mouseSignal(2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } } QList TerminalDisplay::filterActions(const QPoint& position) { int charLine, charColumn; getCharacterPosition(position, charLine, charColumn, false); Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); return spot != nullptr ? spot->actions() : QList(); } void TerminalDisplay::mouseMoveEvent(QMouseEvent* ev) { int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); processFilters(); // handle filters // change link hot-spot appearance on mouse-over Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) { QRegion previousHotspotArea = _mouseOverHotspotArea; _mouseOverHotspotArea = QRegion(); QRect r; if (spot->startLine() == spot->endLine()) { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } else { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) { r.setCoords(0 * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } r.setCoords(0 * _fontWidth + _contentRect.left(), spot->endLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && (cursor().shape() != Qt::PointingHandCursor)) { setCursor(Qt::PointingHandCursor); } update(_mouseOverHotspotArea | previousHotspotArea); } else if (!_mouseOverHotspotArea.isEmpty()) { if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) || (cursor().shape() == Qt::PointingHandCursor)) { setCursor(_usesMouseTracking ? Qt::ArrowCursor : Qt::IBeamCursor); } update(_mouseOverHotspotArea); // set hotspot area to an invalid rectangle _mouseOverHotspotArea = QRegion(); } // for auto-hiding the cursor, we need mouseTracking if (ev->buttons() == Qt::NoButton) { return; } // if the program running in the terminal is interested in Mouse Tracking // evnets then emit a mouse movement signal, unless the shift key is // being held down, which overrides this. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { int button = 3; if ((ev->buttons() & Qt::LeftButton) != 0u) { button = 0; } if ((ev->buttons() & Qt::MidButton) != 0u) { button = 1; } if ((ev->buttons() & Qt::RightButton) != 0u) { button = 2; } emit mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 1); return; } if (_dragInfo.state == diPending) { // we had a mouse down, but haven't confirmed a drag yet // if the mouse has moved sufficiently, we will confirm const int distance = QApplication::startDragDistance(); if (ev->x() > _dragInfo.start.x() + distance || ev->x() < _dragInfo.start.x() - distance || ev->y() > _dragInfo.start.y() + distance || ev->y() < _dragInfo.start.y() - distance) { // we've left the drag square, we can start a real drag operation now _screenWindow->clearSelection(); doDrag(); } return; } else if (_dragInfo.state == diDragging) { // this isn't technically needed because mouseMoveEvent is suppressed during // Qt drag operations, replaced by dragMoveEvent return; } if (_actSel == 0) { return; } // don't extend selection while pasting if ((ev->buttons() & Qt::MidButton) != 0u) { return; } extendSelection(ev->pos()); } void TerminalDisplay::leaveEvent(QEvent *) { // remove underline from an active link when cursor leaves the widget area if(!_mouseOverHotspotArea.isEmpty()) { update(_mouseOverHotspotArea); _mouseOverHotspotArea = QRegion(); } } void TerminalDisplay::extendSelection(const QPoint& position) { if (_screenWindow.isNull()) { return; } //if ( !contentsRect().contains(ev->pos()) ) return; const QPoint tL = contentsRect().topLeft(); const int tLx = tL.x(); const int tLy = tL.y(); const int scroll = _scrollBar->value(); // we're in the process of moving the mouse with the left button pressed // the mouse cursor will kept caught within the bounds of the text in // this widget. int linesBeyondWidget = 0; QRect textBounds(tLx + _contentRect.left(), tLy + _contentRect.top(), _usedColumns * _fontWidth - 1, _usedLines * _fontHeight - 1); QPoint pos = position; // Adjust position within text area bounds. const QPoint oldpos = pos; pos.setX(qBound(textBounds.left(), pos.x(), textBounds.right())); pos.setY(qBound(textBounds.top(), pos.y(), textBounds.bottom())); if (oldpos.y() > textBounds.bottom()) { linesBeyondWidget = (oldpos.y() - textBounds.bottom()) / _fontHeight; _scrollBar->setValue(_scrollBar->value() + linesBeyondWidget + 1); // scrollforward } if (oldpos.y() < textBounds.top()) { linesBeyondWidget = (textBounds.top() - oldpos.y()) / _fontHeight; _scrollBar->setValue(_scrollBar->value() - linesBeyondWidget - 1); // history } int charColumn = 0; int charLine = 0; getCharacterPosition(pos, charLine, charColumn, true); QPoint here = QPoint(charColumn, charLine); QPoint ohere; QPoint _iPntSelCorr = _iPntSel; _iPntSelCorr.ry() -= _scrollBar->value(); QPoint _pntSelCorr = _pntSel; _pntSelCorr.ry() -= _scrollBar->value(); bool swapping = false; if (_wordSelectionMode) { // Extend to word boundaries const bool left_not_right = (here.y() < _iPntSelCorr.y() || (here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x())); const bool old_left_not_right = (_pntSelCorr.y() < _iPntSelCorr.y() || (_pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x())); swapping = left_not_right != old_left_not_right; // Find left (left_not_right ? from here : from start of word) QPoint left = left_not_right ? here : _iPntSelCorr; // Find left (left_not_right ? from end of word : from here) QPoint right = left_not_right ? _iPntSelCorr : here; if (left.y() < 0 || left.y() >= _lines || left.x() < 0 || left.x() >= _columns) { left = _pntSelCorr; } else { left = findWordStart(left); } if (right.y() < 0 || right.y() >= _lines || right.x() < 0 || right.x() >= _columns) { right = _pntSelCorr; } else { right = findWordEnd(right); } // Pick which is start (ohere) and which is extension (here) if (left_not_right) { here = left; ohere = right; } else { here = right; ohere = left; } ohere.rx()++; } if (_lineSelectionMode) { // Extend to complete line const bool above_not_below = (here.y() < _iPntSelCorr.y()); if (above_not_below) { ohere = findLineEnd(_iPntSelCorr); here = findLineStart(here); } else { ohere = findLineStart(_iPntSelCorr); here = findLineEnd(here); } swapping = !(_tripleSelBegin == ohere); _tripleSelBegin = ohere; ohere.rx()++; } int offset = 0; if (!_wordSelectionMode && !_lineSelectionMode) { QChar selClass; const bool left_not_right = (here.y() < _iPntSelCorr.y() || (here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x())); const bool old_left_not_right = (_pntSelCorr.y() < _iPntSelCorr.y() || (_pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x())); swapping = left_not_right != old_left_not_right; // Find left (left_not_right ? from here : from start) const QPoint left = left_not_right ? here : _iPntSelCorr; // Find left (left_not_right ? from start : from here) QPoint right = left_not_right ? _iPntSelCorr : here; if (right.x() > 0 && !_columnSelectionMode) { if (right.x() - 1 < _columns && right.y() < _lines) { selClass = charClass(_image[loc(right.x() - 1, right.y())]); } } // Pick which is start (ohere) and which is extension (here) if (left_not_right) { here = left; ohere = right; offset = 0; } else { here = right; ohere = left; offset = -1; } } if ((here == _pntSelCorr) && (scroll == _scrollBar->value())) { return; // not moved } if (here == ohere) { return; // It's not left, it's not right. } if (_actSel < 2 || swapping) { if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) { _screenWindow->setSelectionStart(ohere.x() , ohere.y() , true); } else { _screenWindow->setSelectionStart(ohere.x() - 1 - offset , ohere.y() , false); } } _actSel = 2; // within selection _pntSel = here; _pntSel.ry() += _scrollBar->value(); if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) { _screenWindow->setSelectionEnd(here.x() , here.y()); } else { _screenWindow->setSelectionEnd(here.x() + offset , here.y()); } } void TerminalDisplay::mouseReleaseEvent(QMouseEvent* ev) { if (_screenWindow.isNull()) { return; } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); if (ev->button() == Qt::LeftButton) { if (_dragInfo.state == diPending) { // We had a drag event pending but never confirmed. Kill selection _screenWindow->clearSelection(); } else { if (_actSel > 1) { copyToX11Selection(); } _actSel = 0; //FIXME: emits a release event even if the mouse is // outside the range. The procedure used in `mouseMoveEvent' // applies here, too. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2); } } _dragInfo.state = diNone; } if (_usesMouseTracking && (ev->button() == Qt::RightButton || ev->button() == Qt::MidButton) && !(ev->modifiers() & Qt::ShiftModifier)) { emit mouseSignal(ev->button() == Qt::MidButton ? 1 : 2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2); } } void TerminalDisplay::getCharacterPosition(const QPoint& widgetPoint, int& line, int& column, bool edge) const { // the column value returned can be equal to _usedColumns (when edge == true), // which is the position just after the last character displayed in a line. // // this is required so that the user can select characters in the right-most // column (or left-most for right-to-left input) const int columnMax = edge ? _usedColumns : _usedColumns - 1; const int xOffset = edge ? _fontWidth / 2 : 0; column = qBound(0, (widgetPoint.x() + xOffset - contentsRect().left() - _contentRect.left()) / _fontWidth, columnMax); line = qBound(0, (widgetPoint.y() - contentsRect().top() - _contentRect.top()) / _fontHeight, _usedLines - 1); } void TerminalDisplay::updateLineProperties() { if (_screenWindow.isNull()) { return; } _lineProperties = _screenWindow->getLineProperties(); } void TerminalDisplay::processMidButtonClick(QMouseEvent* ev) { if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) { const bool appendEnter = (ev->modifiers() & Qt::ControlModifier) != 0u; if (_middleClickPasteMode == Enum::PasteFromX11Selection) { pasteFromX11Selection(appendEnter); } else if (_middleClickPasteMode == Enum::PasteFromClipboard) { pasteFromClipboard(appendEnter); } else { Q_ASSERT(false); } } else { if(!_readOnly) { int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); emit mouseSignal(1, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } void TerminalDisplay::mouseDoubleClickEvent(QMouseEvent* ev) { // Yes, successive middle click can trigger this event if (ev->button() == Qt::MidButton) { processMidButtonClick(ev); return; } if (ev->button() != Qt::LeftButton) { return; } if (_screenWindow.isNull()) { return; } int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); QPoint pos(qMin(charColumn, _columns - 1), qMin(charLine, _lines - 1)); // pass on double click as two clicks. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { if(!_readOnly) { // Send just _ONE_ click event, since the first click of the double click // was already sent by the click handler emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0); // left button } return; } _screenWindow->clearSelection(); _iPntSel = pos; _iPntSel.ry() += _scrollBar->value(); _wordSelectionMode = true; _actSel = 2; // within selection // find word boundaries... { // find the start of the word const QPoint bgnSel = findWordStart(pos); const QPoint endSel = findWordEnd(pos); _actSel = 2; // within selection _screenWindow->setSelectionStart(bgnSel.x() , bgnSel.y() , false); _screenWindow->setSelectionEnd(endSel.x() , endSel.y()); copyToX11Selection(); } _possibleTripleClick = true; QTimer::singleShot(QApplication::doubleClickInterval(), this, [this]() { _possibleTripleClick = false; }); } void TerminalDisplay::wheelEvent(QWheelEvent* ev) { // Only vertical scrolling is supported if (ev->orientation() != Qt::Vertical) { return; } const int modifiers = ev->modifiers(); // ctrl+ for zooming, like in konqueror and firefox if (((modifiers & Qt::ControlModifier) != 0u) && _mouseWheelZoom) { _scrollWheelState.addWheelEvent(ev); int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE); for (;steps > 0; --steps) { // wheel-up for increasing font size increaseFontSize(); } for (;steps < 0; ++steps) { // wheel-down for decreasing font size decreaseFontSize(); } } else if (!_usesMouseTracking && (_scrollBar->maximum() > 0)) { // If the program running in the terminal is not interested in Mouse // Tracking events, send the event to the scrollbar if the slider // has room to move _scrollWheelState.addWheelEvent(ev); _scrollBar->event(ev); Q_ASSERT(_sessionController != nullptr); _sessionController->setSearchStartToWindowCurrentLine(); _scrollWheelState.clearAll(); } else if (!_readOnly) { _scrollWheelState.addWheelEvent(ev); Q_ASSERT(!_sessionController->session().isNull()); if(!_usesMouseTracking && !_sessionController->session()->isPrimaryScreen() && _alternateScrolling) { // Send simulated up / down key presses to the terminal program // for the benefit of programs such as 'less' (which use the alternate screen) // assume that each Up / Down key event will cause the terminal application // to scroll by one line. // // to get a reasonable scrolling speed, scroll by one line for every 5 degrees // of mouse wheel rotation. Mouse wheels typically move in steps of 15 degrees, // giving a scroll of 3 lines const int lines = _scrollWheelState.consumeSteps(static_cast(_fontHeight * qApp->devicePixelRatio()), ScrollState::degreesToAngle(5)); const int keyCode = lines > 0 ? Qt::Key_Up : Qt::Key_Down; QKeyEvent keyEvent(QEvent::KeyPress, keyCode, Qt::NoModifier); for (int i = 0; i < abs(lines); i++) { _screenWindow->screen()->setCurrentTerminalDisplay(this); emit keyPressedSignal(&keyEvent); } } else if (_usesMouseTracking) { // terminal program wants notification of mouse activity int charLine; int charColumn; getCharacterPosition(ev->pos() , charLine , charColumn, !_usesMouseTracking); const int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE); const int button = (steps > 0) ? 4 : 5; for (int i = 0; i < abs(steps); ++i) { emit mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } } void TerminalDisplay::viewScrolledByUser() { Q_ASSERT(_sessionController != nullptr); _sessionController->setSearchStartToWindowCurrentLine(); } /* Moving left/up from the line containing pnt, return the starting offset point which the given line is continuously wrapped (top left corner = 0,0; previous line not visible = 0,-1). */ QPoint TerminalDisplay::findLineStart(const QPoint &pnt) { const int visibleScreenLines = _lineProperties.size(); const int topVisibleLine = _screenWindow->currentLine(); Screen *screen = _screenWindow->screen(); int line = pnt.y(); int lineInHistory= line + topVisibleLine; QVector lineProperties = _lineProperties; while (lineInHistory > 0) { for (; line > 0; line--, lineInHistory--) { // Does previous line wrap around? if ((lineProperties[line - 1] & LINE_WRAPPED) == 0) { return {0, lineInHistory - topVisibleLine}; } } if (lineInHistory < 1) { break; } // _lineProperties is only for the visible screen, so grab new data int newRegionStart = qMax(0, lineInHistory - visibleScreenLines); lineProperties = screen->getLineProperties(newRegionStart, lineInHistory - 1); line = lineInHistory - newRegionStart; } return {0, lineInHistory - topVisibleLine}; } /* Moving right/down from the line containing pnt, return the ending offset point which the given line is continuously wrapped. */ QPoint TerminalDisplay::findLineEnd(const QPoint &pnt) { const int visibleScreenLines = _lineProperties.size(); const int topVisibleLine = _screenWindow->currentLine(); const int maxY = _screenWindow->lineCount() - 1; Screen *screen = _screenWindow->screen(); int line = pnt.y(); int lineInHistory= line + topVisibleLine; QVector lineProperties = _lineProperties; while (lineInHistory < maxY) { for (; line < lineProperties.count() && lineInHistory < maxY; line++, lineInHistory++) { // Does current line wrap around? if ((lineProperties[line] & LINE_WRAPPED) == 0) { return {_columns - 1, lineInHistory - topVisibleLine}; } } line = 0; lineProperties = screen->getLineProperties(lineInHistory, qMin(lineInHistory + visibleScreenLines, maxY)); } return {_columns - 1, lineInHistory - topVisibleLine}; } QPoint TerminalDisplay::findWordStart(const QPoint &pnt) { const int regSize = qMax(_screenWindow->windowLines(), 10); const int firstVisibleLine = _screenWindow->currentLine(); Screen *screen = _screenWindow->screen(); Character *image = _image; Character *tmp_image = nullptr; int imgLine = pnt.y(); int x = pnt.x(); int y = imgLine + firstVisibleLine; int imgLoc = loc(x, imgLine); QVector lineProperties = _lineProperties; const QChar selClass = charClass(image[imgLoc]); const int imageSize = regSize * _columns; while (true) { for (;;imgLoc--, x--) { if (imgLoc < 1) { // no more chars in this region break; } if (x > 0) { // has previous char on this line if (charClass(image[imgLoc - 1]) == selClass) { continue; } goto out; } else if (imgLine > 0) { // not the first line in the session if ((lineProperties[imgLine - 1] & LINE_WRAPPED) != 0) { // have continuation on prev line if (charClass(image[imgLoc - 1]) == selClass) { x = _columns; imgLine--; y--; continue; } } goto out; } else if (y > 0) { // want more data, but need to fetch new region break; } else { goto out; } } if (y <= 0) { // No more data goto out; } int newRegStart = qMax(0, y - regSize + 1); lineProperties = screen->getLineProperties(newRegStart, y - 1); imgLine = y - newRegStart; delete[] tmp_image; tmp_image = new Character[imageSize]; image = tmp_image; screen->getImage(tmp_image, imageSize, newRegStart, y - 1); imgLoc = loc(x, imgLine); if (imgLoc < 1) { // Reached the start of the session break; } } out: delete[] tmp_image; return {x, y - firstVisibleLine}; } QPoint TerminalDisplay::findWordEnd(const QPoint &pnt) { const int regSize = qMax(_screenWindow->windowLines(), 10); const int curLine = _screenWindow->currentLine(); int i = pnt.y(); int x = pnt.x(); int y = i + curLine; int j = loc(x, i); QVector lineProperties = _lineProperties; Screen *screen = _screenWindow->screen(); Character *image = _image; Character *tmp_image = nullptr; const QChar selClass = charClass(image[j]); const int imageSize = regSize * _columns; const int maxY = _screenWindow->lineCount() - 1; const int maxX = _columns - 1; while (true) { const int lineCount = lineProperties.count(); for (;;j++, x++) { if (x < maxX) { if (charClass(image[j + 1]) == selClass && // A colon right before whitespace is never part of a word ! (image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) { continue; } goto out; } else if (i < lineCount - 1) { if (((lineProperties[i] & LINE_WRAPPED) != 0) && charClass(image[j + 1]) == selClass && // A colon right before whitespace is never part of a word ! (image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) { x = -1; i++; y++; continue; } goto out; } else if (y < maxY) { if (i < lineCount && ((lineProperties[i] & LINE_WRAPPED) == 0)) { goto out; } break; } else { goto out; } } int newRegEnd = qMin(y + regSize - 1, maxY); lineProperties = screen->getLineProperties(y, newRegEnd); i = 0; if (tmp_image == nullptr) { tmp_image = new Character[imageSize]; image = tmp_image; } screen->getImage(tmp_image, imageSize, y, newRegEnd); x--; j = loc(x, i); } out: y -= curLine; // In word selection mode don't select @ (64) if at end of word. if (((image[j].rendition & RE_EXTENDED_CHAR) == 0) && (QChar(image[j].character) == QLatin1Char('@')) && (y > pnt.y() || x > pnt.x())) { if (x > 0) { x--; } else { y--; } } delete[] tmp_image; return {x, y}; } Screen::DecodingOptions TerminalDisplay::currentDecodingOptions() { Screen::DecodingOptions decodingOptions; if (_preserveLineBreaks) { decodingOptions |= Screen::PreserveLineBreaks; } if (_trimLeadingSpaces) { decodingOptions |= Screen::TrimLeadingWhitespace; } if (_trimTrailingSpaces) { decodingOptions |= Screen::TrimTrailingWhitespace; } return decodingOptions; } void TerminalDisplay::mouseTripleClickEvent(QMouseEvent* ev) { if (_screenWindow.isNull()) { return; } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, true); selectLine(QPoint(charColumn, charLine), _tripleClickMode == Enum::SelectWholeLine); } void TerminalDisplay::selectLine(QPoint pos, bool entireLine) { _iPntSel = pos; _screenWindow->clearSelection(); _lineSelectionMode = true; _wordSelectionMode = false; _actSel = 2; // within selection if (!entireLine) { // Select from cursor to end of line _tripleSelBegin = findWordStart(_iPntSel); _screenWindow->setSelectionStart(_tripleSelBegin.x(), _tripleSelBegin.y() , false); } else { _tripleSelBegin = findLineStart(_iPntSel); _screenWindow->setSelectionStart(0 , _tripleSelBegin.y() , false); } _iPntSel = findLineEnd(_iPntSel); _screenWindow->setSelectionEnd(_iPntSel.x() , _iPntSel.y()); copyToX11Selection(); _iPntSel.ry() += _scrollBar->value(); } void TerminalDisplay::selectCurrentLine() { if (_screenWindow.isNull()) { return; } selectLine(cursorPosition(), true); } void TerminalDisplay::selectAll() { if (_screenWindow.isNull()) { return; } _preserveLineBreaks = true; _screenWindow->setSelectionByLineRange(0, _screenWindow->lineCount()); copyToX11Selection(); } bool TerminalDisplay::focusNextPrevChild(bool next) { // for 'Tab', always disable focus switching among widgets // for 'Shift+Tab', leave the decision to higher level if (next) { return false; } else { return QWidget::focusNextPrevChild(next); } } QChar TerminalDisplay::charClass(const Character& ch) const { if ((ch.rendition & RE_EXTENDED_CHAR) != 0) { ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength); if ((chars != nullptr) && extendedCharLength > 0) { const QString s = QString::fromUcs4(chars, extendedCharLength); if (_wordCharacters.contains(s, Qt::CaseInsensitive)) { return QLatin1Char('a'); } bool letterOrNumber = false; for (int i = 0; !letterOrNumber && i < s.size(); ++i) { letterOrNumber = s.at(i).isLetterOrNumber(); } return letterOrNumber ? QLatin1Char('a') : s.at(0); } return 0; } else { const QChar qch(ch.character); if (qch.isSpace()) { return QLatin1Char(' '); } if (qch.isLetterOrNumber() || _wordCharacters.contains(qch, Qt::CaseInsensitive)) { return QLatin1Char('a'); } return qch; } } void TerminalDisplay::setWordCharacters(const QString& wc) { _wordCharacters = wc; } void TerminalDisplay::setUsesMouseTracking(bool on) { _usesMouseTracking = on; setCursor(_usesMouseTracking ? Qt::ArrowCursor : Qt::IBeamCursor); } bool TerminalDisplay::usesMouseTracking() const { return _usesMouseTracking; } void TerminalDisplay::setAlternateScrolling(bool enable) { _alternateScrolling = enable; } bool TerminalDisplay::alternateScrolling() const { return _alternateScrolling; } void TerminalDisplay::setBracketedPasteMode(bool on) { _bracketedPasteMode = on; } bool TerminalDisplay::bracketedPasteMode() const { return _bracketedPasteMode; } /* ------------------------------------------------------------------------- */ /* */ /* Clipboard */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::doPaste(QString text, bool appendReturn) { if (_screenWindow.isNull()) { return; } if (_readOnly) { return; } if (appendReturn) { text.append(QLatin1String("\r")); } if (text.length() > 8000) { if (KMessageBox::warningContinueCancel(window(), i18np("Are you sure you want to paste %1 character?", "Are you sure you want to paste %1 characters?", text.length()), i18n("Confirm Paste"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("ShowPasteHugeTextWarning")) == KMessageBox::Cancel) { return; } } // Most code in Konsole uses UTF-32. We're filtering // UTF-16 here, as all control characters can be represented // in this encoding as single code unit. If you ever need to // filter anything above 0xFFFF (specific code points or // categories which contain such code points), convert text to // UTF-32 using QString::toUcs4() and use QChar static // methods which take "uint ucs4". static const QVector whitelist = { u'\t', u'\r', u'\n' }; static const auto isUnsafe = [](const QChar &c) { return (c.category() == QChar::Category::Other_Control && !whitelist.contains(c.unicode())); }; // Returns control sequence string (e.g. "^C") for control character c static const auto charToSequence = [](const QChar &c) { if (c.unicode() <= 0x1F) { return QStringLiteral("^%1").arg(QChar(u'@' + c.unicode())); } else if (c.unicode() == 0x7F) { return QStringLiteral("^?"); } else if (c.unicode() >= 0x80 && c.unicode() <= 0x9F){ return QStringLiteral("^[%1").arg(QChar(u'@' + c.unicode() - 0x80)); } return QString(); }; const QMap characterDescriptions = { {0x0003, i18n("End Of Text/Interrupt: may exit the current process")}, {0x0004, i18n("End Of Transmission: may exit the current process")}, {0x0007, i18n("Bell: will try to emit an audible warning")}, {0x0008, i18n("Backspace")}, {0x0013, i18n("Device Control Three/XOFF: suspends output")}, {0x001a, i18n("Substitute/Suspend: may suspend current process")}, {0x001b, i18n("Escape: used for manipulating terminal state")}, {0x001c, i18n("File Separator/Quit: may abort the current process")}, }; QStringList unsafeCharacters; for (const QChar &c : text) { if (isUnsafe(c)) { const QString sequence = charToSequence(c); const QString description = characterDescriptions.value(c.unicode(), QString()); QString entry = QStringLiteral("U+%1").arg(c.unicode(), 4, 16, QLatin1Char('0')); if(!sequence.isEmpty()) { entry += QStringLiteral("\t%1").arg(sequence); } if(!description.isEmpty()) { entry += QStringLiteral("\t%1").arg(description); } unsafeCharacters.append(entry); } } unsafeCharacters.removeDuplicates(); if (!unsafeCharacters.isEmpty()) { int result = KMessageBox::warningYesNoCancelList(window(), i18n("The text you're trying to paste contains hidden control characters, " "do you want to filter them out?"), unsafeCharacters, i18nc("@title", "Confirm Paste"), KGuiItem(i18nc("@action:button", "Paste &without control characters"), QStringLiteral("filter-symbolic")), KGuiItem(i18nc("@action:button", "&Paste everything"), QStringLiteral("edit-paste")), KGuiItem(i18nc("@action:button", "&Cancel"), QStringLiteral("dialog-cancel")), QStringLiteral("ShowPasteUnprintableWarning") ); switch(result){ case KMessageBox::Cancel: return; case KMessageBox::Yes: { QString sanitized; for (const QChar &c : text) { if (!isUnsafe(c)) { sanitized.append(c); } } text = sanitized; } case KMessageBox::No: break; default: break; } } if (!text.isEmpty()) { text.replace(QLatin1Char('\n'), QLatin1Char('\r')); if (bracketedPasteMode()) { text.remove(QLatin1String("\033")); text.prepend(QLatin1String("\033[200~")); text.append(QLatin1String("\033[201~")); } // perform paste by simulating keypress events QKeyEvent e(QEvent::KeyPress, 0, Qt::NoModifier, text); emit keyPressedSignal(&e); } } void TerminalDisplay::setAutoCopySelectedText(bool enabled) { _autoCopySelectedText = enabled; } void TerminalDisplay::setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode) { _middleClickPasteMode = mode; } void TerminalDisplay::setCopyTextAsHTML(bool enabled) { _copyTextAsHTML = enabled; } void TerminalDisplay::copyToX11Selection() { if (_screenWindow.isNull()) { return; } const QString &text = _screenWindow->selectedText(currentDecodingOptions()); if (text.isEmpty()) { return; } auto mimeData = new QMimeData; mimeData->setText(text); if (_copyTextAsHTML) { mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml)); } if (QApplication::clipboard()->supportsSelection()) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection); } if (_autoCopySelectedText) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } } void TerminalDisplay::copyToClipboard() { if (_screenWindow.isNull()) { return; } const QString &text = _screenWindow->selectedText(currentDecodingOptions()); if (text.isEmpty()) { return; } auto mimeData = new QMimeData; mimeData->setText(text); if (_copyTextAsHTML) { mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml)); } QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void TerminalDisplay::pasteFromClipboard(bool appendEnter) { QString text; const QMimeData *mimeData = QApplication::clipboard()->mimeData(QClipboard::Clipboard); // When pasting urls of local files: // - remove the scheme part, "file://" // - paste the path(s) as a space-separated list of strings, which are quoted if needed if (!mimeData->hasUrls()) { // fast path if there are no urls text = mimeData->text(); } else { // handle local file urls const QList list = mimeData->urls(); for (const QUrl &url : list) { if (url.isLocalFile()) { text += KShell::quoteArg(url.toLocalFile()); text += QLatin1Char(' '); } else { // can users copy urls of both local and remote files at the same time? text = mimeData->text(); break; } } } doPaste(text, appendEnter); } void TerminalDisplay::pasteFromX11Selection(bool appendEnter) { if (QApplication::clipboard()->supportsSelection()) { QString text = QApplication::clipboard()->text(QClipboard::Selection); doPaste(text, appendEnter); } } /* ------------------------------------------------------------------------- */ /* */ /* Input Method */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::inputMethodEvent(QInputMethodEvent* event) { if (!event->commitString().isEmpty()) { QKeyEvent keyEvent(QEvent::KeyPress, 0, Qt::NoModifier, event->commitString()); emit keyPressedSignal(&keyEvent); } if (!_readOnly && isCursorOnDisplay()) { _inputMethodData.preeditString = event->preeditString(); update(preeditRect() | _inputMethodData.previousPreeditRect); } event->accept(); } QVariant TerminalDisplay::inputMethodQuery(Qt::InputMethodQuery query) const { const QPoint cursorPos = cursorPosition(); switch (query) { case Qt::ImMicroFocus: return imageToWidget(QRect(cursorPos.x(), cursorPos.y(), 1, 1)); case Qt::ImFont: return font(); case Qt::ImCursorPosition: // return the cursor position within the current line return cursorPos.x(); case Qt::ImSurroundingText: { // return the text from the current line QString lineText; QTextStream stream(&lineText); PlainTextDecoder decoder; decoder.begin(&stream); if (isCursorOnDisplay()) { decoder.decodeLine(&_image[loc(0, cursorPos.y())], _usedColumns, LINE_DEFAULT); } decoder.end(); return lineText; } case Qt::ImCurrentSelection: return QString(); default: break; } return QVariant(); } QRect TerminalDisplay::preeditRect() const { const int preeditLength = Character::stringWidth(_inputMethodData.preeditString); if (preeditLength == 0) { return {}; } const QRect stringRect(_contentRect.left() + _fontWidth * cursorPosition().x(), _contentRect.top() + _fontHeight * cursorPosition().y(), _fontWidth * preeditLength, _fontHeight); return stringRect.intersected(_contentRect); } void TerminalDisplay::drawInputMethodPreeditString(QPainter& painter , const QRect& rect) { if (_inputMethodData.preeditString.isEmpty() || !isCursorOnDisplay()) { return; } const QPoint cursorPos = cursorPosition(); bool invertColors = false; const QColor background = _colorTable[DEFAULT_BACK_COLOR]; const QColor foreground = _colorTable[DEFAULT_FORE_COLOR]; const Character* style = &_image[loc(cursorPos.x(), cursorPos.y())]; drawBackground(painter, rect, background, true); drawCursor(painter, rect, foreground, background, invertColors); drawCharacters(painter, rect, _inputMethodData.preeditString, style, invertColors); _inputMethodData.previousPreeditRect = rect; } /* ------------------------------------------------------------------------- */ /* */ /* Keyboard */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setFlowControlWarningEnabled(bool enable) { _flowControlWarningEnabled = enable; // if the dialog is currently visible and the flow control warning has // been disabled then hide the dialog if (!enable) { outputSuspended(false); } } void TerminalDisplay::outputSuspended(bool suspended) { //create the label when this function is first called if (_outputSuspendedMessageWidget == nullptr) { //This label includes a link to an English language website //describing the 'flow control' (Xon/Xoff) feature found in almost //all terminal emulators. //If there isn't a suitable article available in the target language the link //can simply be removed. _outputSuspendedMessageWidget = createMessageWidget(i18n("Output has been " "suspended" " by pressing Ctrl+S." " Press Ctrl+Q to resume.")); connect(_outputSuspendedMessageWidget, &KMessageWidget::linkActivated, this, [](const QString &url) { QDesktopServices::openUrl(QUrl(url)); }); _outputSuspendedMessageWidget->setMessageType(KMessageWidget::Warning); } suspended ? _outputSuspendedMessageWidget->animatedShow() : _outputSuspendedMessageWidget->animatedHide(); } void TerminalDisplay::dismissOutputSuspendedMessage() { outputSuspended(false); } KMessageWidget* TerminalDisplay::createMessageWidget(const QString &text) { auto widget = new KMessageWidget(text); widget->setWordWrap(true); widget->setFocusProxy(this); widget->setCursor(Qt::ArrowCursor); _verticalLayout->insertWidget(1, widget); return widget; } void TerminalDisplay::updateReadOnlyState(bool readonly) { if (_readOnly == readonly) { return; } if (readonly) { // Lazy create the readonly messagewidget if (_readOnlyMessageWidget == nullptr) { _readOnlyMessageWidget = createMessageWidget(i18n("This terminal is read-only.")); _readOnlyMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("object-locked"))); } } if (_readOnlyMessageWidget != nullptr) { readonly ? _readOnlyMessageWidget->animatedShow() : _readOnlyMessageWidget->animatedHide(); } _readOnly = readonly; } void TerminalDisplay::scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode, int amount) { _screenWindow->scrollBy(mode, amount, _scrollFullPage); _screenWindow->setTrackOutput(_screenWindow->atEndOfOutput()); updateLineProperties(); updateImage(); viewScrolledByUser(); } void TerminalDisplay::keyPressEvent(QKeyEvent* event) { if ((_urlHintsModifiers != 0u) && event->modifiers() == _urlHintsModifiers) { int nHotSpots = _filterChain->hotSpots().count(); int hintSelected = event->key() - 0x31; if (hintSelected >= 0 && hintSelected < 10 && hintSelected < nHotSpots) { if (_reverseUrlHints) { hintSelected = nHotSpots - hintSelected - 1; } _filterChain->hotSpots().at(hintSelected)->activate(); _showUrlHint = false; update(); return; } if (!_showUrlHint) { processFilters(); _showUrlHint = true; update(); } } _screenWindow->screen()->setCurrentTerminalDisplay(this); if (!_readOnly) { _actSel = 0; // Key stroke implies a screen update, so TerminalDisplay won't // know where the current selection is. if (_allowBlinkingCursor) { _blinkCursorTimer->start(); if (_cursorBlinking) { // if cursor is blinking(hidden), blink it again to show it blinkCursorEvent(); } Q_ASSERT(!_cursorBlinking); } } if (_searchBar->isVisible() && event->key() == Qt::Key_Escape) { _sessionController->searchClosed(); } emit keyPressedSignal(event); #ifndef QT_NO_ACCESSIBILITY if (!_readOnly) { QAccessibleTextCursorEvent textCursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX()); QAccessible::updateAccessibility(&textCursorEvent); } #endif event->accept(); } void TerminalDisplay::keyReleaseEvent(QKeyEvent *event) { if (_showUrlHint) { _showUrlHint = false; update(); } if (_readOnly) { event->accept(); return; } QWidget::keyReleaseEvent(event); } bool TerminalDisplay::handleShortcutOverrideEvent(QKeyEvent* keyEvent) { const int modifiers = keyEvent->modifiers(); // When a possible shortcut combination is pressed, // emit the overrideShortcutCheck() signal to allow the host // to decide whether the terminal should override it or not. if (modifiers != Qt::NoModifier) { int modifierCount = 0; unsigned int currentModifier = Qt::ShiftModifier; while (currentModifier <= Qt::KeypadModifier) { if ((modifiers & currentModifier) != 0u) { modifierCount++; } currentModifier <<= 1; } if (modifierCount < 2) { bool override = false; emit overrideShortcutCheck(keyEvent, override); if (override) { keyEvent->accept(); return true; } } } // Override any of the following shortcuts because // they are needed by the terminal int keyCode = keyEvent->key() | modifiers; switch (keyCode) { // list is taken from the QLineEdit::event() code case Qt::Key_Tab: case Qt::Key_Delete: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Backspace: case Qt::Key_Left: case Qt::Key_Right: case Qt::Key_Slash: case Qt::Key_Period: case Qt::Key_Space: keyEvent->accept(); return true; } return false; } bool TerminalDisplay::event(QEvent* event) { bool eventHandled = false; switch (event->type()) { case QEvent::ShortcutOverride: eventHandled = handleShortcutOverrideEvent(static_cast(event)); break; case QEvent::PaletteChange: case QEvent::ApplicationPaletteChange: onColorsChanged(); break; case QEvent::FocusOut: case QEvent::FocusIn: if(_screenWindow != nullptr) { // force a redraw on focusIn, fixes the // black screen bug when the view is focused // but doesn't redraws. _screenWindow->notifyOutputChanged(); } update(); break; default: break; } return eventHandled ? true : QWidget::event(event); } void TerminalDisplay::contextMenuEvent(QContextMenuEvent* event) { // the logic for the mouse case is within MousePressEvent() if (event->reason() != QContextMenuEvent::Mouse) { emit configureRequest(mapFromGlobal(QCursor::pos())); } } /* --------------------------------------------------------------------- */ /* */ /* Bell */ /* */ /* --------------------------------------------------------------------- */ void TerminalDisplay::setBellMode(int mode) { _bellMode = mode; } int TerminalDisplay::bellMode() const { return _bellMode; } void TerminalDisplay::bell(const QString& message) { if (_bellMasked) { return; } switch (_bellMode) { case Enum::SystemBeepBell: KNotification::beep(); break; case Enum::NotifyBell: // STABLE API: // Please note that these event names, "BellVisible" and "BellInvisible", // should not change and should be kept stable, because other applications // that use this code via KPart rely on these names for notifications. KNotification::event(hasFocus() ? QStringLiteral("BellVisible") : QStringLiteral("BellInvisible"), message, QPixmap(), this); break; case Enum::VisualBell: visualBell(); break; default: break; } // limit the rate at which bells can occur. // ...mainly for sound effects where rapid bells in sequence // produce a horrible noise. _bellMasked = true; QTimer::singleShot(500, this, [this]() { _bellMasked = false; }); } void TerminalDisplay::visualBell() { swapFGBGColors(); QTimer::singleShot(200, this, &Konsole::TerminalDisplay::swapFGBGColors); } void TerminalDisplay::swapFGBGColors() { // swap the default foreground & background color ColorEntry color = _colorTable[DEFAULT_BACK_COLOR]; _colorTable[DEFAULT_BACK_COLOR] = _colorTable[DEFAULT_FORE_COLOR]; _colorTable[DEFAULT_FORE_COLOR] = color; onColorsChanged(); } /* --------------------------------------------------------------------- */ /* */ /* Drag & Drop */ /* */ /* --------------------------------------------------------------------- */ void TerminalDisplay::dragEnterEvent(QDragEnterEvent* event) { // text/plain alone is enough for KDE-apps // text/uri-list is for supporting some non-KDE apps, such as thunar // and pcmanfm // That also applies in dropEvent() const auto mimeData = event->mimeData(); if ((!_readOnly) && (mimeData != nullptr) && (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list")))) { event->acceptProposedAction(); } } namespace { QString extractDroppedText(const QList& urls) { QString dropText; for (int i = 0 ; i < urls.count() ; i++) { KIO::StatJob* job = KIO::mostLocalUrl(urls[i], KIO::HideProgressInfo); if (!job->exec()) { continue; } const QUrl url = job->mostLocalUrl(); // in future it may be useful to be able to insert file names with drag-and-drop // without quoting them (this only affects paths with spaces in) dropText += KShell::quoteArg(url.isLocalFile() ? url.path() : url.url()); // Each filename(including the last) should be followed by one space. dropText += QLatin1Char(' '); } return dropText; } void setupCdToUrlAction(const QString& dropText, const QUrl& url, QList& additionalActions, TerminalDisplay *display) { KIO::StatJob* job = KIO::mostLocalUrl(url, KIO::HideProgressInfo); if (!job->exec()) { return; } const QUrl localUrl = job->mostLocalUrl(); if (!localUrl.isLocalFile()) { return; } const QFileInfo fileInfo(localUrl.path()); if (!fileInfo.isDir()) { return; } QAction* cdAction = new QAction(i18n("Change &Directory To"), display); const QByteArray triggerText = QString(QLatin1String(" cd ") + dropText + QLatin1Char('\n')).toLocal8Bit(); display->connect(cdAction, &QAction::triggered, display, [display, triggerText]{ emit display->sendStringToEmu(triggerText);} ); additionalActions.append(cdAction); } } void TerminalDisplay::dropEvent(QDropEvent* event) { if (_readOnly) { event->accept(); return; } const auto mimeData = event->mimeData(); if (mimeData == nullptr) { return; } auto urls = mimeData->urls(); QString dropText; if (!urls.isEmpty()) { dropText = extractDroppedText(urls); // If our target is local we will open a popup - otherwise the fallback kicks // in and the URLs will simply be pasted as text. if (!_dropUrlsAsText && (_sessionController != nullptr) && _sessionController->url().isLocalFile()) { // A standard popup with Copy, Move and Link as options - // plus an additional Paste option. QAction* pasteAction = new QAction(i18n("&Paste Location"), this); connect(pasteAction, &QAction::triggered, this, [this, dropText]{ emit sendStringToEmu(dropText.toLocal8Bit());} ); QList additionalActions; additionalActions.append(pasteAction); if (urls.count() == 1) { setupCdToUrlAction(dropText, urls.at(0), additionalActions, this); } QUrl target = QUrl::fromLocalFile(_sessionController->currentDir()); KIO::DropJob* job = KIO::drop(event, target); KJobWidgets::setWindow(job, this); job->setApplicationActions(additionalActions); return; } } else { dropText = mimeData->text(); } if (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list"))) { emit sendStringToEmu(dropText.toLocal8Bit()); } } void TerminalDisplay::doDrag() { const QMimeData *clipboardMimeData = QApplication::clipboard()->mimeData(QClipboard::Selection); if (clipboardMimeData == nullptr) { return; } auto mimeData = new QMimeData(); _dragInfo.state = diDragging; _dragInfo.dragObject = new QDrag(this); mimeData->setText(clipboardMimeData->text()); mimeData->setHtml(clipboardMimeData->html()); _dragInfo.dragObject->setMimeData(mimeData); _dragInfo.dragObject->exec(Qt::CopyAction); } void TerminalDisplay::setSessionController(SessionController* controller) { _sessionController = controller; _headerBar->finishHeaderSetup(controller); } SessionController* TerminalDisplay::sessionController() { return _sessionController; } IncrementalSearchBar *TerminalDisplay::searchBar() const { return _searchBar; } AutoScrollHandler::AutoScrollHandler(QWidget* parent) : QObject(parent) , _timerId(0) { parent->installEventFilter(this); } void AutoScrollHandler::timerEvent(QTimerEvent* event) { if (event->timerId() != _timerId) { return; } QMouseEvent mouseEvent(QEvent::MouseMove, widget()->mapFromGlobal(QCursor::pos()), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(widget(), &mouseEvent); } bool AutoScrollHandler::eventFilter(QObject* watched, QEvent* event) { Q_ASSERT(watched == parent()); Q_UNUSED(watched); switch (event->type()) { case QEvent::MouseMove: { auto* mouseEvent = static_cast(event); bool mouseInWidget = widget()->rect().contains(mouseEvent->pos()); if (mouseInWidget) { if (_timerId != 0) { killTimer(_timerId); } _timerId = 0; } else { if ((_timerId == 0) && ((mouseEvent->buttons() & Qt::LeftButton) != 0u)) { _timerId = startTimer(100); } } break; } case QEvent::MouseButtonRelease: { auto* mouseEvent = static_cast(event); if ((_timerId != 0) && ((mouseEvent->buttons() & ~Qt::LeftButton) != 0u)) { killTimer(_timerId); _timerId = 0; } break; } default: break; }; return false; } void TerminalDisplay::applyProfile(const Profile::Ptr &profile) { // load color scheme ColorEntry table[TABLE_COLORS]; _colorScheme = ViewManager::colorSchemeForProfile(profile); _colorScheme->getColorTable(table, randomSeed()); setColorTable(table); setOpacity(_colorScheme->opacity()); setWallpaper(_colorScheme->wallpaper()); // load font _antialiasText = profile->antiAliasFonts(); _boldIntense = profile->boldIntense(); _useFontLineCharacters = profile->useFontLineCharacters(); setVTFont(profile->font()); // set scroll-bar position setScrollBarPosition(Enum::ScrollBarPositionEnum(profile->property(Profile::ScrollBarPosition))); setScrollFullPage(profile->property(Profile::ScrollFullPage)); // show hint about terminal size after resizing _showTerminalSizeHint = profile->showTerminalSizeHint(); _dimWhenInactive = profile->dimWhenInactive(); // terminal features setBlinkingCursorEnabled(profile->blinkingCursorEnabled()); setBlinkingTextEnabled(profile->blinkingTextEnabled()); _tripleClickMode = Enum::TripleClickModeEnum(profile->property(Profile::TripleClickMode)); setAutoCopySelectedText(profile->autoCopySelectedText()); _ctrlRequiredForDrag = profile->property(Profile::CtrlRequiredForDrag); _dropUrlsAsText = profile->property(Profile::DropUrlsAsText); _bidiEnabled = profile->bidiRenderingEnabled(); setLineSpacing(uint(profile->lineSpacing())); _trimLeadingSpaces = profile->property(Profile::TrimLeadingSpacesInSelectedText); _trimTrailingSpaces = profile->property(Profile::TrimTrailingSpacesInSelectedText); _openLinksByDirectClick = profile->property(Profile::OpenLinksByDirectClickEnabled); _urlHintsModifiers = Qt::KeyboardModifiers(profile->property(Profile::UrlHintsModifiers)); _reverseUrlHints = profile->property(Profile::ReverseUrlHints); setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum(profile->property(Profile::MiddleClickPasteMode))); setCopyTextAsHTML(profile->property(Profile::CopyTextAsHTML)); // margin/center setMargin(profile->property(Profile::TerminalMargin)); setCenterContents(profile->property(Profile::TerminalCenter)); // cursor shape setKeyboardCursorShape(Enum::CursorShapeEnum(profile->property(Profile::CursorShape))); // cursor color // an invalid QColor is used to inform the view widget to // draw the cursor using the default color( matching the text) setKeyboardCursorColor(profile->useCustomCursorColor() ? profile->customCursorColor() : QColor()); // word characters setWordCharacters(profile->wordCharacters()); // bell mode setBellMode(profile->property(Profile::BellMode)); // mouse wheel zoom _mouseWheelZoom = profile->mouseWheelZoomEnabled(); setAlternateScrolling(profile->property(Profile::AlternateScrolling)); } + +// +// CompositeWidgetFocusWatcher +// + +CompositeWidgetFocusWatcher::CompositeWidgetFocusWatcher(QWidget *compositeWidget, QObject *parent) + : QObject(parent) + , _compositeWidget(compositeWidget) +{ + registerWidgetAndChildren(compositeWidget); +} + +bool CompositeWidgetFocusWatcher::eventFilter(QObject *watched, QEvent *event) +{ + Q_UNUSED(watched); + + auto *focusEvent = static_cast(event); + switch(event->type()) { + case QEvent::FocusIn: + emit compositeFocusChanged(true); + break; + case QEvent::FocusOut: + if(focusEvent->reason() != Qt::PopupFocusReason) { + emit compositeFocusChanged(false); + } + break; + default: + break; + } + return false; +} + +void CompositeWidgetFocusWatcher::registerWidgetAndChildren(QWidget *widget) +{ + Q_ASSERT(widget != nullptr); + + if (widget->focusPolicy() != Qt::NoFocus) { + widget->installEventFilter(this); + } + for (auto *child: widget->children()) { + auto *childWidget = qobject_cast(child); + if (childWidget) { + registerWidgetAndChildren(childWidget); + } + } +} diff --git a/src/TerminalDisplay.h b/src/TerminalDisplay.h index 7c78dafb..eec94b96 100644 --- a/src/TerminalDisplay.h +++ b/src/TerminalDisplay.h @@ -1,889 +1,918 @@ /* Copyright 2007-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle 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 TERMINALDISPLAY_H #define TERMINALDISPLAY_H // Qt #include #include #include // Konsole #include "Character.h" #include "konsoleprivate_export.h" #include "ScreenWindow.h" #include "ColorScheme.h" #include "Enumeration.h" #include "ScrollState.h" #include "Profile.h" #include "TerminalHeaderBar.h" class QDrag; class QDragEnterEvent; class QDropEvent; class QLabel; class QTimer; class QEvent; class QVBoxLayout; class QKeyEvent; class QScrollBar; class QShowEvent; class QHideEvent; class QTimerEvent; class KMessageWidget; namespace Konsole { class FilterChain; class TerminalImageFilterChain; class SessionController; class IncrementalSearchBar; /** * A widget which displays output from a terminal emulation and sends input keypresses and mouse activity * to the terminal. * * When the terminal emulation receives new output from the program running in the terminal, * it will update the display by calling updateImage(). * * TODO More documentation */ class KONSOLEPRIVATE_EXPORT TerminalDisplay : public QWidget { Q_OBJECT public: /** Constructs a new terminal display widget with the specified parent. */ explicit TerminalDisplay(QWidget *parent = nullptr); ~TerminalDisplay() Q_DECL_OVERRIDE; void showDragTarget(const QPoint& pos); void hideDragTarget(); void applyProfile(const Profile::Ptr& profile); /** Returns the terminal color palette used by the display. */ const ColorEntry *colorTable() const; /** Sets the terminal color palette used by the display. */ void setColorTable(const ColorEntry table[]); /** * Sets the seed used to generate random colors for the display * (in color schemes that support them). */ void setRandomSeed(uint randomSeed); /** * Returns the seed used to generate random colors for the display * (in color schemes that support them). */ uint randomSeed() const; /** Sets the opacity of the terminal display. */ void setOpacity(qreal opacity); /** Sets the background picture */ void setWallpaper(const ColorSchemeWallpaper::Ptr &p); /** * Specifies whether the terminal display has a vertical scroll bar, and if so whether it * is shown on the left or right side of the display. */ void setScrollBarPosition(Enum::ScrollBarPositionEnum position); /** * Sets the current position and range of the display's scroll bar. * * @param cursor The position of the scroll bar's thumb. * @param slines The maximum value of the scroll bar. */ void setScroll(int cursor, int slines); void setScrollFullPage(bool fullPage); bool scrollFullPage() const; /** * Returns the display's filter chain. When the image for the display is updated, * the text is passed through each filter in the chain. Each filter can define * hotspots which correspond to certain strings (such as URLs or particular words). * Depending on the type of the hotspots created by the filter ( returned by Filter::Hotspot::type() ) * the view will draw visual cues such as underlines on mouse-over for links or translucent * rectangles for markers. * * To add a new filter to the view, call: * viewWidget->filterChain()->addFilter( filterObject ); */ FilterChain *filterChain() const; /** * Updates the filters in the display's filter chain. This will cause * the hotspots to be updated to match the current image. * * WARNING: This function can be expensive depending on the * image size and number of filters in the filterChain() * * TODO - This API does not really allow efficient usage. Revise it so * that the processing can be done in a better way. * * eg: * - Area of interest may be known ( eg. mouse cursor hovering * over an area ) */ void processFilters(); /** * Returns a list of menu actions created by the filters for the content * at the given @p position. */ QList filterActions(const QPoint &position); /** Specifies whether or not the cursor can blink. */ void setBlinkingCursorEnabled(bool blink); /** Specifies whether or not text can blink. */ void setBlinkingTextEnabled(bool blink); void setLineSpacing(uint); uint lineSpacing() const; void setSessionController(SessionController *controller); SessionController *sessionController(); /** * Sets the shape of the keyboard cursor. This is the cursor drawn * at the position in the terminal where keyboard input will appear. * * In addition the terminal display widget also has a cursor for * the mouse pointer, which can be set using the QWidget::setCursor() * method. * * Defaults to BlockCursor */ void setKeyboardCursorShape(Enum::CursorShapeEnum shape); /** * Returns the shape of the keyboard cursor. See setKeyboardCursorShape() */ Enum::CursorShapeEnum keyboardCursorShape() const; /** * Sets the Cursor Style (DECSCUSR) via escape sequences * @p shape cursor shape * @p isBlinking if true, the cursor will be set to blink */ void setCursorStyle(Enum::CursorShapeEnum shape, bool isBlinking); /** * Resets the cursor style to the current profile cursor shape and * blinking settings */ void resetCursorStyle(); /** * Sets the color used to draw the keyboard cursor. * * The keyboard cursor defaults to using the foreground color of the character * underneath it. * * @param color By default, the widget uses the color of the * character under the cursor to draw the cursor, and inverts the * color of that character to make sure it is still readable. If @p * color is a valid QColor, the widget uses that color to draw the * cursor. If @p color is not an valid QColor, the widget falls back * to the default behavior. */ void setKeyboardCursorColor(const QColor &color); /** * Returns the color of the keyboard cursor, or an invalid color if the keyboard * cursor color is set to change according to the foreground color of the character * underneath it. */ QColor keyboardCursorColor() const; /** * Returns the number of lines of text which can be displayed in the widget. * * This will depend upon the height of the widget and the current font. * See fontHeight() */ int lines() const { return _lines; } /** * Returns the number of characters of text which can be displayed on * each line in the widget. * * This will depend upon the width of the widget and the current font. * See fontWidth() */ int columns() const { return _columns; } /** * Returns the height of the characters in the font used to draw the text in the display. */ int fontHeight() const { return _fontHeight; } /** * Returns the width of the characters in the display. * This assumes the use of a fixed-width font. */ int fontWidth() const { return _fontWidth; } void setSize(int columns, int lines); // reimplemented QSize sizeHint() const Q_DECL_OVERRIDE; /** * Sets which characters, in addition to letters and numbers, * are regarded as being part of a word for the purposes * of selecting words in the display by double clicking on them. * * The word boundaries occur at the first and last characters which * are either a letter, number, or a character in @p wc * * @param wc An array of characters which are to be considered parts * of a word ( in addition to letters and numbers ). */ void setWordCharacters(const QString &wc); /** * Sets the type of effect used to alert the user when a 'bell' occurs in the * terminal session. * * The terminal session can trigger the bell effect by calling bell() with * the alert message. */ void setBellMode(int mode); /** * Returns the type of effect used to alert the user when a 'bell' occurs in * the terminal session. * * See setBellMode() */ int bellMode() const; /** Play a visual bell for prompt or warning. */ void visualBell(); /** Returns the font used to draw characters in the display */ QFont getVTFont() { return font(); } TerminalHeaderBar *headerBar() const { return _headerBar; } /** * Sets the font used to draw the display. Has no effect if @p font * is larger than the size of the display itself. */ void setVTFont(const QFont &f); /** Increases the font size */ void increaseFontSize(); /** Decreases the font size */ void decreaseFontSize(); /** Reset the font size */ void resetFontSize(); /** * Sets the terminal screen section which is displayed in this widget. * When updateImage() is called, the display fetches the latest character image from the * the associated terminal screen window. * * In terms of the model-view paradigm, the ScreenWindow is the model which is rendered * by the TerminalDisplay. */ void setScreenWindow(ScreenWindow *window); /** Returns the terminal screen section which is displayed in this widget. See setScreenWindow() */ ScreenWindow *screenWindow() const; // Select the current line. void selectCurrentLine(); /** * Selects everything in the terminal */ void selectAll(); void printContent(QPainter &painter, bool friendly); /** * Gets the background of the display * @see setBackgroundColor(), setColorTable(), setForegroundColor() */ QColor getBackgroundColor() const; bool bracketedPasteMode() const; /** * Returns true if the flow control warning box is enabled. * See outputSuspended() and setFlowControlWarningEnabled() */ bool flowControlWarningEnabled() const { return _flowControlWarningEnabled; } /** See setUsesMouseTracking() */ bool usesMouseTracking() const; /** See setAlternateScrolling() */ bool alternateScrolling() const; + bool hasCompositeFocus() const + { + return _hasCompositeFocus; + } + public Q_SLOTS: /** * Scrolls current ScreenWindow * * it's needed for proper handling scroll commands in the Vt102Emulation class */ void scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode, int amount); /** * Causes the terminal display to fetch the latest character image from the associated * terminal screen ( see setScreenWindow() ) and redraw the display. */ void updateImage(); /** * Causes the terminal display to fetch the latest line status flags from the * associated terminal screen ( see setScreenWindow() ). */ void updateLineProperties(); void setAutoCopySelectedText(bool enabled); void setCopyTextAsHTML(bool enabled); void setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode); /** Copies the selected text to the X11 Selection. */ void copyToX11Selection(); /** Copies the selected text to the system clipboard. */ void copyToClipboard(); /** * Pastes the content of the clipboard into the display. * * URLs of local files are treated specially: * - The scheme part, "file://", is removed from each URL * - The URLs are pasted as a space-separated list of file paths */ void pasteFromClipboard(bool appendEnter = false); /** * Pastes the content of the X11 selection into the * display. */ void pasteFromX11Selection(bool appendEnter = false); /** * Changes whether the flow control warning box should be shown when the flow control * stop key (Ctrl+S) are pressed. */ void setFlowControlWarningEnabled(bool enable); /** * Causes the widget to display or hide a message informing the user that terminal * output has been suspended (by using the flow control key combination Ctrl+S) * * @param suspended True if terminal output has been suspended and the warning message should * be shown or false to indicate that terminal output has been resumed and that * the warning message should disappear. */ void outputSuspended(bool suspended); /** * Sets whether the program currently running in the terminal is interested * in Mouse Tracking events. * * When set to true, Konsole will send Mouse Tracking events. * * The user interaction needed to create text selections will change * also, and the user will be required to hold down the Shift key to * create a selection or perform other mouse activities inside the view * area, since the program running in the terminal is being allowed * to handle normal mouse events itself. * * @param on Set to true if the program running in the terminal is * interested in Mouse Tracking events or false otherwise. */ void setUsesMouseTracking(bool on); /** * Sets the AlternateScrolling profile property which controls whether * to emulate up/down key presses for mouse scroll wheel events. * For more details, check the documentation of that property in the * Profile header. * Enabled by default. */ void setAlternateScrolling(bool enable); void setBracketedPasteMode(bool on); /** * Shows a notification that a bell event has occurred in the terminal. * TODO: More documentation here */ void bell(const QString &message); /** * Sets the background of the display to the specified color. * @see setColorTable(), getBackgroundColor(), setForegroundColor() */ void setBackgroundColor(const QColor &color); /** * Sets the text of the display to the specified color. * @see setColorTable(), setBackgroundColor(), getBackgroundColor() */ void setForegroundColor(const QColor &color); /** * Sets the display's contents margins. */ void setMargin(int margin); /** * Sets whether the contents are centered between the margins. */ void setCenterContents(bool enable); /** * Return the current color scheme */ ColorScheme const *colorScheme() const { return _colorScheme; } Enum::ScrollBarPositionEnum scrollBarPosition() const { return _scrollbarLocation; } Qt::Edge droppedEdge() const { return _overlayEdge; } // Used to show/hide the message widget void updateReadOnlyState(bool readonly); IncrementalSearchBar *searchBar() const; Q_SIGNALS: void requestToggleExpansion(); /** * Emitted when the user presses a key whilst the terminal widget has focus. */ void keyPressedSignal(QKeyEvent *event); /** * A mouse event occurred. * @param button The mouse button (0 for left button, 1 for middle button, 2 for right button, 3 for release) * @param column The character column where the event occurred * @param line The character row where the event occurred * @param eventType The type of event. 0 for a mouse press / release or 1 for mouse motion */ void mouseSignal(int button, int column, int line, int eventType); void changedFontMetricSignal(int height, int width); void changedContentSizeSignal(int height, int width); /** * Emitted when the user right clicks on the display, or right-clicks * with the Shift key held down if usesMouseTracking() is true. * * This can be used to display a context menu. */ void configureRequest(const QPoint &position); /** * When a shortcut which is also a valid terminal key sequence is pressed while * the terminal widget has focus, this signal is emitted to allow the host to decide * whether the shortcut should be overridden. * When the shortcut is overridden, the key sequence will be sent to the terminal emulation instead * and the action associated with the shortcut will not be triggered. * * @p override is set to false by default and the shortcut will be triggered as normal. */ void overrideShortcutCheck(QKeyEvent *keyEvent, bool &override); void sendStringToEmu(const QByteArray &local8BitString); - void focusLost(); - void focusGained(); + void compositeFocusChanged(bool focused); protected: // events bool event(QEvent *event) Q_DECL_OVERRIDE; void paintEvent(QPaintEvent *pe) Q_DECL_OVERRIDE; void showEvent(QShowEvent *event) Q_DECL_OVERRIDE; void hideEvent(QHideEvent *event) Q_DECL_OVERRIDE; void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE; void contextMenuEvent(QContextMenuEvent *event) Q_DECL_OVERRIDE; void focusInEvent(QFocusEvent *event) Q_DECL_OVERRIDE; void focusOutEvent(QFocusEvent *event) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; void keyReleaseEvent(QKeyEvent *event) Q_DECL_OVERRIDE; void leaveEvent(QEvent *event) Q_DECL_OVERRIDE; void mouseDoubleClickEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mousePressEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mouseReleaseEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mouseMoveEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void wheelEvent(QWheelEvent *ev) Q_DECL_OVERRIDE; bool focusNextPrevChild(bool next) Q_DECL_OVERRIDE; void fontChange(const QFont &); void extendSelection(const QPoint &position); // drag and drop void dragEnterEvent(QDragEnterEvent *event) Q_DECL_OVERRIDE; void dropEvent(QDropEvent *event) Q_DECL_OVERRIDE; void doDrag(); enum DragState { diNone, diPending, diDragging }; struct DragInfo { DragState state; QPoint start; QDrag *dragObject; } _dragInfo; // classifies the 'ch' into one of three categories // and returns a character to indicate which category it is in // // - A space (returns ' ') // - Part of a word (returns 'a') // - Other characters (returns the input character) QChar charClass(const Character &ch) const; void clearImage(); void mouseTripleClickEvent(QMouseEvent *ev); void selectLine(QPoint pos, bool entireLine); // reimplemented void inputMethodEvent(QInputMethodEvent *event) Q_DECL_OVERRIDE; QVariant inputMethodQuery(Qt::InputMethodQuery query) const Q_DECL_OVERRIDE; void onColorsChanged(); protected Q_SLOTS: void scrollBarPositionChanged(int value); void blinkTextEvent(); void blinkCursorEvent(); private Q_SLOTS: void swapFGBGColors(); void viewScrolledByUser(); void dismissOutputSuspendedMessage(); private: Q_DISABLE_COPY(TerminalDisplay) // -- Drawing helpers -- // divides the part of the display specified by 'rect' into // fragments according to their colors and styles and calls // drawTextFragment() or drawPrinterFriendlyTextFragment() // to draw the fragments void drawContents(QPainter &painter, const QRect &rect); // draw a transparent rectangle over the line of the current match void drawCurrentResultRect(QPainter &painter); // draws a section of text, all the text in this section // has a common color and style void drawTextFragment(QPainter &painter, const QRect &rect, const QString &text, const Character *style); void drawPrinterFriendlyTextFragment(QPainter &painter, const QRect &rect, const QString &text, const Character *style); // draws the background for a text fragment // if useOpacitySetting is true then the color's alpha value will be set to // the display's transparency (set with setOpacity()), otherwise the background // will be drawn fully opaque void drawBackground(QPainter &painter, const QRect &rect, const QColor &backgroundColor, bool useOpacitySetting); // draws the cursor character void drawCursor(QPainter &painter, const QRect &rect, const QColor &foregroundColor, const QColor &backgroundColor, bool &invertCharacterColor); // draws the characters or line graphics in a text fragment void drawCharacters(QPainter &painter, const QRect &rect, const QString &text, const Character *style, bool invertCharacterColor); // draws a string of line graphics void drawLineCharString(QPainter &painter, int x, int y, const QString &str, const Character *attributes); // draws the preedit string for input methods void drawInputMethodPreeditString(QPainter &painter, const QRect &rect); // -- // maps an area in the character image to an area on the widget QRect imageToWidget(const QRect &imageArea) const; QRect widgetToImage(const QRect &widgetArea) const; // maps a point on the widget to the position ( ie. line and column ) // of the character at that point. When the edge is true, it maps to // a character which left edge is closest to the point. void getCharacterPosition(const QPoint &widgetPoint, int &line, int &column, bool edge) const; // the area where the preedit string for input methods will be draw QRect preeditRect() const; // shows a notification window in the middle of the widget indicating the terminal's // current size in columns and lines void showResizeNotification(); // scrolls the image by a number of lines. // 'lines' may be positive ( to scroll the image down ) // or negative ( to scroll the image up ) // 'region' is the part of the image to scroll - currently only // the top, bottom and height of 'region' are taken into account, // the left and right are ignored. void scrollImage(int lines, const QRect &screenWindowRegion); void calcGeometry(); void propagateSize(); void updateImageSize(); void makeImage(); void paintFilters(QPainter &painter); // returns a region covering all of the areas of the widget which contain // a hotspot QRegion hotSpotRegion() const; // returns the position of the cursor in columns and lines QPoint cursorPosition() const; // returns true if the cursor's position is on display. bool isCursorOnDisplay() const; // redraws the cursor void updateCursor(); bool handleShortcutOverrideEvent(QKeyEvent *keyEvent); void doPaste(QString text, bool appendReturn); void processMidButtonClick(QMouseEvent *ev); QPoint findLineStart(const QPoint &pnt); QPoint findLineEnd(const QPoint &pnt); QPoint findWordStart(const QPoint &pnt); QPoint findWordEnd(const QPoint &pnt); // Uses the current settings for trimming whitespace and preserving linebreaks to create a proper flag value for Screen Screen::DecodingOptions currentDecodingOptions(); // Boilerplate setup for MessageWidget KMessageWidget* createMessageWidget(const QString &text); int loc(int x, int y) const; // the window onto the terminal screen which this display // is currently showing. QPointer _screenWindow; bool _bellMasked; QVBoxLayout *_verticalLayout; bool _fixedFont; // has fixed pitch int _fontHeight; // height int _fontWidth; // width int _fontAscent; // ascend bool _boldIntense; // Whether intense colors should be rendered with bold font int _lines; // the number of lines that can be displayed in the widget int _columns; // the number of columns that can be displayed in the widget int _usedLines; // the number of lines that are actually being used, this will be less // than 'lines' if the character image provided with setImage() is smaller // than the maximum image size which can be displayed int _usedColumns; // the number of columns that are actually being used, this will be less // than 'columns' if the character image provided with setImage() is smaller // than the maximum image size which can be displayed QRect _contentRect; Character *_image; // [lines][columns] // only the area [usedLines][usedColumns] in the image contains valid data int _imageSize; QVector _lineProperties; ColorEntry _colorTable[TABLE_COLORS]; uint _randomSeed; bool _resizing; bool _showTerminalSizeHint; bool _bidiEnabled; bool _usesMouseTracking; bool _alternateScrolling; bool _bracketedPasteMode; QPoint _iPntSel; // initial selection point QPoint _pntSel; // current selection point QPoint _tripleSelBegin; // help avoid flicker int _actSel; // selection state bool _wordSelectionMode; bool _lineSelectionMode; bool _preserveLineBreaks; bool _columnSelectionMode; bool _autoCopySelectedText; bool _copyTextAsHTML; Enum::MiddleClickPasteModeEnum _middleClickPasteMode; QScrollBar *_scrollBar; Enum::ScrollBarPositionEnum _scrollbarLocation; bool _scrollFullPage; QString _wordCharacters; int _bellMode; bool _allowBlinkingText; // allow text to blink bool _allowBlinkingCursor; // allow cursor to blink bool _textBlinking; // text is blinking, hide it when drawing bool _cursorBlinking; // cursor is blinking, hide it when drawing bool _hasTextBlinker; // has characters to blink QTimer *_blinkTextTimer; QTimer *_blinkCursorTimer; Qt::KeyboardModifiers _urlHintsModifiers; bool _showUrlHint; bool _reverseUrlHints; bool _openLinksByDirectClick; // Open URL and hosts by single mouse click bool _ctrlRequiredForDrag; // require Ctrl key for drag selected text bool _dropUrlsAsText; // always paste URLs as text without showing copy/move menu Enum::TripleClickModeEnum _tripleClickMode; bool _possibleTripleClick; // is set in mouseDoubleClickEvent and deleted // after QApplication::doubleClickInterval() delay QLabel *_resizeWidget; QTimer *_resizeTimer; bool _flowControlWarningEnabled; //widgets related to the warning message that appears when the user presses Ctrl+S to suspend //terminal output - informing them what has happened and how to resume output KMessageWidget *_outputSuspendedMessageWidget; uint _lineSpacing; QSize _size; QRgb _blendColor; ColorScheme const* _colorScheme; ColorSchemeWallpaper::Ptr _wallpaper; // list of filters currently applied to the display. used for links and // search highlight TerminalImageFilterChain *_filterChain; QRegion _mouseOverHotspotArea; bool _filterUpdateRequired; Enum::CursorShapeEnum _cursorShape; // cursor color. If it is invalid (by default) then the foreground // color of the character under the cursor is used QColor _cursorColor; struct InputMethodData { QString preeditString; QRect previousPreeditRect; }; InputMethodData _inputMethodData; bool _antialiasText; // do we anti-alias or not bool _useFontLineCharacters; bool _printerFriendly; // are we currently painting to a printer in black/white mode //the delay in milliseconds between redrawing blinking text static const int TEXT_BLINK_DELAY = 500; //the duration of the size hint in milliseconds static const int SIZE_HINT_DURATION = 1000; SessionController *_sessionController; bool _trimLeadingSpaces; // trim leading spaces in selected text bool _trimTrailingSpaces; // trim trailing spaces in selected text bool _mouseWheelZoom; // enable mouse wheel zooming or not int _margin; // the contents margin bool _centerContents; // center the contents between margins KMessageWidget *_readOnlyMessageWidget; // Message shown at the top when read-only mode gets activated // Needed to know whether the mode really changed between update calls bool _readOnly; qreal _opacity; bool _dimWhenInactive; ScrollState _scrollWheelState; IncrementalSearchBar *_searchBar; TerminalHeaderBar *_headerBar; QRect _searchResultRect; friend class TerminalDisplayAccessible; bool _drawOverlay; Qt::Edge _overlayEdge; + + bool _hasCompositeFocus; }; class AutoScrollHandler : public QObject { Q_OBJECT public: explicit AutoScrollHandler(QWidget *parent); protected: void timerEvent(QTimerEvent *event) Q_DECL_OVERRIDE; bool eventFilter(QObject *watched, QEvent *event) Q_DECL_OVERRIDE; private: QWidget *widget() const { return static_cast(parent()); } int _timerId; }; + +// Watches compositeWidget and all its focusable children, +// and emits focusChanged() signal when either compositeWidget's +// or a child's focus changed. +// Limitation: children added after the object was created +// will not be registered. +class CompositeWidgetFocusWatcher : public QObject +{ + Q_OBJECT + +public: + explicit CompositeWidgetFocusWatcher(QWidget *compositeWidget, QObject *parent); + bool eventFilter(QObject *watched, QEvent *event) Q_DECL_OVERRIDE; + +Q_SIGNALS: + void compositeFocusChanged(bool focused); + +private: + void registerWidgetAndChildren(QWidget *widget); + + QWidget *_compositeWidget; +}; + } #endif // TERMINALDISPLAY_H diff --git a/src/TerminalHeaderBar.cpp b/src/TerminalHeaderBar.cpp index 2278fda2..b7307c8d 100644 --- a/src/TerminalHeaderBar.cpp +++ b/src/TerminalHeaderBar.cpp @@ -1,180 +1,173 @@ /* * This file is part of Konsole, a terminal emulator for KDE. * * Copyright 2019 Tomaz Canabrava * * 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. */ #include "TerminalHeaderBar.h" #include "TerminalDisplay.h" #include "SessionController.h" #include "ViewProperties.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Konsole { TerminalHeaderBar::TerminalHeaderBar(QWidget *parent) : QWidget(parent) { m_closeBtn = new QToolButton(this); m_closeBtn->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); m_closeBtn->setToolTip(i18nc("@info:tooltip", "Close terminal")); m_closeBtn->setText(i18nc("@info:tooltip", "Close terminal")); m_closeBtn->setObjectName(QStringLiteral("close-terminal-button")); m_closeBtn->setAutoRaise(true); m_toggleExpandedMode = new QToolButton(this); m_toggleExpandedMode->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen"))); // fake 'expand' icon. VDG input? m_toggleExpandedMode->setAutoRaise(true); m_toggleExpandedMode->setCheckable(true); m_toggleExpandedMode->setToolTip(i18nc("@info:tooltip", "Maximize terminal")); m_terminalTitle = new QLabel(this); m_terminalTitle->setFont(QApplication::font()); m_terminalIcon = new QLabel(this); m_terminalActivity = new QLabel(this); m_boxLayout = new QBoxLayout(QBoxLayout::LeftToRight); m_boxLayout->setSpacing(0); m_boxLayout->setContentsMargins(0, 0, 0, 0); // Layout Setup m_boxLayout->addStretch(); m_boxLayout->addWidget(m_terminalIcon); m_boxLayout->addWidget(m_terminalTitle); m_boxLayout->addWidget(m_terminalActivity); m_boxLayout->addStretch(); m_boxLayout->addWidget(m_toggleExpandedMode); m_boxLayout->addWidget(m_closeBtn); setLayout(m_boxLayout); setAutoFillBackground(true); - terminalFocusOut(); + setFocusIndicatorState(false); connect(m_toggleExpandedMode, &QToolButton::clicked, this, &TerminalHeaderBar::requestToggleExpansion); - } // Hack untill I can detangle the creation of the TerminalViews void TerminalHeaderBar::finishHeaderSetup(ViewProperties *properties) { auto controller = dynamic_cast(properties); connect(properties, &Konsole::ViewProperties::titleChanged, this, [this, properties]{ m_terminalTitle->setText(properties->title()); }); connect(properties, &Konsole::ViewProperties::iconChanged, this, [this, properties] { m_terminalIcon->setPixmap(properties->icon().pixmap(QSize(22,22))); }); connect(properties, &Konsole::ViewProperties::activity, this, [this]{ m_terminalActivity->setPixmap(QPixmap()); }); connect(m_closeBtn, &QToolButton::clicked, controller, &SessionController::closeSession); } +void TerminalHeaderBar::setFocusIndicatorState(bool focused) +{ + m_terminalIsFocused = focused; + update(); +} + void TerminalHeaderBar::paintEvent(QPaintEvent *paintEvent) { /* Try to get the widget that's 10px above this one. * If the widget is something else than a TerminalWidget, a TabBar or a QSplitter, * draw a 1px line to separate it from the others. */ const auto globalPos = parentWidget()->mapToGlobal(pos()); auto *widget = qApp->widgetAt(globalPos.x() + 10, globalPos.y() - 10); const bool isTabbar = qobject_cast(widget) != nullptr; const bool isTerminalWidget = qobject_cast(widget) != nullptr; const bool isSplitter = (qobject_cast(widget) != nullptr) || (qobject_cast(widget) != nullptr); if ((widget != nullptr) && !isTabbar && !isTerminalWidget && !isSplitter) { QStyleOptionTabBarBase optTabBase; QStylePainter p(this); optTabBase.init(this); optTabBase.shape = QTabBar::Shape::RoundedSouth; optTabBase.documentMode = false; p.drawPrimitive(QStyle::PE_FrameTabBarBase, optTabBase); } QWidget::paintEvent(paintEvent); if (!m_terminalIsFocused) { auto p = qApp->palette(); auto shadowColor = p.color(QPalette::ColorRole::Shadow); shadowColor.setAlphaF( qreal(0.2) * shadowColor.alphaF() ); // same as breeze. QPainter painter(this); painter.setPen(Qt::NoPen); painter.setBrush(shadowColor); painter.drawRect(rect()); } } void TerminalHeaderBar::mouseMoveEvent(QMouseEvent* ev) { if (m_toggleExpandedMode->isChecked()) { return; } auto point = ev->pos() - m_startDrag; if (point.manhattanLength() > 10) { auto drag = new QDrag(parent()); auto mimeData = new QMimeData(); QByteArray payload; payload.setNum(qApp->applicationPid()); mimeData->setData(QStringLiteral("konsole/terminal_display"), payload); drag->setMimeData(mimeData); drag->exec(); } } void TerminalHeaderBar::mousePressEvent(QMouseEvent* ev) { m_startDrag = ev->pos(); } void TerminalHeaderBar::mouseReleaseEvent(QMouseEvent* ev) { Q_UNUSED(ev); } -void TerminalHeaderBar::terminalFocusIn() -{ - m_terminalIsFocused = true; - update(); -} - -void TerminalHeaderBar::terminalFocusOut() -{ - m_terminalIsFocused = false; - update(); -} - } diff --git a/src/TerminalHeaderBar.h b/src/TerminalHeaderBar.h index f97ae38b..67874f86 100644 --- a/src/TerminalHeaderBar.h +++ b/src/TerminalHeaderBar.h @@ -1,68 +1,68 @@ /* * This file is part of Konsole, a terminal emulator for KDE. * * Copyright 2019 Tomaz Canabrava * * 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 TERMINAL_HEADER_BAR_H #define TERMINAL_HEADER_BAR_H #include #include class QLabel; class QToolButton; class QBoxLayout; namespace Konsole { class TerminalDisplay; class ViewProperties; class TerminalHeaderBar : public QWidget { Q_OBJECT public: // TODO: Verify if the terminalDisplay is needed, or some other thing like SessionController. explicit TerminalHeaderBar(QWidget *parent = nullptr); void finishHeaderSetup(ViewProperties *properties); - void terminalFocusIn(); - void terminalFocusOut(); +public Q_SLOTS: + void setFocusIndicatorState(bool focused); protected: void paintEvent(QPaintEvent* ev) override; void mousePressEvent(QMouseEvent *ev) override; void mouseReleaseEvent(QMouseEvent *ev) override; void mouseMoveEvent(QMouseEvent *ev) override; Q_SIGNALS: void requestToggleExpansion(); private: QBoxLayout *m_boxLayout; TerminalDisplay *m_terminalDisplay; QLabel *m_terminalTitle; QLabel *m_terminalIcon; QLabel *m_terminalActivity; // Bell icon. QToolButton *m_closeBtn; QToolButton *m_toggleExpandedMode; bool m_terminalIsFocused; QPoint m_startDrag; }; } // namespace Konsole #endif diff --git a/src/ViewContainer.cpp b/src/ViewContainer.cpp index 5002c977..76356d7c 100644 --- a/src/ViewContainer.cpp +++ b/src/ViewContainer.cpp @@ -1,634 +1,634 @@ /* This file is part of the Konsole Terminal. Copyright 2006-2008 Robert Knight 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 "ViewContainer.h" #include "config-konsole.h" // Qt #include #include #include #include // KDE #include #include #include #include // Konsole #include "IncrementalSearchBar.h" #include "ViewProperties.h" #include "ProfileList.h" #include "ViewManager.h" #include "KonsoleSettings.h" #include "SessionController.h" #include "DetachableTabBar.h" #include "TerminalDisplay.h" #include "ViewSplitter.h" #include "MainWindow.h" #include "Session.h" // TODO Perhaps move everything which is Konsole-specific into different files using namespace Konsole; TabbedViewContainer::TabbedViewContainer(ViewManager *connectedViewManager, QWidget *parent) : QTabWidget(parent), _connectedViewManager(connectedViewManager), _newTabButton(new QToolButton(this)), _closeTabButton(new QToolButton(this)), _contextMenuTabIndex(-1), _navigationVisibility(ViewManager::NavigationVisibility::NavigationNotSet), _newTabBehavior(PutNewTabAtTheEnd) { setAcceptDrops(true); auto tabBarWidget = new DetachableTabBar(); setTabBar(tabBarWidget); setDocumentMode(true); setMovable(true); connect(tabBarWidget, &DetachableTabBar::moveTabToWindow, this, &TabbedViewContainer::moveTabToWindow); tabBar()->setContextMenuPolicy(Qt::CustomContextMenu); _newTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-new"))); _newTabButton->setAutoRaise(true); connect(_newTabButton, &QToolButton::clicked, this, &TabbedViewContainer::newViewRequest); _closeTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); _closeTabButton->setAutoRaise(true); connect(_closeTabButton, &QToolButton::clicked, this, [this]{ closeCurrentTab(); }); connect(tabBar(), &QTabBar::tabBarDoubleClicked, this, &Konsole::TabbedViewContainer::tabDoubleClicked); connect(tabBar(), &QTabBar::customContextMenuRequested, this, &Konsole::TabbedViewContainer::openTabContextMenu); connect(tabBarWidget, &DetachableTabBar::detachTab, this, [this](int idx) { emit detachTab(idx); }); connect(tabBarWidget, &DetachableTabBar::closeTab, this, &TabbedViewContainer::closeTerminalTab); connect(tabBarWidget, &DetachableTabBar::newTabRequest, this, [this]{ emit newViewRequest(); }); connect(this, &TabbedViewContainer::currentChanged, this, &TabbedViewContainer::currentTabChanged); // The context menu of tab bar _contextPopupMenu = new QMenu(tabBar()); connect(_contextPopupMenu, &QMenu::aboutToHide, this, [this]() { // Remove the read-only action when the popup closes for (auto &action : _contextPopupMenu->actions()) { if (action->objectName() == QLatin1String("view-readonly")) { _contextPopupMenu->removeAction(action); break; } } }); connect(tabBar(), &QTabBar::tabCloseRequested, this, &TabbedViewContainer::closeTerminalTab); auto detachAction = _contextPopupMenu->addAction( QIcon::fromTheme(QStringLiteral("tab-detach")), i18nc("@action:inmenu", "&Detach Tab"), this, [this] { emit detachTab(_contextMenuTabIndex); } ); detachAction->setObjectName(QStringLiteral("tab-detach")); auto editAction = _contextPopupMenu->addAction( QIcon::fromTheme(QStringLiteral("edit-rename")), i18nc("@action:inmenu", "&Rename Tab..."), this, [this]{ renameTab(_contextMenuTabIndex); } ); editAction->setObjectName(QStringLiteral("edit-rename")); auto closeAction = _contextPopupMenu->addAction( QIcon::fromTheme(QStringLiteral("tab-close")), i18nc("@action:inmenu", "Close Tab"), this, [this] { closeTerminalTab(_contextMenuTabIndex); } ); closeAction->setObjectName(QStringLiteral("tab-close")); auto profileMenu = new QMenu(this); auto profileList = new ProfileList(false, profileMenu); profileList->syncWidgetActions(profileMenu, true); connect(profileList, &Konsole::ProfileList::profileSelected, this, &TabbedViewContainer::newViewWithProfileRequest); _newTabButton->setMenu(profileMenu); konsoleConfigChanged(); connect(KonsoleSettings::self(), &KonsoleSettings::configChanged, this, &TabbedViewContainer::konsoleConfigChanged); } TabbedViewContainer::~TabbedViewContainer() { for(int i = 0, end = count(); i < end; i++) { auto view = widget(i); disconnect(view, &QWidget::destroyed, this, &Konsole::TabbedViewContainer::viewDestroyed); } } ViewSplitter *TabbedViewContainer::activeViewSplitter() { return viewSplitterAt(currentIndex()); } ViewSplitter *TabbedViewContainer::viewSplitterAt(int index) { return qobject_cast(widget(index)); } void TabbedViewContainer::moveTabToWindow(int index, QWidget *window) { auto splitter = viewSplitterAt(index); auto manager = window->findChild(); QHash sessionsMap = _connectedViewManager->forgetAll(splitter); foreach(TerminalDisplay* terminal, splitter->findChildren()) { manager->attachView(terminal, sessionsMap[terminal]); } auto container = manager->activeContainer(); container->addSplitter(splitter); auto controller = splitter->activeTerminalDisplay()->sessionController(); container->currentSessionControllerChanged(controller); forgetView(splitter); } void TabbedViewContainer::konsoleConfigChanged() { // don't show tabs if we are in KParts mode. // This is a hack, and this needs to be rewritten. // The container should not be part of the KParts, perhaps just the // TerminalDisplay should. // ASAN issue if using sessionController->isKonsolePart(), just // duplicate code for now if (qApp->applicationName() != QLatin1String("konsole")) { tabBar()->setVisible(false); } else { // if we start with --show-tabbar or --hide-tabbar we ignore the preferences. setTabBarAutoHide(KonsoleSettings::tabBarVisibility() == KonsoleSettings::EnumTabBarVisibility::ShowTabBarWhenNeeded); if (KonsoleSettings::tabBarVisibility() == KonsoleSettings::EnumTabBarVisibility::AlwaysShowTabBar) { tabBar()->setVisible(true); } else if (KonsoleSettings::tabBarVisibility() == KonsoleSettings::EnumTabBarVisibility::AlwaysHideTabBar) { tabBar()->setVisible(false); } } setTabPosition((QTabWidget::TabPosition) KonsoleSettings::tabBarPosition()); setCornerWidget(KonsoleSettings::newTabButton() ? _newTabButton : nullptr, Qt::TopLeftCorner); _newTabButton->setVisible(KonsoleSettings::newTabButton()); setCornerWidget(KonsoleSettings::closeTabButton() == 1 ? _closeTabButton : nullptr, Qt::TopRightCorner); _closeTabButton->setVisible(KonsoleSettings::closeTabButton() == 1); tabBar()->setTabsClosable(KonsoleSettings::closeTabButton() == 0); tabBar()->setExpanding(KonsoleSettings::expandTabWidth()); tabBar()->update(); if (KonsoleSettings::tabBarUseUserStyleSheet()) { setCssFromFile(KonsoleSettings::tabBarUserStyleSheetFile()); } else { setCss(); } } void TabbedViewContainer::setCss(const QString& styleSheet) { static const QString defaultCss = QStringLiteral("QTabWidget::tab-bar, QTabWidget::pane { margin: 0; }\n"); setStyleSheet(defaultCss + styleSheet); } void TabbedViewContainer::setCssFromFile(const QUrl &url) { // Let's only deal w/ local files for now if (!url.isLocalFile()) { setStyleSheet(KonsoleSettings::tabBarStyleSheet()); } QFile file(url.toLocalFile()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { setStyleSheet(KonsoleSettings::tabBarStyleSheet()); } QTextStream in(&file); setCss(in.readAll()); } void TabbedViewContainer::moveActiveView(MoveDirection direction) { if (count() < 2) { // return if only one view return; } const int currentIndex = indexOf(currentWidget()); int newIndex = direction == MoveViewLeft ? qMax(currentIndex - 1, 0) : qMin(currentIndex + 1, count() - 1); auto swappedWidget = viewSplitterAt(newIndex); auto swappedTitle = tabBar()->tabText(newIndex); auto swappedIcon = tabBar()->tabIcon(newIndex); auto currentWidget = viewSplitterAt(currentIndex); auto currentTitle = tabBar()->tabText(currentIndex); auto currentIcon = tabBar()->tabIcon(currentIndex); if (newIndex < currentIndex) { insertTab(newIndex, currentWidget, currentIcon, currentTitle); insertTab(currentIndex, swappedWidget, swappedIcon, swappedTitle); } else { insertTab(currentIndex, swappedWidget, swappedIcon, swappedTitle); insertTab(newIndex, currentWidget, currentIcon, currentTitle); } setCurrentIndex(newIndex); } void TabbedViewContainer::terminalDisplayDropped(TerminalDisplay *terminalDisplay) { Session* terminalSession = terminalDisplay->sessionController()->session(); terminalDisplay->sessionController()->deleteLater(); connectedViewManager()->attachView(terminalDisplay, terminalSession); } QSize TabbedViewContainer::sizeHint() const { // QTabWidget::sizeHint() contains some margins added by widgets // style, which were making the initial window size too big. const auto tabsSize = tabBar()->sizeHint(); const auto *leftWidget = cornerWidget(Qt::TopLeftCorner); const auto *rightWidget = cornerWidget(Qt::TopRightCorner); const auto leftSize = leftWidget ? leftWidget->sizeHint() : QSize(0, 0); const auto rightSize = rightWidget ? rightWidget->sizeHint() : QSize(0, 0); auto tabBarSize = QSize(0, 0); // isVisible() won't work; this is called when the window is not yet visible if (tabBar()->isVisibleTo(this)) { tabBarSize.setWidth(leftSize.width() + tabsSize.width() + rightSize.width()); tabBarSize.setHeight(qMax(tabsSize.height(), qMax(leftSize.height(), rightSize.height()))); } const auto terminalSize = currentWidget() ? currentWidget()->sizeHint() : QSize(0, 0); // width // ├──────────────────┤ // // ┌──────────────────┐ ┬ // │ │ │ // │ Terminal │ │ // │ │ │ height // ├───┬──────────┬───┤ │ ┬ // │ L │ Tabs │ R │ │ │ tab bar height // └───┴──────────┴───┘ ┴ ┴ // // L/R = left/right widget return {qMax(terminalSize.width(), tabBarSize.width()), tabBarSize.height() + terminalSize.height()}; } void TabbedViewContainer::addSplitter(ViewSplitter *viewSplitter, int index) { if (index == -1) { index = addTab(viewSplitter, QString()); } else { insertTab(index, viewSplitter, QString()); } connect(viewSplitter, &ViewSplitter::destroyed, this, &TabbedViewContainer::viewDestroyed); disconnect(viewSplitter, &ViewSplitter::terminalDisplayDropped, nullptr, nullptr); connect(viewSplitter, &ViewSplitter::terminalDisplayDropped, this, &TabbedViewContainer::terminalDisplayDropped); auto terminalDisplays = viewSplitter->findChildren(); foreach(TerminalDisplay* terminal, terminalDisplays) { connectTerminalDisplay(terminal); } if (terminalDisplays.count() > 0) { updateTitle(qobject_cast(terminalDisplays.at(0)->sessionController())); } setCurrentIndex(index); } void TabbedViewContainer::addView(TerminalDisplay *view) { auto viewSplitter = new ViewSplitter(); viewSplitter->addTerminalDisplay(view, Qt::Horizontal); auto item = view->sessionController(); int index = _newTabBehavior == PutNewTabAfterCurrentTab ? currentIndex() + 1 : -1; if (index == -1) { index = addTab(viewSplitter, item->icon(), item->title()); } else { insertTab(index, viewSplitter, item->icon(), item->title()); } connectTerminalDisplay(view); connect(viewSplitter, &ViewSplitter::destroyed, this, &TabbedViewContainer::viewDestroyed); connect(viewSplitter, &ViewSplitter::terminalDisplayDropped, this, &TabbedViewContainer::terminalDisplayDropped); setCurrentIndex(index); emit viewAdded(view); } void TabbedViewContainer::splitView(TerminalDisplay *view, Qt::Orientation orientation) { auto viewSplitter = qobject_cast(currentWidget()); viewSplitter->addTerminalDisplay(view, orientation); connectTerminalDisplay(view); } void TabbedViewContainer::connectTerminalDisplay(TerminalDisplay *display) { auto item = display->sessionController(); - connect(item, &Konsole::SessionController::focused, this, + connect(item, &Konsole::SessionController::viewFocused, this, &Konsole::TabbedViewContainer::currentSessionControllerChanged); connect(item, &Konsole::ViewProperties::titleChanged, this, &Konsole::TabbedViewContainer::updateTitle); connect(item, &Konsole::ViewProperties::iconChanged, this, &Konsole::TabbedViewContainer::updateIcon); connect(item, &Konsole::ViewProperties::activity, this, &Konsole::TabbedViewContainer::updateActivity); } void TabbedViewContainer::disconnectTerminalDisplay(TerminalDisplay *display) { auto item = display->sessionController(); - disconnect(item, &Konsole::SessionController::focused, this, + disconnect(item, &Konsole::SessionController::viewFocused, this, &Konsole::TabbedViewContainer::currentSessionControllerChanged); disconnect(item, &Konsole::ViewProperties::titleChanged, this, &Konsole::TabbedViewContainer::updateTitle); disconnect(item, &Konsole::ViewProperties::iconChanged, this, &Konsole::TabbedViewContainer::updateIcon); disconnect(item, &Konsole::ViewProperties::activity, this, &Konsole::TabbedViewContainer::updateActivity); } void TabbedViewContainer::viewDestroyed(QObject *view) { auto widget = static_cast(view); const auto idx = indexOf(widget); removeTab(idx); forgetView(widget); } void TabbedViewContainer::forgetView(ViewSplitter *view) { Q_UNUSED(view); if (count() == 0) { emit empty(this); } } void TabbedViewContainer::activateNextView() { QWidget *active = currentWidget(); int index = indexOf(active); setCurrentIndex(index == count() - 1 ? 0 : index + 1); } void TabbedViewContainer::activateLastView() { setCurrentIndex(count() - 1); } void TabbedViewContainer::activatePreviousView() { QWidget *active = currentWidget(); int index = indexOf(active); setCurrentIndex(index == 0 ? count() - 1 : index - 1); } void TabbedViewContainer::keyReleaseEvent(QKeyEvent* event) { if (event->modifiers() == Qt::NoModifier) { _connectedViewManager->updateTerminalDisplayHistory(); } } void TabbedViewContainer::closeCurrentTab() { if (currentIndex() != -1) { closeTerminalTab(currentIndex()); } } void TabbedViewContainer::tabDoubleClicked(int index) { if (index >= 0) { renameTab(index); } else { emit newViewRequest(); } } void TabbedViewContainer::renameTab(int index) { if (index != -1) { setCurrentIndex(index); viewSplitterAt(index) -> activeTerminalDisplay() -> sessionController() -> rename(); } } void TabbedViewContainer::openTabContextMenu(const QPoint &point) { if (point.isNull()) { return; } _contextMenuTabIndex = tabBar()->tabAt(point); if (_contextMenuTabIndex < 0) { return; } //TODO: add a countChanged signal so we can remove this for. // Detaching in mac causes crashes. for(auto action : _contextPopupMenu->actions()) { if (action->objectName() == QLatin1String("tab-detach")) { action->setEnabled(count() > 1); } } /* This needs to nove away fro the tab or to lock every thing inside of it. * for now, disable. * */ // // Add the read-only action #if 0 auto sessionController = terminalAt(_contextMenuTabIndex)->sessionController(); if (sessionController != nullptr) { auto collection = sessionController->actionCollection(); auto readonlyAction = collection->action(QStringLiteral("view-readonly")); if (readonlyAction != nullptr) { const auto readonlyActions = _contextPopupMenu->actions(); _contextPopupMenu->insertAction(readonlyActions.last(), readonlyAction); } // Disable tab rename for (auto &action : _contextPopupMenu->actions()) { if (action->objectName() == QLatin1String("edit-rename")) { action->setEnabled(!sessionController->isReadOnly()); break; } } } #endif _contextPopupMenu->exec(tabBar()->mapToGlobal(point)); } void TabbedViewContainer::currentTabChanged(int index) { if (index != -1) { auto splitview = qobject_cast(widget(index)); auto view = splitview->activeTerminalDisplay(); emit activeViewChanged(view); setTabActivity(index, false); } else { deleteLater(); } } void TabbedViewContainer::wheelScrolled(int delta) { if (delta < 0) { activateNextView(); } else { activatePreviousView(); } } void TabbedViewContainer::setTabActivity(int index, bool activity) { const QPalette &palette = tabBar()->palette(); KColorScheme colorScheme(palette.currentColorGroup()); const QColor colorSchemeActive = colorScheme.foreground(KColorScheme::ActiveText).color(); const QColor normalColor = palette.text().color(); const QColor activityColor = KColorUtils::mix(normalColor, colorSchemeActive); QColor color = activity ? activityColor : QColor(); if (color != tabBar()->tabTextColor(index)) { tabBar()->setTabTextColor(index, color); } } void TabbedViewContainer::updateActivity(ViewProperties *item) { auto controller = qobject_cast(item); auto topLevelSplitter = qobject_cast(controller->view()->parentWidget())->getToplevelSplitter(); const int index = indexOf(topLevelSplitter); if (index != currentIndex()) { setTabActivity(index, true); } } void TabbedViewContainer::currentSessionControllerChanged(SessionController *controller) { updateTitle(qobject_cast(controller)); } void TabbedViewContainer::updateTitle(ViewProperties *item) { auto controller = qobject_cast(item); auto topLevelSplitter = qobject_cast(controller->view()->parentWidget())->getToplevelSplitter(); const int index = indexOf(topLevelSplitter); QString tabText = item->title(); setTabToolTip(index, tabText); // To avoid having & replaced with _ (shortcut indicator) tabText.replace(QLatin1Char('&'), QLatin1String("&&")); setTabText(index, tabText); } void TabbedViewContainer::updateIcon(ViewProperties *item) { auto controller = qobject_cast(item); const int index = indexOf(controller->view()); setTabIcon(index, item->icon()); } void TabbedViewContainer::closeTerminalTab(int idx) { //TODO: This for should probably go to the ViewSplitter for (auto terminal : viewSplitterAt(idx)->findChildren()) { terminal->sessionController()->closeSession(); } } ViewManager *TabbedViewContainer::connectedViewManager() { return _connectedViewManager; } void TabbedViewContainer::setNavigationVisibility(ViewManager::NavigationVisibility navigationVisibility) { if (navigationVisibility == ViewManager::NavigationNotSet) { return; } setTabBarAutoHide(navigationVisibility == ViewManager::ShowNavigationAsNeeded); if (navigationVisibility == ViewManager::AlwaysShowNavigation) { tabBar()->setVisible(true); } else if (navigationVisibility == ViewManager::AlwaysHideNavigation) { tabBar()->setVisible(false); } } void TabbedViewContainer::toggleMaximizeCurrentTerminal() { if (auto *terminal = qobject_cast(sender())) { terminal->setFocus(Qt::FocusReason::OtherFocusReason); } activeViewSplitter()->toggleMaximizeCurrentTerminal(); } void TabbedViewContainer::moveTabLeft() { if (currentIndex() == 0) { return; } tabBar()->moveTab(currentIndex(), currentIndex() -1); } void TabbedViewContainer::moveTabRight() { if (currentIndex() == count() -1) { return; } tabBar()->moveTab(currentIndex(), currentIndex() + 1); } void TabbedViewContainer::setNavigationBehavior(int behavior) { _newTabBehavior = static_cast(behavior); } diff --git a/src/ViewManager.cpp b/src/ViewManager.cpp index 3efbc3ec..c2604db5 100644 --- a/src/ViewManager.cpp +++ b/src/ViewManager.cpp @@ -1,1144 +1,1144 @@ /* Copyright 2006-2008 by Robert Knight 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 "ViewManager.h" #include "config-konsole.h" // Qt #include #include // KDE #include #include #include // Konsole #include #include "ColorScheme.h" #include "ColorSchemeManager.h" #include "Session.h" #include "TerminalDisplay.h" #include "SessionController.h" #include "SessionManager.h" #include "ProfileManager.h" #include "ViewSplitter.h" #include "Enumeration.h" #include "ViewContainer.h" using namespace Konsole; int ViewManager::lastManagerId = 0; ViewManager::ViewManager(QObject *parent, KActionCollection *collection) : QObject(parent), _viewContainer(nullptr), _pluggedController(nullptr), _sessionMap(QHash()), _actionCollection(collection), _navigationMethod(NoNavigation), _navigationVisibility(NavigationNotSet), _managerId(0), _terminalDisplayHistoryIndex(-1) { _viewContainer = createContainer(); // setup actions which are related to the views setupActions(); /* TODO: Reconnect // emit a signal when all of the views held by this view manager are destroyed */ connect(_viewContainer.data(), &Konsole::TabbedViewContainer::empty, this, &Konsole::ViewManager::empty); // listen for profile changes connect(ProfileManager::instance(), &Konsole::ProfileManager::profileChanged, this, &Konsole::ViewManager::profileChanged); connect(SessionManager::instance(), &Konsole::SessionManager::sessionUpdated, this, &Konsole::ViewManager::updateViewsForSession); //prepare DBus communication new WindowAdaptor(this); _managerId = ++lastManagerId; QDBusConnection::sessionBus().registerObject(QLatin1String("/Windows/") + QString::number(_managerId), this); } ViewManager::~ViewManager() = default; int ViewManager::managerId() const { return _managerId; } QWidget *ViewManager::activeView() const { return _viewContainer->currentWidget(); } QWidget *ViewManager::widget() const { return _viewContainer; } void ViewManager::setupActions() { Q_ASSERT(_actionCollection); if (_actionCollection == nullptr) { return; } KActionCollection *collection = _actionCollection; // Let's reuse the pointer, no need not to. auto *action = new QAction(this); action->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right"))); action->setText(i18nc("@action:inmenu", "Split View Left/Right")); connect(action, &QAction::triggered, this, &ViewManager::splitLeftRight); collection->addAction(QStringLiteral("split-view-left-right"), action); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::Key_ParenLeft); action = new QAction(this); action->setIcon(QIcon::fromTheme(QStringLiteral("view-split-top-bottom"))); action->setText(i18nc("@action:inmenu", "Split View Top/Bottom")); connect(action, &QAction::triggered, this, &ViewManager::splitTopBottom); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::Key_ParenRight); collection->addAction(QStringLiteral("split-view-top-bottom"), action); action = new QAction(this); action->setText(i18nc("@action:inmenu", "Expand View")); action->setEnabled(false); connect(action, &QAction::triggered, this, &ViewManager::expandActiveContainer); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_BracketRight); collection->addAction(QStringLiteral("expand-active-view"), action); _multiSplitterOnlyActions << action; action = new QAction(this); action->setText(i18nc("@action:inmenu", "Shrink View")); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_BracketLeft); action->setEnabled(false); collection->addAction(QStringLiteral("shrink-active-view"), action); connect(action, &QAction::triggered, this, &ViewManager::shrinkActiveContainer); _multiSplitterOnlyActions << action; action = collection->addAction(QStringLiteral("detach-view")); action->setEnabled(true); action->setIcon(QIcon::fromTheme(QStringLiteral("tab-detach"))); action->setText(i18nc("@action:inmenu", "Detach Current &View")); connect(action, &QAction::triggered, this, &ViewManager::detachActiveView); _multiSplitterOnlyActions << action; // Ctrl+Shift+D is not used as a shortcut by default because it is too close // to Ctrl+D - which will terminate the session in many cases collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_H); action = collection->addAction(QStringLiteral("detach-tab")); action->setEnabled(true); action->setIcon(QIcon::fromTheme(QStringLiteral("tab-detach"))); action->setText(i18nc("@action:inmenu", "Detach Current &Tab")); connect(action, &QAction::triggered, this, &ViewManager::detachActiveTab); _multiTabOnlyActions << action; // Ctrl+Shift+D is not used as a shortcut by default because it is too close // to Ctrl+D - which will terminate the session in many cases collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_L); // keyboard shortcut only actions action = new QAction(i18nc("@action Shortcut entry", "Next Tab"), this); const QList nextViewActionKeys{Qt::SHIFT + Qt::Key_Right, Qt::CTRL + Qt::Key_PageDown}; collection->setDefaultShortcuts(action, nextViewActionKeys); collection->addAction(QStringLiteral("next-tab"), action); connect(action, &QAction::triggered, this, &ViewManager::nextView); _multiTabOnlyActions << action; // _viewSplitter->addAction(nextViewAction); action = new QAction(i18nc("@action Shortcut entry", "Previous Tab"), this); const QList previousViewActionKeys{Qt::SHIFT + Qt::Key_Left, Qt::CTRL + Qt::Key_PageUp}; collection->setDefaultShortcuts(action, previousViewActionKeys); collection->addAction(QStringLiteral("previous-tab"), action); connect(action, &QAction::triggered, this, &ViewManager::previousView); _multiTabOnlyActions << action; // _viewSplitter->addAction(previousViewAction); action = new QAction(i18nc("@action Shortcut entry", "Focus Above Terminal"), this); connect(action, &QAction::triggered, this, &ViewManager::focusUp); collection->addAction(QStringLiteral("focus-view-above"), action); collection->setDefaultShortcut(action, Qt::SHIFT + Qt::CTRL + Qt::Key_Up); _viewContainer->addAction(action); _multiSplitterOnlyActions << action; action = new QAction(i18nc("@action Shortcut entry", "Focus Below Terminal"), this); collection->setDefaultShortcut(action, Qt::SHIFT + Qt::CTRL + Qt::Key_Down); collection->addAction(QStringLiteral("focus-view-below"), action); connect(action, &QAction::triggered, this, &ViewManager::focusDown); _multiSplitterOnlyActions << action; _viewContainer->addAction(action); action = new QAction(i18nc("@action Shortcut entry", "Focus Left Terminal"), this); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Konsole::LEFT); connect(action, &QAction::triggered, this, &ViewManager::focusLeft); collection->addAction(QStringLiteral("focus-view-left"), action); _multiSplitterOnlyActions << action; action = new QAction(i18nc("@action Shortcut entry", "Focus Right Terminal"), this); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Konsole::RIGHT); connect(action, &QAction::triggered, this, &ViewManager::focusRight); collection->addAction(QStringLiteral("focus-view-right"), action); _multiSplitterOnlyActions << action; action = new QAction(i18nc("@action Shortcut entry", "Switch to Last Tab"), this); connect(action, &QAction::triggered, this, &ViewManager::lastView); collection->addAction(QStringLiteral("last-tab"), action); _multiTabOnlyActions << action; action = new QAction(i18nc("@action Shortcut entry", "Last Used Tabs"), this); connect(action, &QAction::triggered, this, &ViewManager::lastUsedView); collection->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Tab); collection->addAction(QStringLiteral("last-used-tab"), action); action = new QAction(i18nc("@action Shortcut entry", "Toggle Between Two Tabs"), this); connect(action, &QAction::triggered, this, &Konsole::ViewManager::toggleTwoViews); collection->addAction(QStringLiteral("toggle-two-tabs"), action); _multiTabOnlyActions << action; action = new QAction(i18nc("@action Shortcut entry", "Last Used Tabs (Reverse)"), this); collection->addAction(QStringLiteral("last-used-tab-reverse"), action); collection->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_Tab); connect(action, &QAction::triggered, this, &ViewManager::lastUsedViewReverse); action = new QAction(i18nc("@action Shortcut entry", "Maximize current Terminal"), this); collection->addAction(QStringLiteral("maximize-current-terminal"), action); collection->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_E); connect(action, &QAction::triggered, _viewContainer, &TabbedViewContainer::toggleMaximizeCurrentTerminal); _multiSplitterOnlyActions << action; _viewContainer->addAction(action); action = new QAction(i18nc("@action Shortcut entry", "Move tab to the right"), this); collection->addAction(QStringLiteral("move-tab-to-right"), action); collection->setDefaultShortcut(action, Qt::CTRL + Qt::ALT + Qt::Key_Right); connect(action, &QAction::triggered, _viewContainer, &TabbedViewContainer::moveTabRight); _viewContainer->addAction(action); action = new QAction(i18nc("@action Shortcut entry", "Move tab to the left"), this); collection->addAction(QStringLiteral("move-tab-to-left"), action); collection->setDefaultShortcut(action, Qt::CTRL + Qt::ALT + Qt::Key_Left); connect(action, &QAction::triggered, _viewContainer, &TabbedViewContainer::moveTabLeft); _viewContainer->addAction(action); // _viewSplitter->addAction(lastUsedViewReverseAction); const int SWITCH_TO_TAB_COUNT = 19; for (int i = 0; i < SWITCH_TO_TAB_COUNT; i++) { action = new QAction(i18nc("@action Shortcut entry", "Switch to Tab %1", i + 1), this); connect(action, &QAction::triggered, this, [this, i]() { switchToView(i); }); collection->addAction(QStringLiteral("switch-to-tab-%1").arg(i), action); } connect(_viewContainer, &TabbedViewContainer::viewAdded, this, &ViewManager::toggleActionsBasedOnState); connect(_viewContainer, &TabbedViewContainer::viewRemoved, this, &ViewManager::toggleActionsBasedOnState); connect(_viewContainer, &QTabWidget::currentChanged, this, &ViewManager::toggleActionsBasedOnState); toggleActionsBasedOnState(); } void ViewManager::toggleActionsBasedOnState() { const int count = _viewContainer->count(); foreach(QAction *tabOnlyAction, _multiTabOnlyActions) { tabOnlyAction->setEnabled(count > 1); } if ((_viewContainer != nullptr) && (_viewContainer->activeViewSplitter() != nullptr)) { const int splitCount = _viewContainer ->activeViewSplitter() ->getToplevelSplitter() ->findChildren() .count(); foreach (QAction *action, _multiSplitterOnlyActions) { action->setEnabled(splitCount > 1); } } } void ViewManager::switchToView(int index) { _viewContainer->setCurrentIndex(index); } void ViewManager::switchToTerminalDisplay(Konsole::TerminalDisplay* terminalDisplay) { auto splitter = qobject_cast(terminalDisplay->parentWidget()); auto toplevelSplitter = splitter->getToplevelSplitter(); // Focus the TermialDisplay terminalDisplay->setFocus(); if (_viewContainer->currentWidget() != toplevelSplitter) { // Focus the tab switchToView(_viewContainer->indexOf(toplevelSplitter)); } } void ViewManager::focusUp() { _viewContainer->activeViewSplitter()->focusUp(); } void ViewManager::focusDown() { _viewContainer->activeViewSplitter()->focusDown(); } void ViewManager::focusLeft() { _viewContainer->activeViewSplitter()->focusLeft(); } void ViewManager::focusRight() { _viewContainer->activeViewSplitter()->focusRight(); } void ViewManager::moveActiveViewLeft() { _viewContainer->moveActiveView(TabbedViewContainer::MoveViewLeft); } void ViewManager::moveActiveViewRight() { _viewContainer->moveActiveView(TabbedViewContainer::MoveViewRight); } void ViewManager::nextContainer() { // _viewSplitter->activateNextContainer(); } void ViewManager::nextView() { _viewContainer->activateNextView(); } void ViewManager::previousView() { _viewContainer->activatePreviousView(); } void ViewManager::lastView() { _viewContainer->activateLastView(); } void ViewManager::activateLastUsedView(bool reverse) { if (_terminalDisplayHistory.count() <= 1) { return; } if (_terminalDisplayHistoryIndex == -1) { _terminalDisplayHistoryIndex = reverse ? _terminalDisplayHistory.count() - 1 : 1; } else if (reverse) { if (_terminalDisplayHistoryIndex == 0) { _terminalDisplayHistoryIndex = _terminalDisplayHistory.count() - 1; } else { _terminalDisplayHistoryIndex--; } } else { if (_terminalDisplayHistoryIndex >= _terminalDisplayHistory.count() - 1) { _terminalDisplayHistoryIndex = 0; } else { _terminalDisplayHistoryIndex++; } } switchToTerminalDisplay(_terminalDisplayHistory[_terminalDisplayHistoryIndex]); } void ViewManager::lastUsedView() { activateLastUsedView(false); } void ViewManager::lastUsedViewReverse() { activateLastUsedView(true); } void ViewManager::toggleTwoViews() { if (_terminalDisplayHistory.count() <= 1) { return; } switchToTerminalDisplay(_terminalDisplayHistory.at(1)); } void ViewManager::detachActiveView() { // find the currently active view and remove it from its container if ((_viewContainer->findChildren()).count() > 1) { auto activeSplitter = _viewContainer->activeViewSplitter(); auto terminal = activeSplitter->activeTerminalDisplay(); auto newSplitter = new ViewSplitter(); newSplitter->addTerminalDisplay(terminal, Qt::Horizontal); QHash detachedSessions = forgetAll(newSplitter); emit terminalsDetached(newSplitter, detachedSessions); focusAnotherTerminal(activeSplitter->getToplevelSplitter()); toggleActionsBasedOnState(); } } void ViewManager::detachActiveTab() { const int currentIdx = _viewContainer->currentIndex(); detachTab(currentIdx); } void ViewManager::detachTab(int tabIdx) { ViewSplitter* splitter = _viewContainer->viewSplitterAt(tabIdx); QHash detachedSessions = forgetAll(_viewContainer->viewSplitterAt(tabIdx)); emit terminalsDetached(splitter, detachedSessions); } QHash ViewManager::forgetAll(ViewSplitter* splitter) { splitter->setParent(nullptr); QHash detachedSessions; foreach(TerminalDisplay* terminal, splitter->findChildren()) { Session* session = forgetTerminal(terminal); detachedSessions[terminal] = session; } return detachedSessions; } Session* ViewManager::forgetTerminal(TerminalDisplay* terminal) { disconnect(terminal, &TerminalDisplay::requestToggleExpansion, nullptr, nullptr); removeController(terminal->sessionController()); auto session = _sessionMap.take(terminal); if (session != nullptr) { disconnect(session, &Konsole::Session::finished, this, &Konsole::ViewManager::sessionFinished); } _viewContainer->disconnectTerminalDisplay(terminal); updateTerminalDisplayHistory(terminal, true); return session; } Session* ViewManager::createSession(const Profile::Ptr &profile, const QString &directory) { Session *session = SessionManager::instance()->createSession(profile); Q_ASSERT(session); if (!directory.isEmpty()) { session->setInitialWorkingDirectory(directory); } session->addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_WINDOW=/Windows/%1").arg(managerId())); return session; } void ViewManager::sessionFinished() { // if this slot is called after the view manager's main widget // has been destroyed, do nothing if (_viewContainer.isNull()) { return; } auto *session = qobject_cast(sender()); Q_ASSERT(session); auto view = _sessionMap.key(session); _sessionMap.remove(view); if (SessionManager::instance()->isClosingAllSessions()){ return; } // Before deleting the view, let's unmaximize if it's maximized. auto *splitter = qobject_cast(view->parentWidget()); if (splitter == nullptr) { return; } auto *toplevelSplitter = splitter->getToplevelSplitter(); toplevelSplitter->handleMinimizeMaximize(false); view->deleteLater(); // Only remove the controller from factory() if it's actually controlling // the session from the sender. // This fixes BUG: 348478 - messed up menus after a detached tab is closed if ((!_pluggedController.isNull()) && (_pluggedController->session() == session)) { // This is needed to remove this controller from factory() in // order to prevent BUG: 185466 - disappearing menu popup emit unplugController(_pluggedController); } if (!_sessionMap.empty()) { updateTerminalDisplayHistory(view, true); focusAnotherTerminal(toplevelSplitter); toggleActionsBasedOnState(); } } void ViewManager::focusAnotherTerminal(ViewSplitter *toplevelSplitter) { auto tabTterminalDisplays = toplevelSplitter->findChildren(); if (tabTterminalDisplays.count() == 0) { return; } if (tabTterminalDisplays.count() > 1) { // Give focus to the last used terminal in this tab for (auto *historyItem : _terminalDisplayHistory) { for (auto *terminalDisplay : tabTterminalDisplays) { if (terminalDisplay == historyItem) { terminalDisplay->setFocus(Qt::OtherFocusReason); return; } } } } else if (_terminalDisplayHistory.count() >= 1) { // Give focus to the last used terminal tab switchToTerminalDisplay(_terminalDisplayHistory[0]); } } -void ViewManager::viewActivated(TerminalDisplay *view) +void ViewManager::activateView(TerminalDisplay *view) { Q_ASSERT(view != nullptr); // focus the activated view, this will cause the SessionController // to notify the world that the view has been focused and the appropriate UI // actions will be plugged in. view->setFocus(Qt::OtherFocusReason); } void ViewManager::splitLeftRight() { splitView(Qt::Horizontal); } void ViewManager::splitTopBottom() { splitView(Qt::Vertical); } void ViewManager::splitView(Qt::Orientation orientation) { int currentSessionId = currentSession(); // At least one display/session exists if we are splitting Q_ASSERT(currentSessionId >= 0); Session *activeSession = SessionManager::instance()->idToSession(currentSessionId); Q_ASSERT(activeSession); auto profile = SessionManager::instance()->sessionProfile(activeSession); const QString directory = profile->startInCurrentSessionDir() ? activeSession->currentWorkingDirectory() : QString(); auto *session = createSession(profile, directory); auto terminalDisplay = createView(session); _viewContainer->splitView(terminalDisplay, orientation); toggleActionsBasedOnState(); // focus the new container terminalDisplay->setFocus(); } void ViewManager::expandActiveContainer() { _viewContainer->activeViewSplitter()->adjustActiveTerminalDisplaySize(10); } void ViewManager::shrinkActiveContainer() { _viewContainer->activeViewSplitter()->adjustActiveTerminalDisplaySize(-10); } SessionController *ViewManager::createController(Session *session, TerminalDisplay *view) { // create a new controller for the session, and ensure that this view manager // is notified when the view gains the focus auto controller = new SessionController(session, view, this); - connect(controller, &Konsole::SessionController::focused, this, + connect(controller, &Konsole::SessionController::viewFocused, this, &Konsole::ViewManager::controllerChanged); connect(session, &Konsole::Session::destroyed, controller, &Konsole::SessionController::deleteLater); connect(session, &Konsole::Session::primaryScreenInUse, controller, &Konsole::SessionController::setupPrimaryScreenSpecificActions); connect(session, &Konsole::Session::selectionChanged, controller, &Konsole::SessionController::selectionChanged); connect(view, &Konsole::TerminalDisplay::destroyed, controller, &Konsole::SessionController::deleteLater); // if this is the first controller created then set it as the active controller if (_pluggedController.isNull()) { controllerChanged(controller); } return controller; } // should this be handed by ViewManager::unplugController signal void ViewManager::removeController(SessionController* controller) { if (_pluggedController == controller) { _pluggedController.clear(); } controller->deleteLater(); } void ViewManager::controllerChanged(SessionController *controller) { if (controller == _pluggedController) { return; } _viewContainer->setFocusProxy(controller->view()); updateTerminalDisplayHistory(controller->view()); _pluggedController = controller; emit activeViewChanged(controller); } SessionController *ViewManager::activeViewController() const { return _pluggedController; } void ViewManager::attachView(TerminalDisplay *terminal, Session *session) { connect(session, &Konsole::Session::finished, this, &Konsole::ViewManager::sessionFinished, Qt::UniqueConnection); // Disconnect from the other viewcontainer. disconnect(terminal, &TerminalDisplay::requestToggleExpansion, nullptr, nullptr); // reconnect on this container. connect(terminal, &TerminalDisplay::requestToggleExpansion, _viewContainer, &TabbedViewContainer::toggleMaximizeCurrentTerminal, Qt::UniqueConnection); _sessionMap[terminal] = session; createController(session, terminal); toggleActionsBasedOnState(); _terminalDisplayHistory.append(terminal); } TerminalDisplay *ViewManager::createView(Session *session) { // notify this view manager when the session finishes so that its view // can be deleted // // Use Qt::UniqueConnection to avoid duplicate connection connect(session, &Konsole::Session::finished, this, &Konsole::ViewManager::sessionFinished, Qt::UniqueConnection); TerminalDisplay *display = createTerminalDisplay(session); const Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); applyProfileToView(display, profile); // set initial size const QSize &preferredSize = session->preferredSize(); display->setSize(preferredSize.width(), preferredSize.height()); createController(session, display); _sessionMap[display] = session; session->addView(display); _terminalDisplayHistory.append(display); // tell the session whether it has a light or dark background session->setDarkBackground(colorSchemeForProfile(profile)->hasDarkBackground()); display->setFocus(Qt::OtherFocusReason); // updateDetachViewState(); return display; } TabbedViewContainer *ViewManager::createContainer() { auto *container = new TabbedViewContainer(this, nullptr); container->setNavigationVisibility(_navigationVisibility); connect(container, &TabbedViewContainer::detachTab, this, &ViewManager::detachTab); // connect signals and slots connect(container, &Konsole::TabbedViewContainer::viewAdded, this, [this, container]() { containerViewsChanged(container); }); connect(container, &Konsole::TabbedViewContainer::viewRemoved, this, [this, container]() { containerViewsChanged(container); }); connect(container, &TabbedViewContainer::newViewRequest, this, &ViewManager::newViewRequest); connect(container, &Konsole::TabbedViewContainer::newViewWithProfileRequest, this, &Konsole::ViewManager::newViewWithProfileRequest); connect(container, &Konsole::TabbedViewContainer::viewRemoved, this, &Konsole::ViewManager::viewDestroyed); connect(container, &Konsole::TabbedViewContainer::activeViewChanged, this, - &Konsole::ViewManager::viewActivated); + &Konsole::ViewManager::activateView); return container; } void ViewManager::setNavigationMethod(NavigationMethod method) { Q_ASSERT(_actionCollection); if (_actionCollection == nullptr) { return; } KActionCollection *collection = _actionCollection; _navigationMethod = method; // FIXME: The following disables certain actions for the KPart that it // doesn't actually have a use for, to avoid polluting the action/shortcut // namespace of an application using the KPart (otherwise, a shortcut may // be in use twice, and the user gets to see an "ambiguous shortcut over- // load" error dialog). However, this approach sucks - it's the inverse of // what it should be. Rather than disabling actions not used by the KPart, // a method should be devised to only enable those that are used, perhaps // by using a separate action collection. const bool enable = (method != NoNavigation); auto enableAction = [&enable, &collection](const QString& actionName) { auto *action = collection->action(actionName); if (action != nullptr) { action->setEnabled(enable); } }; enableAction(QStringLiteral("next-view")); enableAction(QStringLiteral("previous-view")); enableAction(QStringLiteral("last-tab")); enableAction(QStringLiteral("last-used-tab")); enableAction(QStringLiteral("last-used-tab-reverse")); enableAction(QStringLiteral("split-view-left-right")); enableAction(QStringLiteral("split-view-top-bottom")); enableAction(QStringLiteral("rename-session")); enableAction(QStringLiteral("move-view-left")); enableAction(QStringLiteral("move-view-right")); } ViewManager::NavigationMethod ViewManager::navigationMethod() const { return _navigationMethod; } void ViewManager::containerViewsChanged(TabbedViewContainer *container) { Q_UNUSED(container); // TODO: Verify that this is right. emit viewPropertiesChanged(viewProperties()); } void ViewManager::viewDestroyed(QWidget *view) { // Note: the received QWidget has already been destroyed, so // using dynamic_cast<> or qobject_cast<> does not work here // We only need the pointer address to look it up below auto *display = reinterpret_cast(view); // 1. detach view from session // 2. if the session has no views left, close it Session *session = _sessionMap[ display ]; _sessionMap.remove(display); if (session != nullptr) { if (session->views().count() == 0) { session->close(); } } //we only update the focus if the splitter is still alive toggleActionsBasedOnState(); // The below causes the menus to be messed up // Only happens when using the tab bar close button // if (_pluggedController) // emit unplugController(_pluggedController); } TerminalDisplay *ViewManager::createTerminalDisplay(Session *session) { auto display = new TerminalDisplay(nullptr); display->setRandomSeed(session->sessionId() | (qApp->applicationPid() << 10)); connect(display, &TerminalDisplay::requestToggleExpansion, _viewContainer, &TabbedViewContainer::toggleMaximizeCurrentTerminal); return display; } const ColorScheme *ViewManager::colorSchemeForProfile(const Profile::Ptr &profile) { const ColorScheme *colorScheme = ColorSchemeManager::instance()-> findColorScheme(profile->colorScheme()); if (colorScheme == nullptr) { colorScheme = ColorSchemeManager::instance()->defaultColorScheme(); } Q_ASSERT(colorScheme); return colorScheme; } bool ViewManager::profileHasBlurEnabled(const Profile::Ptr &profile) { return colorSchemeForProfile(profile)->blur(); } void ViewManager::applyProfileToView(TerminalDisplay *view, const Profile::Ptr &profile) { Q_ASSERT(profile); view->applyProfile(profile); emit updateWindowIcon(); emit blurSettingChanged(view->colorScheme()->blur()); } void ViewManager::updateViewsForSession(Session *session) { const Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); const QList sessionMapKeys = _sessionMap.keys(session); foreach (TerminalDisplay *view, sessionMapKeys) { applyProfileToView(view, profile); } } void ViewManager::profileChanged(const Profile::Ptr &profile) { // update all views associated with this profile QHashIterator iter(_sessionMap); while (iter.hasNext()) { iter.next(); // if session uses this profile, update the display if (iter.key() != nullptr && iter.value() != nullptr && SessionManager::instance()->sessionProfile(iter.value()) == profile) { applyProfileToView(iter.key(), profile); } } } QList ViewManager::viewProperties() const { QList list; TabbedViewContainer *container = _viewContainer; if (container == nullptr) { return {}; } auto terminalContainers = _viewContainer->findChildren(); list.reserve(terminalContainers.size()); for(auto terminalDisplay : _viewContainer->findChildren()) { list.append(terminalDisplay->sessionController()); } return list; } namespace { QJsonObject saveSessionTerminal(TerminalDisplay *terminalDisplay) { QJsonObject thisTerminal; auto terminalSession = terminalDisplay->sessionController()->session(); const int sessionRestoreId = SessionManager::instance()->getRestoreId(terminalSession); thisTerminal.insert(QStringLiteral("SessionRestoreId"), sessionRestoreId); return thisTerminal; } QJsonObject saveSessionsRecurse(QSplitter *splitter) { QJsonObject thisSplitter; thisSplitter.insert( QStringLiteral("Orientation"), splitter->orientation() == Qt::Horizontal ? QStringLiteral("Horizontal") : QStringLiteral("Vertical") ); QJsonArray internalWidgets; for (int i = 0; i < splitter->count(); i++) { auto *widget = splitter->widget(i); auto *maybeSplitter = qobject_cast(widget); auto *maybeTerminalDisplay = qobject_cast(widget); if (maybeSplitter != nullptr) { internalWidgets.append(saveSessionsRecurse(maybeSplitter)); } else if (maybeTerminalDisplay != nullptr) { internalWidgets.append(saveSessionTerminal(maybeTerminalDisplay)); } } thisSplitter.insert(QStringLiteral("Widgets"), internalWidgets); return thisSplitter; } } // namespace void ViewManager::saveSessions(KConfigGroup &group) { QJsonArray rootArray; for(int i = 0; i < _viewContainer->count(); i++) { auto *splitter = qobject_cast(_viewContainer->widget(i)); rootArray.append(saveSessionsRecurse(splitter)); } group.writeEntry("Tabs", QJsonDocument(rootArray).toJson(QJsonDocument::Compact)); group.writeEntry("Active", _viewContainer->currentIndex()); } namespace { ViewSplitter *restoreSessionsSplitterRecurse(const QJsonObject& jsonSplitter, ViewManager *manager) { const QJsonArray splitterWidgets = jsonSplitter[QStringLiteral("Widgets")].toArray(); auto orientation = (jsonSplitter[QStringLiteral("Orientation")].toString() == QLatin1String("Horizontal")) ? Qt::Horizontal : Qt::Vertical; auto *currentSplitter = new ViewSplitter(); currentSplitter->setOrientation(orientation); for (const auto widgetJsonValue : splitterWidgets) { const auto widgetJsonObject = widgetJsonValue.toObject(); const auto sessionIterator = widgetJsonObject.constFind(QStringLiteral("SessionRestoreId")); if (sessionIterator != widgetJsonObject.constEnd()) { Session *session = SessionManager::instance()->idToSession(sessionIterator->toInt()); auto newView = manager->createView(session); currentSplitter->addWidget(newView); } else { auto nextSplitter = restoreSessionsSplitterRecurse(widgetJsonObject, manager); currentSplitter->addWidget(nextSplitter); } } return currentSplitter; } } // namespace void ViewManager::restoreSessions(const KConfigGroup &group) { const auto tabList = group.readEntry("Tabs", QByteArray("[]")); const auto jsonTabs = QJsonDocument::fromJson(tabList).array(); for (const auto& jsonSplitter : jsonTabs) { auto topLevelSplitter = restoreSessionsSplitterRecurse(jsonSplitter.toObject(), this); _viewContainer->addSplitter(topLevelSplitter, _viewContainer->count()); } if (!jsonTabs.isEmpty()) return; // Session file is unusable, try older format QList ids = group.readEntry("Sessions", QList()); int activeTab = group.readEntry("Active", 0); TerminalDisplay *display = nullptr; int tab = 1; foreach (int id, ids) { Session *session = SessionManager::instance()->idToSession(id); if (session == nullptr) { qWarning() << "Unable to load session with id" << id; // Force a creation of a default session below ids.clear(); break; } activeContainer()->addView(createView(session)); if (!session->isRunning()) { session->run(); } if (tab++ == activeTab) { display = qobject_cast(activeView()); } } if (display != nullptr) { activeContainer()->setCurrentWidget(display); display->setFocus(Qt::OtherFocusReason); } if (ids.isEmpty()) { // Session file is unusable, start default Profile Profile::Ptr profile = ProfileManager::instance()->defaultProfile(); Session *session = SessionManager::instance()->createSession(profile); activeContainer()->addView(createView(session)); if (!session->isRunning()) { session->run(); } } } TabbedViewContainer *ViewManager::activeContainer() { return _viewContainer; } int ViewManager::sessionCount() { return _sessionMap.size(); } QStringList ViewManager::sessionList() { QStringList ids; QHash::const_iterator i; for (i = _sessionMap.constBegin(); i != _sessionMap.constEnd(); ++i) { ids.append(QString::number(i.value()->sessionId())); } return ids; } int ViewManager::currentSession() { if (_pluggedController) { Q_ASSERT(_pluggedController->session() != nullptr); return _pluggedController->session()->sessionId(); } return -1; } void ViewManager::setCurrentSession(int sessionId) { auto *session = SessionManager::instance()->idToSession(sessionId); if (session == nullptr || session->views().count() == 0) { return; } auto *display = session->views().at(0); if (display != nullptr) { display->setFocus(Qt::OtherFocusReason); } } int ViewManager::newSession() { return newSession(QString(), QString()); } int ViewManager::newSession(const QString &profile) { return newSession(profile, QString()); } int ViewManager::newSession(const QString &profile, const QString &directory) { Profile::Ptr profileptr = ProfileManager::instance()->defaultProfile(); if(!profile.isEmpty()) { const QList profilelist = ProfileManager::instance()->allProfiles(); for (const auto &i : profilelist) { if (i->name() == profile) { profileptr = i; break; } } } Session *session = createSession(profileptr, directory); auto newView = createView(session); activeContainer()->addView(newView); session->run(); return session->sessionId(); } QString ViewManager::defaultProfile() { return ProfileManager::instance()->defaultProfile()->name(); } QStringList ViewManager::profileList() { return ProfileManager::instance()->availableProfileNames(); } void ViewManager::nextSession() { nextView(); } void ViewManager::prevSession() { previousView(); } void ViewManager::moveSessionLeft() { moveActiveViewLeft(); } void ViewManager::moveSessionRight() { moveActiveViewRight(); } void ViewManager::setTabWidthToText(bool setTabWidthToText) { _viewContainer->tabBar()->setExpanding(!setTabWidthToText); _viewContainer->tabBar()->update(); } void ViewManager::setNavigationVisibility(NavigationVisibility navigationVisibility) { if (_navigationVisibility != navigationVisibility) { _navigationVisibility = navigationVisibility; _viewContainer->setNavigationVisibility(navigationVisibility); } } void ViewManager::updateTerminalDisplayHistory(TerminalDisplay* terminalDisplay, bool remove) { if (terminalDisplay == nullptr) { if (_terminalDisplayHistoryIndex >= 0) { // This is the case when we finished walking through the history // (i.e. when Ctrl-Tab has been released) terminalDisplay = _terminalDisplayHistory[_terminalDisplayHistoryIndex]; _terminalDisplayHistoryIndex = -1; } else { return; } } if (_terminalDisplayHistoryIndex >= 0 && !remove) { // Do not reorder the tab history while we are walking through it return; } for (int i = 0; i < _terminalDisplayHistory.count(); i++) { if (_terminalDisplayHistory[i] == terminalDisplay) { _terminalDisplayHistory.removeAt(i); if (!remove) { _terminalDisplayHistory.prepend(terminalDisplay); } break; } } } diff --git a/src/ViewManager.h b/src/ViewManager.h index cded6a38..3038027b 100644 --- a/src/ViewManager.h +++ b/src/ViewManager.h @@ -1,447 +1,447 @@ /* Copyright 2006-2008 by Robert Knight 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 VIEWMANAGER_H #define VIEWMANAGER_H // Qt #include #include #include #include // Konsole #include "Profile.h" class KActionCollection; class KConfigGroup; namespace Konsole { class ColorScheme; class Session; class TerminalDisplay; class TabbedViewContainer; class SessionController; class ViewProperties; class ViewSplitter; class TabbedViewContainer; /** * Manages the terminal display widgets in a Konsole window or part. * * When a view manager is created, it constructs a tab widget ( accessed via * widget() ) to hold one or more view splitters. Each view splitter holds * one or more terminal displays and splitters. * * The view manager provides menu actions ( defined in the 'konsoleui.rc' XML file ) * to manipulate the views and view containers - for example, actions to split the view * left/right or top/bottom, detach a view from the current window and navigate between * views and containers. These actions are added to the collection specified in the * ViewManager's constructor. * * The view manager provides facilities to construct display widgets for a terminal * session and also to construct the SessionController which provides the menus and other * user interface elements specific to each display/session pair. * */ class KONSOLEPRIVATE_EXPORT ViewManager : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.konsole.Window") public: /** * Constructs a new view manager with the specified @p parent. * View-related actions defined in 'konsoleui.rc' are created * and added to the specified @p collection. */ ViewManager(QObject *parent, KActionCollection *collection); ~ViewManager() Q_DECL_OVERRIDE; /** * Creates a new view to display the output from and deliver input to @p session. * Constructs a new container to hold the views if no container has yet been created. */ void createView(TabbedViewContainer *tabWidget, Session *session); /* * Applies the view-specific settings associated with specified @p profile * to the terminal display @p view. */ void applyProfileToView(TerminalDisplay *view, const Profile::Ptr &profile); void toggleActionsBasedOnState(); /** * Return the main widget for the view manager which * holds all of the views managed by this ViewManager instance. */ QWidget *widget() const; /** * Returns the view manager's active view. */ QWidget *activeView() const; /** * Returns the list of view properties for views in the active container. * Each view widget is associated with a ViewProperties instance which * provides access to basic information about the session being * displayed in the view, such as title, current directory and * associated icon. */ QList viewProperties() const; /** * This enum describes the available types of navigation widget * which newly created containers can provide to allow navigation * between open sessions. */ enum NavigationMethod { /** * Each container has a row of tabs (one per session) which the user * can click on to navigate between open sessions. */ TabbedNavigation, /** The container has no navigation widget. */ NoNavigation }; /** * Describes the options for showing or hiding the container's navigation widget. */ enum NavigationVisibility { NavigationNotSet, // Don't rely on this information, Only use the settings. AlwaysShowNavigation, ShowNavigationAsNeeded, AlwaysHideNavigation }; /** * Sets the visibility of the view container's navigation widget. * The ViewContainer subclass is responsible for ensuring that this * setting is respected as views are dded or removed from the container */ void setNavigationVisibility(NavigationVisibility navigationVisibility); /** Returns the current mode for controlling the visibility of the * view container's navigation widget. */ NavigationVisibility navigationVisibility() const; /** * Sets the type of widget provided to navigate between open sessions * in a container. The changes will only apply to newly created containers. * * The default method is TabbedNavigation. To disable navigation widgets, call * setNavigationMethod(ViewManager::NoNavigation) before creating any sessions. */ void setNavigationMethod(NavigationMethod method); /** * Returns the type of navigation widget created in new containers. * See setNavigationMethod() */ NavigationMethod navigationMethod() const; /** * Returns the controller for the active view. activeViewChanged() is * emitted when this changes. */ SessionController *activeViewController() const; /** * Session management */ void saveSessions(KConfigGroup &group); void restoreSessions(const KConfigGroup &group); int managerId() const; /** Returns a list of sessions in this ViewManager */ QList sessions() { return _sessionMap.values(); } /** * Returns whether the @p profile has the blur setting enabled */ static bool profileHasBlurEnabled(const Profile::Ptr &profile); /** returns the active tab from the view */ TabbedViewContainer *activeContainer(); TerminalDisplay *createView(Session *session); void attachView(TerminalDisplay *terminal, Session *session); static const ColorScheme *colorSchemeForProfile(const Profile::Ptr &profile); /** Reorder the terminal display history list */ void updateTerminalDisplayHistory(TerminalDisplay *terminalDisplay = nullptr, bool remove = false); QHash forgetAll(ViewSplitter* splitter); Session* forgetTerminal(TerminalDisplay* terminal); /** * Creates and returns new session * * The session has specified @p profile, working @p directory * and configured environment. */ Session* createSession(const Profile::Ptr &profile, const QString &directory = QString()); Q_SIGNALS: /** Emitted when the last view is removed from the view manager */ void empty(); /** Emitted when a session is detached from a view owned by this ViewManager */ void terminalsDetached(ViewSplitter *splitter, QHash sessionsMap); /** * Emitted when the active view changes. * @param controller The controller associated with the active view */ void activeViewChanged(SessionController *controller); /** * Emitted when the current session needs unplugged from factory(). * @param controller The controller associated with the active view */ void unplugController(SessionController *controller); /** * Emitted when the list of view properties ( as returned by viewProperties() ) changes. * This occurs when views are added to or removed from the active container, or * if the active container is changed. */ void viewPropertiesChanged(const QList &propertiesList); /** * Emitted when menu bar visibility changes because a profile that requires so is * activated. */ void setMenuBarVisibleRequest(bool); void updateWindowIcon(); void blurSettingChanged(bool); /** Requests creation of a new view with the default profile. */ void newViewRequest(); /** Requests creation of a new view, with the selected profile. */ void newViewWithProfileRequest(const Profile::Ptr&); public Q_SLOTS: /** DBus slot that returns the number of sessions in the current view. */ Q_SCRIPTABLE int sessionCount(); /** * DBus slot that returns the unique ids of the sessions in the * current view. The returned list is not sorted. * QList is not printable by qdbus so we use QStringList */ Q_SCRIPTABLE QStringList sessionList(); /** DBus slot that returns the current (active) session window */ Q_SCRIPTABLE int currentSession(); /** DBus slot that sets the current (active) session window */ Q_SCRIPTABLE void setCurrentSession(int sessionId); /** DBus slot that creates a new session in the current view with the associated * default profile and the default working directory */ Q_SCRIPTABLE int newSession(); /** DBus slot that creates a new session in the current view. * @param profile the name of the profile to be used * started. */ Q_SCRIPTABLE int newSession(const QString &profile); /** DBus slot that creates a new session in the current view. * @param profile the name of the profile to be used * @param directory the working directory where the session is * started. */ Q_SCRIPTABLE int newSession(const QString &profile, const QString &directory); // TODO: its semantic is application-wide. Move it to more appropriate place // DBus slot that returns the name of default profile Q_SCRIPTABLE QString defaultProfile(); // TODO: its semantic is application-wide. Move it to more appropriate place // DBus slot that returns a string list of defined (known) profiles Q_SCRIPTABLE QStringList profileList(); /** DBus slot that changes the view port to the next session */ Q_SCRIPTABLE void nextSession(); /** DBus slot that changes the view port to the previous session */ Q_SCRIPTABLE void prevSession(); /** DBus slot that switches the current session (as returned by * currentSession()) with the left (or previous) one in the * navigation tab. */ Q_SCRIPTABLE void moveSessionLeft(); /** DBus slot that Switches the current session (as returned by * currentSession()) with the right (or next) one in the navigation * tab. */ Q_SCRIPTABLE void moveSessionRight(); /** DBus slot that sets ALL tabs' width to match their text */ Q_SCRIPTABLE void setTabWidthToText(bool); private Q_SLOTS: // called when the "Split View Left/Right" menu item is selected void splitLeftRight(); void splitTopBottom(); void expandActiveContainer(); void shrinkActiveContainer(); // called when the "Detach View" menu item is selected void detachActiveView(); void detachActiveTab(); // called when a session terminates - the view manager will delete any // views associated with the session void sessionFinished(); // called when one view has been destroyed void viewDestroyed(QWidget *view); // controller detects when an associated view is given the focus // and emits a signal. ViewManager listens for that signal // and then plugs the action into the UI //void viewFocused( SessionController* controller ); // called when the active view in a ViewContainer changes, so // that we can plug the appropriate actions into the UI - void viewActivated(TerminalDisplay *view); + void activateView(TerminalDisplay *view); void focusUp(); void focusDown(); void focusLeft(); void focusRight(); // called when "Next View" shortcut is activated void nextView(); // called when "Previous View" shortcut is activated void previousView(); // called when "Switch to last tab" shortcut is activated void lastView(); // called when "Switch to last used tab" shortcut is activated void lastUsedView(); // called when "Switch to last used tab (reverse)" shortcut is activated void lastUsedViewReverse(); // called when "Next View Container" shortcut is activated void nextContainer(); // called when "Toggle Two tabs" shortcut is activated void toggleTwoViews(); // called when the views in a container owned by this view manager // changes void containerViewsChanged(TabbedViewContainer *container); // called when a profile changes void profileChanged(const Profile::Ptr &profile); void updateViewsForSession(Session *session); // moves active view to the left void moveActiveViewLeft(); // moves active view to the right void moveActiveViewRight(); // switches to the view at visual position 'index' // in the current container void switchToView(int index); // gives focus and switches the terminal display, changing tab if needed void switchToTerminalDisplay(TerminalDisplay *terminalDisplay); // called when a SessionController gains focus void controllerChanged(SessionController *controller); /* Detaches the tab at index tabIdx */ void detachTab(int tabIdx); private: Q_DISABLE_COPY(ViewManager) void createView(Session *session, TabbedViewContainer *container, int index); void setupActions(); // takes a view from a view container owned by a different manager and places it in // newContainer owned by this manager void takeView(ViewManager *otherManager, TabbedViewContainer *otherContainer, TabbedViewContainer *newContainer, TerminalDisplay *view); void splitView(Qt::Orientation orientation); // creates a new container which can hold terminal displays TabbedViewContainer *createContainer(); // creates a new terminal display // the 'session' is used so that the terminal display's random seed // can be set to something which depends uniquely on that session TerminalDisplay *createTerminalDisplay(Session *session = nullptr); // creates a new controller for a session/display pair which provides the menu // actions associated with that view, and exposes basic information // about the session ( such as title and associated icon ) to the display. SessionController *createController(Session *session, TerminalDisplay *view); void removeController(SessionController* controller); // Activates a different terminal when the TerminalDisplay // closes or is detached and another one should be focused. // It will activate the last used terminal within the same splitView // if possible otherwise it will focus the last used tab void focusAnotherTerminal(ViewSplitter *toplevelSplitter); void activateLastUsedView(bool reverse); private: QPointer _viewContainer; QPointer _pluggedController; QHash _sessionMap; KActionCollection *_actionCollection; NavigationMethod _navigationMethod; NavigationVisibility _navigationVisibility; int _managerId; static int lastManagerId; QList _terminalDisplayHistory; int _terminalDisplayHistoryIndex; // List of actions that should only be enabled when there are multiple view // containers open QList _multiTabOnlyActions; QList _multiSplitterOnlyActions; }; } #endif diff --git a/src/Vt102Emulation.cpp b/src/Vt102Emulation.cpp index 5fe95206..892fa650 100644 --- a/src/Vt102Emulation.cpp +++ b/src/Vt102Emulation.cpp @@ -1,1543 +1,1529 @@ /* This file is part of Konsole, an X terminal. Copyright 2007-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle 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 "Vt102Emulation.h" // Standard #include #include // Qt #include #include #include // KDE #include // Konsole #include "KeyboardTranslator.h" #include "SessionController.h" #include "TerminalDisplay.h" using Konsole::Vt102Emulation; /* The VT100 has 32 special graphical characters. The usual vt100 extended xterm fonts have these at 0x00..0x1f. QT's iso mapping leaves 0x00..0x7f without any changes. But the graphicals come in here as proper unicode characters. We treat non-iso10646 fonts as VT100 extended and do the required mapping from unicode to 0x00..0x1f. The remaining translation is then left to the QCodec. */ // assert for i in [0..31] : vt100extended(vt100_graphics[i]) == i. unsigned short Konsole::vt100_graphics[32] = { // 0/8 1/9 2/10 3/11 4/12 5/13 6/14 7/15 0x0020, 0x25C6, 0x2592, 0x2409, 0x240c, 0x240d, 0x240a, 0x00b0, 0x00b1, 0x2424, 0x240b, 0x2518, 0x2510, 0x250c, 0x2514, 0x253c, 0xF800, 0xF801, 0x2500, 0xF803, 0xF804, 0x251c, 0x2524, 0x2534, 0x252c, 0x2502, 0x2264, 0x2265, 0x03C0, 0x2260, 0x00A3, 0x00b7 }; Vt102Emulation::Vt102Emulation() : Emulation(), _currentModes(TerminalState()), _savedModes(TerminalState()), _pendingSessionAttributesUpdates(QHash()), _sessionAttributesUpdateTimer(new QTimer(this)), _reportFocusEvents(false) { _sessionAttributesUpdateTimer->setSingleShot(true); QObject::connect(_sessionAttributesUpdateTimer, &QTimer::timeout, this, &Konsole::Vt102Emulation::updateSessionAttributes); initTokenizer(); reset(); } Vt102Emulation::~Vt102Emulation() = default; void Vt102Emulation::clearEntireScreen() { _currentScreen->clearEntireScreen(); bufferedUpdate(); } void Vt102Emulation::reset() { // Save the current codec so we can set it later. // Ideally we would want to use the profile setting const QTextCodec *currentCodec = codec(); resetTokenizer(); resetModes(); resetCharset(0); _screen[0]->reset(); resetCharset(1); _screen[1]->reset(); if (currentCodec != nullptr) { setCodec(currentCodec); } else { setCodec(LocaleCodec); } emit resetCursorStyleRequest(); bufferedUpdate(); } /* ------------------------------------------------------------------------- */ /* */ /* Processing the incoming byte stream */ /* */ /* ------------------------------------------------------------------------- */ /* Incoming Bytes Event pipeline This section deals with decoding the incoming character stream. Decoding means here, that the stream is first separated into `tokens' which are then mapped to a `meaning' provided as operations by the `Screen' class or by the emulation class itself. The pipeline proceeds as follows: - Tokenizing the ESC codes (onReceiveChar) - VT100 code page translation of plain characters (applyCharset) - Interpretation of ESC codes (processToken) The escape codes and their meaning are described in the technical reference of this program. */ // Tokens ------------------------------------------------------------------ -- /* Since the tokens are the central notion if this section, we've put them in front. They provide the syntactical elements used to represent the terminals operations as byte sequences. They are encodes here into a single machine word, so that we can later switch over them easily. Depending on the token itself, additional argument variables are filled with parameter values. The tokens are defined below: - CHR - Printable characters (32..255 but DEL (=127)) - CTL - Control characters (0..31 but ESC (= 27), DEL) - ESC - Escape codes of the form - ESC_DE - Escape codes of the form C - CSI_PN - Escape codes of the form '[' {Pn} ';' {Pn} C - CSI_PS - Escape codes of the form '[' {Pn} ';' ... C - CSI_PR - Escape codes of the form '[' '?' {Pn} ';' ... C - CSI_PE - Escape codes of the form '[' '!' {Pn} ';' ... C - CSI_SP - Escape codes of the form '[' ' ' C (3rd field is a space) - CSI_PSP - Escape codes of the form '[' '{Pn}' ' ' C (4th field is a space) - VT52 - VT52 escape codes - - 'Y'{Pc}{Pc} - XTE_HA - Xterm window/terminal attribute commands of the form `]' {Pn} `;' {Text} (Note that these are handled differently to the other formats) The last two forms allow list of arguments. Since the elements of the lists are treated individually the same way, they are passed as individual tokens to the interpretation. Further, because the meaning of the parameters are names (although represented as numbers), they are includes within the token ('N'). */ constexpr int token_construct(int t, int a, int n) { return (((n & 0xffff) << 16) | ((a & 0xff) << 8) | (t & 0xff)); } constexpr int token_chr() { return token_construct(0, 0, 0); } constexpr int token_ctl(int a) { return token_construct(1, a, 0); } constexpr int token_esc(int a) { return token_construct(2, a, 0); } constexpr int token_esc_cs(int a, int b) { return token_construct(3, a, b); } constexpr int token_esc_de(int a) { return token_construct(4, a, 0); } constexpr int token_csi_ps(int a, int n) { return token_construct(5, a, n); } constexpr int token_csi_pn(int a) { return token_construct(6, a, 0); } constexpr int token_csi_pr(int a, int n) { return token_construct(7, a, n); } constexpr int token_vt52(int a) { return token_construct(8, a, 0); } constexpr int token_csi_pg(int a) { return token_construct(9, a, 0); } constexpr int token_csi_pe(int a) { return token_construct(10, a, 0); } constexpr int token_csi_sp(int a) { return token_construct(11, a, 0); } constexpr int token_csi_psp(int a, int n) { return token_construct(12, a, n); } const int MAX_ARGUMENT = 4096; // Tokenizer --------------------------------------------------------------- -- /* The tokenizer's state The state is represented by the buffer (tokenBuffer, tokenBufferPos), and accompanied by decoded arguments kept in (argv,argc). Note that they are kept internal in the tokenizer. */ void Vt102Emulation::resetTokenizer() { tokenBufferPos = 0; argc = 0; argv[0] = 0; argv[1] = 0; } void Vt102Emulation::addDigit(int digit) { if (argv[argc] < MAX_ARGUMENT) { argv[argc] = 10 * argv[argc] + digit; } } void Vt102Emulation::addArgument() { argc = qMin(argc + 1, MAXARGS - 1); argv[argc] = 0; } void Vt102Emulation::addToCurrentToken(uint cc) { tokenBuffer[tokenBufferPos] = cc; tokenBufferPos = qMin(tokenBufferPos + 1, MAX_TOKEN_LENGTH - 1); } // Character Class flags used while decoding const int CTL = 1; // Control character const int CHR = 2; // Printable character const int CPN = 4; // TODO: Document me const int DIG = 8; // Digit const int SCS = 16; // Select Character Set const int GRP = 32; // TODO: Document me const int CPS = 64; // Character which indicates end of window resize void Vt102Emulation::initTokenizer() { int i; quint8 *s; for (i = 0; i < 256; ++i) { charClass[i] = 0; } for (i = 0; i < 32; ++i) { charClass[i] |= CTL; } for (i = 32; i < 256; ++i) { charClass[i] |= CHR; } for (s = (quint8 *)"@ABCDGHILMPSTXZbcdfry"; *s != 0u; ++s) { charClass[*s] |= CPN; } // resize = \e[8;;t for (s = (quint8 *)"t"; *s != 0u; ++s) { charClass[*s] |= CPS; } for (s = (quint8 *)"0123456789"; *s != 0u; ++s) { charClass[*s] |= DIG; } for (s = (quint8 *)"()+*%"; *s != 0u; ++s) { charClass[*s] |= SCS; } for (s = (quint8 *)"()+*#[]%"; *s != 0u; ++s) { charClass[*s] |= GRP; } resetTokenizer(); } /* Ok, here comes the nasty part of the decoder. Instead of keeping an explicit state, we deduce it from the token scanned so far. It is then immediately combined with the current character to form a scanning decision. This is done by the following defines. - P is the length of the token scanned so far. - L (often P-1) is the position on which contents we base a decision. - C is a character or a group of characters (taken from 'charClass'). - 'cc' is the current character - 's' is a pointer to the start of the token buffer - 'p' is the current position within the token buffer Note that they need to applied in proper order. */ #define lec(P,L,C) (p == (P) && s[(L)] == (C)) #define lun( ) (p == 1 && cc >= 32 ) #define les(P,L,C) (p == (P) && s[L] < 256 && (charClass[s[(L)]] & (C)) == (C)) #define eec(C) (p >= 3 && cc == (C)) #define ees(C) (p >= 3 && cc < 256 && (charClass[cc] & (C)) == (C)) #define eps(C) (p >= 3 && s[2] != '?' && s[2] != '!' && s[2] != '>' && cc < 256 && (charClass[cc] & (C)) == (C)) #define epp( ) (p >= 3 && s[2] == '?') #define epe( ) (p >= 3 && s[2] == '!') #define egt( ) (p >= 3 && s[2] == '>') #define esp( ) (p >= 4 && s[2] == SP ) #define epsp( ) (p >= 5 && s[3] == SP ) #define osc (tokenBufferPos >= 2 && tokenBuffer[1] == ']') #define ces(C) (cc < 256 && (charClass[cc] & (C)) == (C)) #define dcs (p >= 2 && s[0] == ESC && s[1] == 'P') #define CNTL(c) ((c)-'@') const int ESC = 27; const int DEL = 127; const int SP = 32; // process an incoming unicode character void Vt102Emulation::receiveChar(uint cc) { if (cc == DEL) { return; //VT100: ignore. } if (ces(CTL)) { // ignore control characters in the text part of osc (aka OSC) "ESC]" // escape sequences; this matches what XTERM docs say // Allow BEL and ESC here, it will either end the text or be removed later. if (osc && cc != 0x1b && cc != 0x07) { return; } if (!osc) { // DEC HACK ALERT! Control Characters are allowed *within* esc sequences in VT100 // This means, they do neither a resetTokenizer() nor a pushToToken(). Some of them, do // of course. Guess this originates from a weakly layered handling of the X-on // X-off protocol, which comes really below this level. if (cc == CNTL('X') || cc == CNTL('Z') || cc == ESC) { resetTokenizer(); //VT100: CAN or SUB } if (cc != ESC) { processToken(token_ctl(cc+'@'), 0, 0); return; } } } // advance the state addToCurrentToken(cc); uint* s = tokenBuffer; const int p = tokenBufferPos; if (getMode(MODE_Ansi)) { if (lec(1,0,ESC)) { return; } if (lec(1,0,ESC+128)) { s[0] = ESC; receiveChar('['); return; } if (les(2,1,GRP)) { return; } // Operating System Command if (p > 2 && s[1] == ']') { // ']' ... '\' if (s[p-2] == ESC && s[p-1] == '\\') { processSessionAttributeRequest(p-1); resetTokenizer(); return; } // ']' ... + one character for reprocessing if (s[p-2] == ESC) { processSessionAttributeRequest(p-1); resetTokenizer(); receiveChar(cc); return; } // ']' ... if (s[p-1] == 0x07) { processSessionAttributeRequest(p); resetTokenizer(); return; } } // ']' ... if (osc ) { return; } if (lec(3,2,'?')) { return; } if (lec(3,2,'>')) { return; } if (lec(3,2,'!')) { return; } if (lec(3,2,SP )) { return; } if (lec(4,3,SP )) { return; } if (lun( )) { processToken(token_chr(), applyCharset(cc), 0); resetTokenizer(); return; } if (dcs ) { return; /* TODO We don't xterm DCS, so we just eat it */ } if (lec(2,0,ESC)) { processToken(token_esc(s[1]), 0, 0); resetTokenizer(); return; } if (les(3,1,SCS)) { processToken(token_esc_cs(s[1],s[2]), 0, 0); resetTokenizer(); return; } if (lec(3,1,'#')) { processToken(token_esc_de(s[2]), 0, 0); resetTokenizer(); return; } if (eps( CPN)) { processToken(token_csi_pn(cc), argv[0],argv[1]); resetTokenizer(); return; } // resize = \e[8;;t if (eps(CPS)) { processToken(token_csi_ps(cc, argv[0]), argv[1], argv[2]); resetTokenizer(); return; } if (epe( )) { processToken(token_csi_pe(cc), 0, 0); resetTokenizer(); return; } if (esp ( )) { processToken(token_csi_sp(cc), 0, 0); resetTokenizer(); return; } if (epsp( )) { processToken(token_csi_psp(cc, argv[0]), 0, 0); resetTokenizer(); return; } if (ees(DIG)) { addDigit(cc-'0'); return; } if (eec(';')) { addArgument(); return; } for (int i = 0; i <= argc; i++) { if (epp()) { processToken(token_csi_pr(cc,argv[i]), 0, 0); } else if (egt()) { processToken(token_csi_pg(cc), 0, 0); // spec. case for ESC]>0c or ESC]>c } else if (cc == 'm' && argc - i >= 4 && (argv[i] == 38 || argv[i] == 48) && argv[i+1] == 2) { // ESC[ ... 48;2;;; ... m -or- ESC[ ... 38;2;;; ... m i += 2; processToken(token_csi_ps(cc, argv[i-2]), COLOR_SPACE_RGB, (argv[i] << 16) | (argv[i+1] << 8) | argv[i+2]); i += 2; } else if (cc == 'm' && argc - i >= 2 && (argv[i] == 38 || argv[i] == 48) && argv[i+1] == 5) { // ESC[ ... 48;5; ... m -or- ESC[ ... 38;5; ... m i += 2; processToken(token_csi_ps(cc, argv[i-2]), COLOR_SPACE_256, argv[i]); } else { processToken(token_csi_ps(cc,argv[i]), 0, 0); } } resetTokenizer(); } else { // VT52 Mode if (lec(1,0,ESC)) { return; } if (les(1,0,CHR)) { processToken(token_chr(), s[0], 0); resetTokenizer(); return; } if (lec(2,1,'Y')) { return; } if (lec(3,1,'Y')) { return; } if (p < 4) { processToken(token_vt52(s[1] ), 0, 0); resetTokenizer(); return; } processToken(token_vt52(s[1]), s[2], s[3]); resetTokenizer(); return; } } void Vt102Emulation::processSessionAttributeRequest(int tokenSize) { // Describes the window or terminal session attribute to change // See Session::SessionAttributes for possible values int attribute = 0; int i; // ignore last character (ESC or BEL) --tokenSize; // skip first two characters (ESC, ']') for (i = 2; i < tokenSize && tokenBuffer[i] >= '0' && tokenBuffer[i] <= '9'; i++) { attribute = 10 * attribute + (tokenBuffer[i]-'0'); } if (tokenBuffer[i] != ';') { reportDecodingError(); return; } // skip ';' ++i; const QString value = QString::fromUcs4(&tokenBuffer[i], tokenSize - i); if (value == QLatin1String("?")) { emit sessionAttributeRequest(attribute); return; } _pendingSessionAttributesUpdates[attribute] = value; _sessionAttributesUpdateTimer->start(20); } void Vt102Emulation::updateSessionAttributes() { QListIterator iter(_pendingSessionAttributesUpdates.keys()); while (iter.hasNext()) { int arg = iter.next(); emit sessionAttributeChanged(arg , _pendingSessionAttributesUpdates[arg]); } _pendingSessionAttributesUpdates.clear(); } // Interpreting Codes --------------------------------------------------------- /* Now that the incoming character stream is properly tokenized, meaning is assigned to them. These are either operations of the current _screen, or of the emulation class itself. The token to be interpreted comes in as a machine word possibly accompanied by two parameters. Likewise, the operations assigned to, come with up to two arguments. One could consider to make up a proper table from the function below. The technical reference manual provides more information about this mapping. */ void Vt102Emulation::processToken(int token, int p, int q) { switch (token) { case token_chr( ) : _currentScreen->displayCharacter (p ); break; //UTF16 // 127 DEL : ignored on input case token_ctl('@' ) : /* NUL: ignored */ break; case token_ctl('A' ) : /* SOH: ignored */ break; case token_ctl('B' ) : /* STX: ignored */ break; case token_ctl('C' ) : /* ETX: ignored */ break; case token_ctl('D' ) : /* EOT: ignored */ break; case token_ctl('E' ) : reportAnswerBack ( ); break; //VT100 case token_ctl('F' ) : /* ACK: ignored */ break; case token_ctl('G' ) : emit stateSet(NOTIFYBELL); break; //VT100 case token_ctl('H' ) : _currentScreen->backspace ( ); break; //VT100 case token_ctl('I' ) : _currentScreen->tab ( ); break; //VT100 case token_ctl('J' ) : _currentScreen->newLine ( ); break; //VT100 case token_ctl('K' ) : _currentScreen->newLine ( ); break; //VT100 case token_ctl('L' ) : _currentScreen->newLine ( ); break; //VT100 case token_ctl('M' ) : _currentScreen->toStartOfLine ( ); break; //VT100 case token_ctl('N' ) : useCharset ( 1); break; //VT100 case token_ctl('O' ) : useCharset ( 0); break; //VT100 case token_ctl('P' ) : /* DLE: ignored */ break; case token_ctl('Q' ) : /* DC1: XON continue */ break; //VT100 case token_ctl('R' ) : /* DC2: ignored */ break; case token_ctl('S' ) : /* DC3: XOFF halt */ break; //VT100 case token_ctl('T' ) : /* DC4: ignored */ break; case token_ctl('U' ) : /* NAK: ignored */ break; case token_ctl('V' ) : /* SYN: ignored */ break; case token_ctl('W' ) : /* ETB: ignored */ break; case token_ctl('X' ) : _currentScreen->displayCharacter ( 0x2592); break; //VT100 case token_ctl('Y' ) : /* EM : ignored */ break; case token_ctl('Z' ) : _currentScreen->displayCharacter ( 0x2592); break; //VT100 case token_ctl('[' ) : /* ESC: cannot be seen here. */ break; case token_ctl('\\' ) : /* FS : ignored */ break; case token_ctl(']' ) : /* GS : ignored */ break; case token_ctl('^' ) : /* RS : ignored */ break; case token_ctl('_' ) : /* US : ignored */ break; case token_esc('D' ) : _currentScreen->index ( ); break; //VT100 case token_esc('E' ) : _currentScreen->nextLine ( ); break; //VT100 case token_esc('H' ) : _currentScreen->changeTabStop (true ); break; //VT100 case token_esc('M' ) : _currentScreen->reverseIndex ( ); break; //VT100 case token_esc('Z' ) : reportTerminalType ( ); break; case token_esc('c' ) : reset ( ); break; case token_esc('n' ) : useCharset ( 2); break; case token_esc('o' ) : useCharset ( 3); break; case token_esc('7' ) : saveCursor ( ); break; case token_esc('8' ) : restoreCursor ( ); break; case token_esc('=' ) : setMode (MODE_AppKeyPad); break; case token_esc('>' ) : resetMode (MODE_AppKeyPad); break; case token_esc('<' ) : setMode (MODE_Ansi ); break; //VT100 case token_esc_cs('(', '0') : setCharset (0, '0'); break; //VT100 case token_esc_cs('(', 'A') : setCharset (0, 'A'); break; //VT100 case token_esc_cs('(', 'B') : setCharset (0, 'B'); break; //VT100 case token_esc_cs(')', '0') : setCharset (1, '0'); break; //VT100 case token_esc_cs(')', 'A') : setCharset (1, 'A'); break; //VT100 case token_esc_cs(')', 'B') : setCharset (1, 'B'); break; //VT100 case token_esc_cs('*', '0') : setCharset (2, '0'); break; //VT100 case token_esc_cs('*', 'A') : setCharset (2, 'A'); break; //VT100 case token_esc_cs('*', 'B') : setCharset (2, 'B'); break; //VT100 case token_esc_cs('+', '0') : setCharset (3, '0'); break; //VT100 case token_esc_cs('+', 'A') : setCharset (3, 'A'); break; //VT100 case token_esc_cs('+', 'B') : setCharset (3, 'B'); break; //VT100 case token_esc_cs('%', 'G') : setCodec (Utf8Codec ); break; //LINUX case token_esc_cs('%', '@') : setCodec (LocaleCodec ); break; //LINUX case token_esc_de('3' ) : /* Double height line, top half */ _currentScreen->setLineProperty( LINE_DOUBLEWIDTH , true ); _currentScreen->setLineProperty( LINE_DOUBLEHEIGHT , true ); break; case token_esc_de('4' ) : /* Double height line, bottom half */ _currentScreen->setLineProperty( LINE_DOUBLEWIDTH , true ); _currentScreen->setLineProperty( LINE_DOUBLEHEIGHT , true ); break; case token_esc_de('5' ) : /* Single width, single height line*/ _currentScreen->setLineProperty( LINE_DOUBLEWIDTH , false); _currentScreen->setLineProperty( LINE_DOUBLEHEIGHT , false); break; case token_esc_de('6' ) : /* Double width, single height line*/ _currentScreen->setLineProperty( LINE_DOUBLEWIDTH , true); _currentScreen->setLineProperty( LINE_DOUBLEHEIGHT , false); break; case token_esc_de('8' ) : _currentScreen->helpAlign ( ); break; // resize = \e[8;;t case token_csi_ps('t', 8) : setImageSize( p /*lines */, q /* columns */ ); emit imageResizeRequest(QSize(q, p)); break; // change tab text color : \e[28;t color: 0-16,777,215 case token_csi_ps('t', 28) : emit changeTabTextColorRequest ( p ); break; case token_csi_ps('K', 0) : _currentScreen->clearToEndOfLine ( ); break; case token_csi_ps('K', 1) : _currentScreen->clearToBeginOfLine ( ); break; case token_csi_ps('K', 2) : _currentScreen->clearEntireLine ( ); break; case token_csi_ps('J', 0) : _currentScreen->clearToEndOfScreen ( ); break; case token_csi_ps('J', 1) : _currentScreen->clearToBeginOfScreen ( ); break; case token_csi_ps('J', 2) : _currentScreen->clearEntireScreen ( ); break; case token_csi_ps('J', 3) : clearHistory(); break; case token_csi_ps('g', 0) : _currentScreen->changeTabStop (false ); break; //VT100 case token_csi_ps('g', 3) : _currentScreen->clearTabStops ( ); break; //VT100 case token_csi_ps('h', 4) : _currentScreen-> setMode (MODE_Insert ); break; case token_csi_ps('h', 20) : setMode (MODE_NewLine ); break; case token_csi_ps('i', 0) : /* IGNORE: attached printer */ break; //VT100 case token_csi_ps('l', 4) : _currentScreen-> resetMode (MODE_Insert ); break; case token_csi_ps('l', 20) : resetMode (MODE_NewLine ); break; case token_csi_ps('s', 0) : saveCursor ( ); break; case token_csi_ps('u', 0) : restoreCursor ( ); break; case token_csi_ps('m', 0) : _currentScreen->setDefaultRendition ( ); break; case token_csi_ps('m', 1) : _currentScreen-> setRendition (RE_BOLD ); break; //VT100 case token_csi_ps('m', 2) : _currentScreen-> setRendition (RE_FAINT ); break; case token_csi_ps('m', 3) : _currentScreen-> setRendition (RE_ITALIC ); break; //VT100 case token_csi_ps('m', 4) : _currentScreen-> setRendition (RE_UNDERLINE); break; //VT100 case token_csi_ps('m', 5) : _currentScreen-> setRendition (RE_BLINK ); break; //VT100 case token_csi_ps('m', 7) : _currentScreen-> setRendition (RE_REVERSE ); break; case token_csi_ps('m', 8) : _currentScreen-> setRendition (RE_CONCEAL ); break; case token_csi_ps('m', 9) : _currentScreen-> setRendition (RE_STRIKEOUT); break; case token_csi_ps('m', 53) : _currentScreen-> setRendition (RE_OVERLINE ); break; case token_csi_ps('m', 10) : /* IGNORED: mapping related */ break; //LINUX case token_csi_ps('m', 11) : /* IGNORED: mapping related */ break; //LINUX case token_csi_ps('m', 12) : /* IGNORED: mapping related */ break; //LINUX case token_csi_ps('m', 21) : _currentScreen->resetRendition (RE_BOLD ); break; case token_csi_ps('m', 22) : _currentScreen->resetRendition (RE_BOLD ); _currentScreen->resetRendition (RE_FAINT ); break; case token_csi_ps('m', 23) : _currentScreen->resetRendition (RE_ITALIC ); break; //VT100 case token_csi_ps('m', 24) : _currentScreen->resetRendition (RE_UNDERLINE); break; case token_csi_ps('m', 25) : _currentScreen->resetRendition (RE_BLINK ); break; case token_csi_ps('m', 27) : _currentScreen->resetRendition (RE_REVERSE ); break; case token_csi_ps('m', 28) : _currentScreen->resetRendition (RE_CONCEAL ); break; case token_csi_ps('m', 29) : _currentScreen->resetRendition (RE_STRIKEOUT); break; case token_csi_ps('m', 55) : _currentScreen->resetRendition (RE_OVERLINE ); break; case token_csi_ps('m', 30) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 0); break; case token_csi_ps('m', 31) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 1); break; case token_csi_ps('m', 32) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 2); break; case token_csi_ps('m', 33) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 3); break; case token_csi_ps('m', 34) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 4); break; case token_csi_ps('m', 35) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 5); break; case token_csi_ps('m', 36) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 6); break; case token_csi_ps('m', 37) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 7); break; case token_csi_ps('m', 38) : _currentScreen->setForeColor (p, q); break; case token_csi_ps('m', 39) : _currentScreen->setForeColor (COLOR_SPACE_DEFAULT, 0); break; case token_csi_ps('m', 40) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 0); break; case token_csi_ps('m', 41) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 1); break; case token_csi_ps('m', 42) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 2); break; case token_csi_ps('m', 43) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 3); break; case token_csi_ps('m', 44) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 4); break; case token_csi_ps('m', 45) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 5); break; case token_csi_ps('m', 46) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 6); break; case token_csi_ps('m', 47) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 7); break; case token_csi_ps('m', 48) : _currentScreen->setBackColor (p, q); break; case token_csi_ps('m', 49) : _currentScreen->setBackColor (COLOR_SPACE_DEFAULT, 1); break; case token_csi_ps('m', 90) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 8); break; case token_csi_ps('m', 91) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 9); break; case token_csi_ps('m', 92) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 10); break; case token_csi_ps('m', 93) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 11); break; case token_csi_ps('m', 94) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 12); break; case token_csi_ps('m', 95) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 13); break; case token_csi_ps('m', 96) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 14); break; case token_csi_ps('m', 97) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 15); break; case token_csi_ps('m', 100) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 8); break; case token_csi_ps('m', 101) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 9); break; case token_csi_ps('m', 102) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 10); break; case token_csi_ps('m', 103) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 11); break; case token_csi_ps('m', 104) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 12); break; case token_csi_ps('m', 105) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 13); break; case token_csi_ps('m', 106) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 14); break; case token_csi_ps('m', 107) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 15); break; case token_csi_ps('n', 5) : reportStatus ( ); break; case token_csi_ps('n', 6) : reportCursorPosition ( ); break; case token_csi_ps('q', 0) : /* IGNORED: LEDs off */ break; //VT100 case token_csi_ps('q', 1) : /* IGNORED: LED1 on */ break; //VT100 case token_csi_ps('q', 2) : /* IGNORED: LED2 on */ break; //VT100 case token_csi_ps('q', 3) : /* IGNORED: LED3 on */ break; //VT100 case token_csi_ps('q', 4) : /* IGNORED: LED4 on */ break; //VT100 case token_csi_ps('x', 0) : reportTerminalParms ( 2); break; //VT100 case token_csi_ps('x', 1) : reportTerminalParms ( 3); break; //VT100 case token_csi_pn('@' ) : _currentScreen->insertChars (p ); break; case token_csi_pn('A' ) : _currentScreen->cursorUp (p ); break; //VT100 case token_csi_pn('B' ) : _currentScreen->cursorDown (p ); break; //VT100 case token_csi_pn('C' ) : _currentScreen->cursorRight (p ); break; //VT100 case token_csi_pn('D' ) : _currentScreen->cursorLeft (p ); break; //VT100 case token_csi_pn('E' ) : /* Not implemented: cursor next p lines */ break; //VT100 case token_csi_pn('F' ) : /* Not implemented: cursor preceding p lines */ break; //VT100 case token_csi_pn('G' ) : _currentScreen->setCursorX (p ); break; //LINUX case token_csi_pn('H' ) : _currentScreen->setCursorYX (p, q); break; //VT100 case token_csi_pn('I' ) : _currentScreen->tab (p ); break; case token_csi_pn('L' ) : _currentScreen->insertLines (p ); break; case token_csi_pn('M' ) : _currentScreen->deleteLines (p ); break; case token_csi_pn('P' ) : _currentScreen->deleteChars (p ); break; case token_csi_pn('S' ) : _currentScreen->scrollUp (p ); break; case token_csi_pn('T' ) : _currentScreen->scrollDown (p ); break; case token_csi_pn('X' ) : _currentScreen->eraseChars (p ); break; case token_csi_pn('Z' ) : _currentScreen->backtab (p ); break; case token_csi_pn('b' ) : _currentScreen->repeatChars (p ); break; case token_csi_pn('c' ) : reportTerminalType ( ); break; //VT100 case token_csi_pn('d' ) : _currentScreen->setCursorY (p ); break; //LINUX case token_csi_pn('f' ) : _currentScreen->setCursorYX (p, q); break; //VT100 case token_csi_pn('r' ) : setMargins (p, q); break; //VT100 case token_csi_pn('y' ) : /* IGNORED: Confidence test */ break; //VT100 case token_csi_pr('h', 1) : setMode (MODE_AppCuKeys); break; //VT100 case token_csi_pr('l', 1) : resetMode (MODE_AppCuKeys); break; //VT100 case token_csi_pr('s', 1) : saveMode (MODE_AppCuKeys); break; //FIXME case token_csi_pr('r', 1) : restoreMode (MODE_AppCuKeys); break; //FIXME case token_csi_pr('l', 2) : resetMode (MODE_Ansi ); break; //VT100 case token_csi_pr('h', 3) : setMode (MODE_132Columns); break; //VT100 case token_csi_pr('l', 3) : resetMode (MODE_132Columns); break; //VT100 case token_csi_pr('h', 4) : /* IGNORED: soft scrolling */ break; //VT100 case token_csi_pr('l', 4) : /* IGNORED: soft scrolling */ break; //VT100 case token_csi_pr('h', 5) : _currentScreen-> setMode (MODE_Screen ); break; //VT100 case token_csi_pr('l', 5) : _currentScreen-> resetMode (MODE_Screen ); break; //VT100 case token_csi_pr('h', 6) : _currentScreen-> setMode (MODE_Origin ); break; //VT100 case token_csi_pr('l', 6) : _currentScreen-> resetMode (MODE_Origin ); break; //VT100 case token_csi_pr('s', 6) : _currentScreen-> saveMode (MODE_Origin ); break; //FIXME case token_csi_pr('r', 6) : _currentScreen->restoreMode (MODE_Origin ); break; //FIXME case token_csi_pr('h', 7) : _currentScreen-> setMode (MODE_Wrap ); break; //VT100 case token_csi_pr('l', 7) : _currentScreen-> resetMode (MODE_Wrap ); break; //VT100 case token_csi_pr('s', 7) : _currentScreen-> saveMode (MODE_Wrap ); break; //FIXME case token_csi_pr('r', 7) : _currentScreen->restoreMode (MODE_Wrap ); break; //FIXME case token_csi_pr('h', 8) : /* IGNORED: autorepeat on */ break; //VT100 case token_csi_pr('l', 8) : /* IGNORED: autorepeat off */ break; //VT100 case token_csi_pr('s', 8) : /* IGNORED: autorepeat on */ break; //VT100 case token_csi_pr('r', 8) : /* IGNORED: autorepeat off */ break; //VT100 case token_csi_pr('h', 9) : /* IGNORED: interlace */ break; //VT100 case token_csi_pr('l', 9) : /* IGNORED: interlace */ break; //VT100 case token_csi_pr('s', 9) : /* IGNORED: interlace */ break; //VT100 case token_csi_pr('r', 9) : /* IGNORED: interlace */ break; //VT100 case token_csi_pr('h', 12) : /* IGNORED: Cursor blink */ break; //att610 case token_csi_pr('l', 12) : /* IGNORED: Cursor blink */ break; //att610 case token_csi_pr('s', 12) : /* IGNORED: Cursor blink */ break; //att610 case token_csi_pr('r', 12) : /* IGNORED: Cursor blink */ break; //att610 case token_csi_pr('h', 25) : setMode (MODE_Cursor ); break; //VT100 case token_csi_pr('l', 25) : resetMode (MODE_Cursor ); break; //VT100 case token_csi_pr('s', 25) : saveMode (MODE_Cursor ); break; //VT100 case token_csi_pr('r', 25) : restoreMode (MODE_Cursor ); break; //VT100 case token_csi_pr('h', 40) : setMode(MODE_Allow132Columns ); break; // XTERM case token_csi_pr('l', 40) : resetMode(MODE_Allow132Columns ); break; // XTERM case token_csi_pr('h', 41) : /* IGNORED: obsolete more(1) fix */ break; //XTERM case token_csi_pr('l', 41) : /* IGNORED: obsolete more(1) fix */ break; //XTERM case token_csi_pr('s', 41) : /* IGNORED: obsolete more(1) fix */ break; //XTERM case token_csi_pr('r', 41) : /* IGNORED: obsolete more(1) fix */ break; //XTERM case token_csi_pr('h', 47) : setMode (MODE_AppScreen); break; //VT100 case token_csi_pr('l', 47) : resetMode (MODE_AppScreen); break; //VT100 case token_csi_pr('s', 47) : saveMode (MODE_AppScreen); break; //XTERM case token_csi_pr('r', 47) : restoreMode (MODE_AppScreen); break; //XTERM case token_csi_pr('h', 67) : /* IGNORED: DECBKM */ break; //XTERM case token_csi_pr('l', 67) : /* IGNORED: DECBKM */ break; //XTERM case token_csi_pr('s', 67) : /* IGNORED: DECBKM */ break; //XTERM case token_csi_pr('r', 67) : /* IGNORED: DECBKM */ break; //XTERM // XTerm defines the following modes: // SET_VT200_MOUSE 1000 // SET_VT200_HIGHLIGHT_MOUSE 1001 // SET_BTN_EVENT_MOUSE 1002 // SET_ANY_EVENT_MOUSE 1003 // //Note about mouse modes: //There are four mouse modes which xterm-compatible terminals can support - 1000,1001,1002,1003 //Konsole currently supports mode 1000 (basic mouse press and release) and mode 1002 (dragging the mouse). //TODO: Implementation of mouse modes 1001 (something called hilight tracking) and //1003 (a slight variation on dragging the mouse) // case token_csi_pr('h', 1000) : setMode (MODE_Mouse1000); break; //XTERM case token_csi_pr('l', 1000) : resetMode (MODE_Mouse1000); break; //XTERM case token_csi_pr('s', 1000) : saveMode (MODE_Mouse1000); break; //XTERM case token_csi_pr('r', 1000) : restoreMode (MODE_Mouse1000); break; //XTERM case token_csi_pr('h', 1001) : /* IGNORED: hilite mouse tracking */ break; //XTERM case token_csi_pr('l', 1001) : resetMode (MODE_Mouse1001); break; //XTERM case token_csi_pr('s', 1001) : /* IGNORED: hilite mouse tracking */ break; //XTERM case token_csi_pr('r', 1001) : /* IGNORED: hilite mouse tracking */ break; //XTERM case token_csi_pr('h', 1002) : setMode (MODE_Mouse1002); break; //XTERM case token_csi_pr('l', 1002) : resetMode (MODE_Mouse1002); break; //XTERM case token_csi_pr('s', 1002) : saveMode (MODE_Mouse1002); break; //XTERM case token_csi_pr('r', 1002) : restoreMode (MODE_Mouse1002); break; //XTERM case token_csi_pr('h', 1003) : setMode (MODE_Mouse1003); break; //XTERM case token_csi_pr('l', 1003) : resetMode (MODE_Mouse1003); break; //XTERM case token_csi_pr('s', 1003) : saveMode (MODE_Mouse1003); break; //XTERM case token_csi_pr('r', 1003) : restoreMode (MODE_Mouse1003); break; //XTERM case token_csi_pr('h', 1004) : _reportFocusEvents = true; break; case token_csi_pr('l', 1004) : _reportFocusEvents = false; break; case token_csi_pr('h', 1005) : setMode (MODE_Mouse1005); break; //XTERM case token_csi_pr('l', 1005) : resetMode (MODE_Mouse1005); break; //XTERM case token_csi_pr('s', 1005) : saveMode (MODE_Mouse1005); break; //XTERM case token_csi_pr('r', 1005) : restoreMode (MODE_Mouse1005); break; //XTERM case token_csi_pr('h', 1006) : setMode (MODE_Mouse1006); break; //XTERM case token_csi_pr('l', 1006) : resetMode (MODE_Mouse1006); break; //XTERM case token_csi_pr('s', 1006) : saveMode (MODE_Mouse1006); break; //XTERM case token_csi_pr('r', 1006) : restoreMode (MODE_Mouse1006); break; //XTERM case token_csi_pr('h', 1007) : setMode (MODE_Mouse1007); break; //XTERM case token_csi_pr('l', 1007) : resetMode (MODE_Mouse1007); break; //XTERM case token_csi_pr('s', 1007) : saveMode (MODE_Mouse1007); break; //XTERM case token_csi_pr('r', 1007) : restoreMode (MODE_Mouse1007); break; //XTERM case token_csi_pr('h', 1015) : setMode (MODE_Mouse1015); break; //URXVT case token_csi_pr('l', 1015) : resetMode (MODE_Mouse1015); break; //URXVT case token_csi_pr('s', 1015) : saveMode (MODE_Mouse1015); break; //URXVT case token_csi_pr('r', 1015) : restoreMode (MODE_Mouse1015); break; //URXVT case token_csi_pr('h', 1034) : /* IGNORED: 8bitinput activation */ break; //XTERM case token_csi_pr('h', 1047) : setMode (MODE_AppScreen); break; //XTERM case token_csi_pr('l', 1047) : _screen[1]->clearEntireScreen(); resetMode(MODE_AppScreen); break; //XTERM case token_csi_pr('s', 1047) : saveMode (MODE_AppScreen); break; //XTERM case token_csi_pr('r', 1047) : restoreMode (MODE_AppScreen); break; //XTERM //FIXME: Unitoken: save translations case token_csi_pr('h', 1048) : saveCursor ( ); break; //XTERM case token_csi_pr('l', 1048) : restoreCursor ( ); break; //XTERM case token_csi_pr('s', 1048) : saveCursor ( ); break; //XTERM case token_csi_pr('r', 1048) : restoreCursor ( ); break; //XTERM //FIXME: every once new sequences like this pop up in xterm. // Here's a guess of what they could mean. case token_csi_pr('h', 1049) : saveCursor(); _screen[1]->clearEntireScreen(); setMode(MODE_AppScreen); break; //XTERM case token_csi_pr('l', 1049) : resetMode(MODE_AppScreen); restoreCursor(); break; //XTERM case token_csi_pr('h', 2004) : setMode (MODE_BracketedPaste); break; //XTERM case token_csi_pr('l', 2004) : resetMode (MODE_BracketedPaste); break; //XTERM case token_csi_pr('s', 2004) : saveMode (MODE_BracketedPaste); break; //XTERM case token_csi_pr('r', 2004) : restoreMode (MODE_BracketedPaste); break; //XTERM // Set Cursor Style (DECSCUSR), VT520, with the extra xterm sequences // the first one is a special case, 'ESC[ q', which mimics 'ESC[1 q' // Using 0 to reset to default is matching VTE, but not any official standard. case token_csi_sp ('q' ) : emit setCursorStyleRequest(Enum::BlockCursor, true); break; case token_csi_psp('q', 0) : emit resetCursorStyleRequest(); break; case token_csi_psp('q', 1) : emit setCursorStyleRequest(Enum::BlockCursor, true); break; case token_csi_psp('q', 2) : emit setCursorStyleRequest(Enum::BlockCursor, false); break; case token_csi_psp('q', 3) : emit setCursorStyleRequest(Enum::UnderlineCursor, true); break; case token_csi_psp('q', 4) : emit setCursorStyleRequest(Enum::UnderlineCursor, false); break; case token_csi_psp('q', 5) : emit setCursorStyleRequest(Enum::IBeamCursor, true); break; case token_csi_psp('q', 6) : emit setCursorStyleRequest(Enum::IBeamCursor, false); break; //FIXME: weird DEC reset sequence case token_csi_pe('p' ) : /* IGNORED: reset ( ) */ break; //FIXME: when changing between vt52 and ansi mode evtl do some resetting. case token_vt52('A' ) : _currentScreen->cursorUp ( 1); break; //VT52 case token_vt52('B' ) : _currentScreen->cursorDown ( 1); break; //VT52 case token_vt52('C' ) : _currentScreen->cursorRight ( 1); break; //VT52 case token_vt52('D' ) : _currentScreen->cursorLeft ( 1); break; //VT52 case token_vt52('F' ) : setAndUseCharset (0, '0'); break; //VT52 case token_vt52('G' ) : setAndUseCharset (0, 'B'); break; //VT52 case token_vt52('H' ) : _currentScreen->setCursorYX (1,1 ); break; //VT52 case token_vt52('I' ) : _currentScreen->reverseIndex ( ); break; //VT52 case token_vt52('J' ) : _currentScreen->clearToEndOfScreen ( ); break; //VT52 case token_vt52('K' ) : _currentScreen->clearToEndOfLine ( ); break; //VT52 case token_vt52('Y' ) : _currentScreen->setCursorYX (p-31,q-31 ); break; //VT52 case token_vt52('Z' ) : reportTerminalType ( ); break; //VT52 case token_vt52('<' ) : setMode (MODE_Ansi ); break; //VT52 case token_vt52('=' ) : setMode (MODE_AppKeyPad); break; //VT52 case token_vt52('>' ) : resetMode (MODE_AppKeyPad); break; //VT52 case token_csi_pg('c' ) : reportSecondaryAttributes( ); break; //VT100 default: reportDecodingError(); break; }; } void Vt102Emulation::clearScreenAndSetColumns(int columnCount) { setImageSize(_currentScreen->getLines(),columnCount); clearEntireScreen(); setDefaultMargins(); _currentScreen->setCursorYX(0,0); } void Vt102Emulation::sendString(const QByteArray& s) { emit sendData(s); } void Vt102Emulation::reportCursorPosition() { char tmp[30]; snprintf(tmp, sizeof(tmp), "\033[%d;%dR", _currentScreen->getCursorY()+1, _currentScreen->getCursorX()+1); sendString(tmp); } void Vt102Emulation::reportTerminalType() { // Primary device attribute response (Request was: ^[[0c or ^[[c (from TT321 Users Guide)) // VT220: ^[[?63;1;2;3;6;7;8c (list deps on emul. capabilities) // VT100: ^[[?1;2c // VT101: ^[[?1;0c // VT102: ^[[?6v if (getMode(MODE_Ansi)) { sendString("\033[?1;2c"); // I'm a VT100 } else { sendString("\033/Z"); // I'm a VT52 } } void Vt102Emulation::reportSecondaryAttributes() { // Secondary device attribute response (Request was: ^[[>0c or ^[[>c) if (getMode(MODE_Ansi)) { sendString("\033[>0;115;0c"); // Why 115? ;) } else { sendString("\033/Z"); // FIXME I don't think VT52 knows about it but kept for } // konsoles backward compatibility. } /* DECREPTPARM – Report Terminal Parameters ESC [ ; ; ; ; ; ; x https://vt100.net/docs/vt100-ug/chapter3.html */ void Vt102Emulation::reportTerminalParms(int p) { char tmp[100]; /* sol=1: This message is a request; report in response to a request. par=1: No parity set nbits=1: 8 bits per character xspeed=112: 9600 rspeed=112: 9600 clkmul=1: The bit rate multiplier is 16. flags=0: None */ snprintf(tmp, sizeof(tmp), "\033[%d;1;1;112;112;1;0x", p); // not really true. sendString(tmp); } void Vt102Emulation::reportStatus() { sendString("\033[0n"); //VT100. Device status report. 0 = Ready. } void Vt102Emulation::reportAnswerBack() { // FIXME - Test this with VTTEST // This is really obsolete VT100 stuff. const char *ANSWER_BACK = ""; sendString(ANSWER_BACK); } /*! `cx',`cy' are 1-based. `cb' indicates the button pressed or released (0-2) or scroll event (4-5). eventType represents the kind of mouse action that occurred: 0 = Mouse button press 1 = Mouse drag 2 = Mouse button release */ void Vt102Emulation::sendMouseEvent(int cb, int cx, int cy, int eventType) { if (cx < 1 || cy < 1) { return; } // With the exception of the 1006 mode, button release is encoded in cb. // Note that if multiple extensions are enabled, the 1006 is used, so it's okay to check for only that. if (eventType == 2 && !getMode(MODE_Mouse1006)) { cb = 3; } // normal buttons are passed as 0x20 + button, // mouse wheel (buttons 4,5) as 0x5c + button if (cb >= 4) { cb += 0x3c; } //Mouse motion handling if ((getMode(MODE_Mouse1002) || getMode(MODE_Mouse1003)) && eventType == 1) { cb += 0x20; //add 32 to signify motion event } char command[40]; command[0] = '\0'; // Check the extensions in decreasing order of preference. Encoding the release event above assumes that 1006 comes first. if (getMode(MODE_Mouse1006)) { snprintf(command, sizeof(command), "\033[<%d;%d;%d%c", cb, cx, cy, eventType == 2 ? 'm' : 'M'); } else if (getMode(MODE_Mouse1015)) { snprintf(command, sizeof(command), "\033[%d;%d;%dM", cb + 0x20, cx, cy); } else if (getMode(MODE_Mouse1005)) { if (cx <= 2015 && cy <= 2015) { // The xterm extension uses UTF-8 (up to 2 bytes) to encode // coordinate+32, no matter what the locale is. We could easily // convert manually, but QString can also do it for us. QChar coords[2]; coords[0] = cx + 0x20; coords[1] = cy + 0x20; QString coordsStr = QString(coords, 2); QByteArray utf8 = coordsStr.toUtf8(); snprintf(command, sizeof(command), "\033[M%c%s", cb + 0x20, utf8.constData()); } } else if (cx <= 223 && cy <= 223) { snprintf(command, sizeof(command), "\033[M%c%c%c", cb + 0x20, cx + 0x20, cy + 0x20); } sendString(command); } /** - * The focus lost event can be used by Vim (or other terminal applications) - * to recognize that the konsole window has lost focus. + * The focus change event can be used by Vim (or other terminal applications) + * to recognize that the konsole window has changed focus. * The escape sequence is also used by iTerm2. * Vim needs the following plugin to be installed to convert the escape - * sequence into the FocusLost autocmd: https://github.com/sjl/vitality.vim + * sequence into the FocusLost/FocusGained autocmd: + * https://github.com/sjl/vitality.vim */ -void Vt102Emulation::focusLost() -{ - if (_reportFocusEvents) { - sendString("\033[O"); - } -} - -/** - * The focus gained event can be used by Vim (or other terminal applications) - * to recognize that the konsole window has gained focus again. - * The escape sequence is also used by iTerm2. - * Vim needs the following plugin to be installed to convert the escape - * sequence into the FocusGained autocmd: https://github.com/sjl/vitality.vim - */ -void Vt102Emulation::focusGained() -{ +void Vt102Emulation::focusChanged(bool focused) { if (_reportFocusEvents) { - sendString("\033[I"); + sendString(focused ? "\033[I" : "\033[O"); } } void Vt102Emulation::sendText(const QString &text) { if (!text.isEmpty()) { QKeyEvent event(QEvent::KeyPress, 0, Qt::NoModifier, text); sendKeyEvent(&event); // expose as a big fat keypress event } } void Vt102Emulation::sendKeyEvent(QKeyEvent *event) { const Qt::KeyboardModifiers modifiers = event->modifiers(); KeyboardTranslator::States states = KeyboardTranslator::NoState; TerminalDisplay * currentView = _currentScreen->currentTerminalDisplay(); bool isReadOnly = false; if (currentView != nullptr && currentView->sessionController() != nullptr) { isReadOnly = currentView->sessionController()->isReadOnly(); } // get current states if (getMode(MODE_NewLine)) { states |= KeyboardTranslator::NewLineState; } if (getMode(MODE_Ansi)) { states |= KeyboardTranslator::AnsiState; } if (getMode(MODE_AppCuKeys)) { states |= KeyboardTranslator::CursorKeysState; } if (getMode(MODE_AppScreen)) { states |= KeyboardTranslator::AlternateScreenState; } if (getMode(MODE_AppKeyPad) && ((modifiers &Qt::KeypadModifier) != 0u)) { states |= KeyboardTranslator::ApplicationKeypadState; } if (!isReadOnly) { // check flow control state if ((modifiers &Qt::ControlModifier) != 0u) { switch (event->key()) { case Qt::Key_S: emit flowControlKeyPressed(true); break; case Qt::Key_Q: case Qt::Key_C: // cancel flow control emit flowControlKeyPressed(false); break; } } } // look up key binding if (_keyTranslator != nullptr) { KeyboardTranslator::Entry entry = _keyTranslator->findEntry( event->key(), modifiers, states); // send result to terminal QByteArray textToSend; // special handling for the Alt (aka. Meta) modifier. pressing // Alt+[Character] results in Esc+[Character] being sent // (unless there is an entry defined for this particular combination // in the keyboard modifier) const bool wantsAltModifier = ((entry.modifiers() & entry.modifierMask() & Qt::AltModifier) != 0u); const bool wantsMetaModifier = ((entry.modifiers() & entry.modifierMask() & Qt::MetaModifier) != 0u); const bool wantsAnyModifier = ((entry.state() & entry.stateMask() & KeyboardTranslator::AnyModifierState) != 0); if ( ((modifiers & Qt::AltModifier) != 0u) && !(wantsAltModifier || wantsAnyModifier) && !event->text().isEmpty() ) { textToSend.prepend("\033"); } if ( ((modifiers & Qt::MetaModifier) != 0u) && !(wantsMetaModifier || wantsAnyModifier) && !event->text().isEmpty() ) { textToSend.prepend("\030@s"); } if ( entry.command() != KeyboardTranslator::NoCommand ) { if ((entry.command() & KeyboardTranslator::EraseCommand) != 0) { textToSend += eraseChar(); } if (currentView != nullptr) { if ((entry.command() & KeyboardTranslator::ScrollPageUpCommand) != 0) { currentView->scrollScreenWindow(ScreenWindow::ScrollPages, -1); } else if ((entry.command() & KeyboardTranslator::ScrollPageDownCommand) != 0) { currentView->scrollScreenWindow(ScreenWindow::ScrollPages, 1); } else if ((entry.command() & KeyboardTranslator::ScrollLineUpCommand) != 0) { currentView->scrollScreenWindow(ScreenWindow::ScrollLines, -1); } else if ((entry.command() & KeyboardTranslator::ScrollLineDownCommand) != 0) { currentView->scrollScreenWindow(ScreenWindow::ScrollLines, 1); } else if ((entry.command() & KeyboardTranslator::ScrollUpToTopCommand) != 0) { currentView->scrollScreenWindow(ScreenWindow::ScrollLines, - currentView->screenWindow()->currentLine()); } else if ((entry.command() & KeyboardTranslator::ScrollDownToBottomCommand) != 0) { currentView->scrollScreenWindow(ScreenWindow::ScrollLines, lineCount()); } } } else if (!entry.text().isEmpty()) { textToSend += entry.text(true,modifiers); } else { textToSend += _codec->fromUnicode(event->text()); } if (!isReadOnly) { emit sendData(textToSend); } } else { if (!isReadOnly) { // print an error message to the terminal if no key translator has been // set QString translatorError = i18n("No keyboard translator available. " "The information needed to convert key presses " "into characters to send to the terminal " "is missing."); reset(); receiveData(translatorError.toLatin1().constData(), translatorError.count()); } } } /* ------------------------------------------------------------------------- */ /* */ /* VT100 Charsets */ /* */ /* ------------------------------------------------------------------------- */ // Character Set Conversion ------------------------------------------------ -- /* The processing contains a VT100 specific code translation layer. It's still in use and mainly responsible for the line drawing graphics. These and some other glyphs are assigned to codes (0x5f-0xfe) normally occupied by the latin letters. Since this codes also appear within control sequences, the extra code conversion does not permute with the tokenizer and is placed behind it in the pipeline. It only applies to tokens, which represent plain characters. This conversion it eventually continued in TerminalDisplay.C, since it might involve VT100 enhanced fonts, which have these particular glyphs allocated in (0x00-0x1f) in their code page. */ #define CHARSET _charset[_currentScreen == _screen[1]] // Apply current character map. unsigned int Vt102Emulation::applyCharset(uint c) { if (CHARSET.graphic && 0x5f <= c && c <= 0x7e) { return vt100_graphics[c - 0x5f]; } if (CHARSET.pound && c == '#') { return 0xa3; //This mode is obsolete } return c; } /* "Charset" related part of the emulation state. This configures the VT100 charset filter. While most operation work on the current _screen, the following two are different. */ void Vt102Emulation::resetCharset(int scrno) { _charset[scrno].cu_cs = 0; qstrncpy(_charset[scrno].charset, "BBBB", 4); _charset[scrno].sa_graphic = false; _charset[scrno].sa_pound = false; _charset[scrno].graphic = false; _charset[scrno].pound = false; } void Vt102Emulation::setCharset(int n, int cs) // on both screens. { _charset[0].charset[n & 3] = cs; useCharset(_charset[0].cu_cs); _charset[1].charset[n & 3] = cs; useCharset(_charset[1].cu_cs); } void Vt102Emulation::setAndUseCharset(int n, int cs) { CHARSET.charset[n & 3] = cs; useCharset(n & 3); } void Vt102Emulation::useCharset(int n) { CHARSET.cu_cs = n & 3; CHARSET.graphic = (CHARSET.charset[n & 3] == '0'); CHARSET.pound = (CHARSET.charset[n & 3] == 'A'); //This mode is obsolete } void Vt102Emulation::setDefaultMargins() { _screen[0]->setDefaultMargins(); _screen[1]->setDefaultMargins(); } void Vt102Emulation::setMargins(int t, int b) { _screen[0]->setMargins(t, b); _screen[1]->setMargins(t, b); } void Vt102Emulation::saveCursor() { CHARSET.sa_graphic = CHARSET.graphic; CHARSET.sa_pound = CHARSET.pound; //This mode is obsolete // we are not clear about these //sa_charset = charsets[cScreen->_charset]; //sa_charset_num = cScreen->_charset; _currentScreen->saveCursor(); } void Vt102Emulation::restoreCursor() { CHARSET.graphic = CHARSET.sa_graphic; CHARSET.pound = CHARSET.sa_pound; //This mode is obsolete _currentScreen->restoreCursor(); } /* ------------------------------------------------------------------------- */ /* */ /* Mode Operations */ /* */ /* ------------------------------------------------------------------------- */ /* Some of the emulations state is either added to the state of the screens. This causes some scoping problems, since different emulations choose to located the mode either to the current _screen or to both. For strange reasons, the extend of the rendition attributes ranges over all screens and not over the actual _screen. We decided on the precise precise extend, somehow. */ // "Mode" related part of the state. These are all booleans. void Vt102Emulation::resetModes() { // MODE_Allow132Columns is not reset here // to match Xterm's behavior (see Xterm's VTReset() function) // MODE_Mouse1007 (Alternate Scrolling) is not reset here, to maintain // the profile alternate scrolling property after reset() is called, which // makes more sense; also this matches XTerm behavior. resetMode(MODE_132Columns); saveMode(MODE_132Columns); resetMode(MODE_Mouse1000); saveMode(MODE_Mouse1000); resetMode(MODE_Mouse1001); saveMode(MODE_Mouse1001); resetMode(MODE_Mouse1002); saveMode(MODE_Mouse1002); resetMode(MODE_Mouse1003); saveMode(MODE_Mouse1003); resetMode(MODE_Mouse1005); saveMode(MODE_Mouse1005); resetMode(MODE_Mouse1006); saveMode(MODE_Mouse1006); resetMode(MODE_Mouse1015); saveMode(MODE_Mouse1015); resetMode(MODE_BracketedPaste); saveMode(MODE_BracketedPaste); resetMode(MODE_AppScreen); saveMode(MODE_AppScreen); resetMode(MODE_AppCuKeys); saveMode(MODE_AppCuKeys); resetMode(MODE_AppKeyPad); saveMode(MODE_AppKeyPad); resetMode(MODE_NewLine); setMode(MODE_Ansi); } void Vt102Emulation::setMode(int m) { _currentModes.mode[m] = true; switch (m) { case MODE_132Columns: if (getMode(MODE_Allow132Columns)) { clearScreenAndSetColumns(132); } else { _currentModes.mode[m] = false; } break; case MODE_Mouse1000: case MODE_Mouse1001: case MODE_Mouse1002: case MODE_Mouse1003: emit programRequestsMouseTracking(true); break; case MODE_Mouse1007: emit enableAlternateScrolling(true); break; case MODE_BracketedPaste: emit programBracketedPasteModeChanged(true); break; case MODE_AppScreen: _screen[1]->setDefaultRendition(); _screen[1]->clearSelection(); setScreen(1); break; } // FIXME: Currently this has a redundant condition as MODES_SCREEN is 6 // and MODE_NewLine is 5 if (m < MODES_SCREEN || m == MODE_NewLine) { _screen[0]->setMode(m); _screen[1]->setMode(m); } } void Vt102Emulation::resetMode(int m) { _currentModes.mode[m] = false; switch (m) { case MODE_132Columns: if (getMode(MODE_Allow132Columns)) { clearScreenAndSetColumns(80); } break; case MODE_Mouse1000: case MODE_Mouse1001: case MODE_Mouse1002: case MODE_Mouse1003: emit programRequestsMouseTracking(false); break; case MODE_Mouse1007: emit enableAlternateScrolling(false); break; case MODE_BracketedPaste: emit programBracketedPasteModeChanged(false); break; case MODE_AppScreen: _screen[0]->clearSelection(); setScreen(0); break; } // FIXME: Currently this has a redundant condition as MODES_SCREEN is 6 // and MODE_NewLine is 5 if (m < MODES_SCREEN || m == MODE_NewLine) { _screen[0]->resetMode(m); _screen[1]->resetMode(m); } } void Vt102Emulation::saveMode(int m) { _savedModes.mode[m] = _currentModes.mode[m]; } void Vt102Emulation::restoreMode(int m) { if (_savedModes.mode[m]) { setMode(m); } else { resetMode(m); } } bool Vt102Emulation::getMode(int m) { return _currentModes.mode[m]; } char Vt102Emulation::eraseChar() const { KeyboardTranslator::Entry entry = _keyTranslator->findEntry( Qt::Key_Backspace, nullptr, nullptr); if (entry.text().count() > 0) { return entry.text().at(0); } else { return '\b'; } } // return contents of the scan buffer static QString hexdump2(uint *s, int len) { int i; char dump[128]; QString returnDump; for (i = 0; i < len; i++) { if (s[i] == '\\') { snprintf(dump, sizeof(dump), "%s", "\\\\"); } else if ((s[i]) > 32 && s[i] < 127) { snprintf(dump, sizeof(dump), "%c", s[i]); } else { snprintf(dump, sizeof(dump), "\\%04x(hex)", s[i]); } returnDump.append(QLatin1String(dump)); } return returnDump; } void Vt102Emulation::reportDecodingError() { if (tokenBufferPos == 0 || (tokenBufferPos == 1 && (tokenBuffer[0] & 0xff) >= 32)) { return; } QString outputError = QStringLiteral("Undecodable sequence: "); outputError.append(hexdump2(tokenBuffer, tokenBufferPos)); } diff --git a/src/Vt102Emulation.h b/src/Vt102Emulation.h index 7fa395d7..a3c7cbdb 100644 --- a/src/Vt102Emulation.h +++ b/src/Vt102Emulation.h @@ -1,195 +1,194 @@ /* This file is part of Konsole, an X terminal. Copyright 2007-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle 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 VT102EMULATION_H #define VT102EMULATION_H // Qt #include // Konsole #include "Emulation.h" #include "Screen.h" class QTimer; class QKeyEvent; #define MODE_AppScreen (MODES_SCREEN+0) // Mode #1 #define MODE_AppCuKeys (MODES_SCREEN+1) // Application cursor keys (DECCKM) #define MODE_AppKeyPad (MODES_SCREEN+2) // #define MODE_Mouse1000 (MODES_SCREEN+3) // Send mouse X,Y position on press and release #define MODE_Mouse1001 (MODES_SCREEN+4) // Use Hilight mouse tracking #define MODE_Mouse1002 (MODES_SCREEN+5) // Use cell motion mouse tracking #define MODE_Mouse1003 (MODES_SCREEN+6) // Use all motion mouse tracking #define MODE_Mouse1005 (MODES_SCREEN+7) // Xterm-style extended coordinates #define MODE_Mouse1006 (MODES_SCREEN+8) // 2nd Xterm-style extended coordinates #define MODE_Mouse1007 (MODES_SCREEN+9) // XTerm Alternate Scroll mode; also check AlternateScrolling profile property #define MODE_Mouse1015 (MODES_SCREEN+10) // Urxvt-style extended coordinates #define MODE_Ansi (MODES_SCREEN+11) // Use US Ascii for character sets G0-G3 (DECANM) #define MODE_132Columns (MODES_SCREEN+12) // 80 <-> 132 column mode switch (DECCOLM) #define MODE_Allow132Columns (MODES_SCREEN+13) // Allow DECCOLM mode #define MODE_BracketedPaste (MODES_SCREEN+14) // Xterm-style bracketed paste mode #define MODE_total (MODES_SCREEN+15) namespace Konsole { extern unsigned short vt100_graphics[32]; struct CharCodes { // coding info char charset[4]; // int cu_cs; // actual charset. bool graphic; // Some VT100 tricks bool pound; // Some VT100 tricks bool sa_graphic; // saved graphic bool sa_pound; // saved pound }; /** * Provides an xterm compatible terminal emulation based on the DEC VT102 terminal. * A full description of this terminal can be found at https://vt100.net/docs/vt102-ug/ * * In addition, various additional xterm escape sequences are supported to provide * features such as mouse input handling. * See http://rtfm.etla.org/xterm/ctlseq.html for a description of xterm's escape * sequences. * */ class Vt102Emulation : public Emulation { Q_OBJECT public: /** Constructs a new emulation */ Vt102Emulation(); ~Vt102Emulation() Q_DECL_OVERRIDE; // reimplemented from Emulation void clearEntireScreen() Q_DECL_OVERRIDE; void reset() Q_DECL_OVERRIDE; char eraseChar() const Q_DECL_OVERRIDE; public Q_SLOTS: // reimplemented from Emulation void sendString(const QByteArray &string) Q_DECL_OVERRIDE; void sendText(const QString &text) Q_DECL_OVERRIDE; void sendKeyEvent(QKeyEvent *) Q_DECL_OVERRIDE; void sendMouseEvent(int buttons, int column, int line, int eventType) Q_DECL_OVERRIDE; - void focusLost() Q_DECL_OVERRIDE; - void focusGained() Q_DECL_OVERRIDE; + void focusChanged(bool focused) Q_DECL_OVERRIDE; protected: // reimplemented from Emulation void setMode(int mode) Q_DECL_OVERRIDE; void resetMode(int mode) Q_DECL_OVERRIDE; void receiveChar(uint cc) Q_DECL_OVERRIDE; private Q_SLOTS: // Causes sessionAttributeChanged() to be emitted for each (int,QString) // pair in _pendingSessionAttributesUpdates. // Used to buffer multiple attribute updates in the current session void updateSessionAttributes(); private: unsigned int applyCharset(uint c); void setCharset(int n, int cs); void useCharset(int n); void setAndUseCharset(int n, int cs); void saveCursor(); void restoreCursor(); void resetCharset(int scrno); void setMargins(int top, int bottom); //set margins for all screens back to their defaults void setDefaultMargins(); // returns true if 'mode' is set or false otherwise bool getMode(int mode); // saves the current boolean value of 'mode' void saveMode(int mode); // restores the boolean value of 'mode' void restoreMode(int mode); // resets all modes // (except MODE_Allow132Columns) void resetModes(); void resetTokenizer(); #define MAX_TOKEN_LENGTH 256 // Max length of tokens (e.g. window title) void addToCurrentToken(uint cc); uint tokenBuffer[MAX_TOKEN_LENGTH]; //FIXME: overflow? int tokenBufferPos; #define MAXARGS 15 void addDigit(int dig); void addArgument(); int argv[MAXARGS]; int argc; void initTokenizer(); // Set of flags for each of the ASCII characters which indicates // what category they fall into (printable character, control, digit etc.) // for the purposes of decoding terminal output int charClass[256]; void reportDecodingError(); void processToken(int code, int p, int q); void processSessionAttributeRequest(int tokenSize); void reportTerminalType(); void reportSecondaryAttributes(); void reportStatus(); void reportAnswerBack(); void reportCursorPosition(); void reportTerminalParms(int p); // clears the screen and resizes it to the specified // number of columns void clearScreenAndSetColumns(int columnCount); CharCodes _charset[2]; class TerminalState { public: // Initializes all modes to false TerminalState() { memset(&mode, 0, MODE_total * sizeof(bool)); } bool mode[MODE_total]; }; TerminalState _currentModes; TerminalState _savedModes; // Hash table and timer for buffering calls to update certain session // attributes (e.g. the name of the session, window title). // These calls occur when certain escape sequences are detected in the // output from the terminal. See Emulation::sessionAttributeChanged() QHash _pendingSessionAttributesUpdates; QTimer *_sessionAttributesUpdateTimer; bool _reportFocusEvents; }; } #endif // VT102EMULATION_H