From f26f71db013a5440560fd3be3fa4cf249f1fb8dd Mon Sep 17 00:00:00 2001 From: Matan Ziv-Av Date: Sat, 17 Sep 2022 13:12:56 +0300 Subject: [PATCH] Add keyboard selection mode Similar to screen copy/scrollback mode it allows browsing the scrollback and selecting text. Selection is done either by standard GUI shift+arrows, or `vi` style with `v` starting/ending selection. BUG: 100317 --- doc/manual/index.docbook | 27 +++- src/Screen.cpp | 117 ++++++++++++++++ src/Screen.h | 17 ++- src/SearchHistoryTask.cpp | 9 +- src/Vt102Emulation.cpp | 14 +- src/session/Session.cpp | 11 ++ src/session/Session.h | 5 + src/session/SessionController.cpp | 22 ++- src/session/SessionController.h | 1 + src/terminalDisplay/TerminalDisplay.cpp | 175 ++++++++++++++++++++++++ src/terminalDisplay/TerminalDisplay.h | 14 ++ 11 files changed, 399 insertions(+), 13 deletions(-) diff --git a/doc/manual/index.docbook b/doc/manual/index.docbook index 93dd9a922..7f29d1083 100644 --- a/doc/manual/index.docbook +++ b/doc/manual/index.docbook @@ -52,8 +52,8 @@ &FDLNotice; -2021-10-28 -Applications 21.12 +2022-09-16 +Applications 22.12 &konsole; is &kde;'s terminal emulator. @@ -110,6 +110,29 @@ to open this window). + +Selection Mode +&konsole; has a selction by keyboard mode. In this mode it is possible to move around the scrollback and select text +without the mouse. + + +Enter and leave this mode by using the keyboard shortcut (&Ctrl;&Shift;D by default). + + + +Moving the cursor: Arrows, PageUp, PageDown, Home, End. + + +Moving the cursor vi style: h,j,k,l, to move one character, Ctrl+b,f,u,d for page up/down or half page up/down. + + +Select text by using Ctrl or Shift with arrows, or by using V to start selection, moving the cursor and then V again to end selection. +&Shift;V selects whole lines, instead of characters. + + + + + Profiles Profiles allow the user to quickly and easily automate the running diff --git a/src/Screen.cpp b/src/Screen.cpp index 6c5bc7e6d..f537f14ae 100644 --- a/src/Screen.cpp +++ b/src/Screen.cpp @@ -176,6 +176,117 @@ void Screen::cursorRight(int n) _cuX = qMin(getScreenLineColumns(_cuY) - 1, _cuX + n); } +void Screen::initSelCursor() +{ + _selCuX = _cuX; + _selCuY = _cuY; +} + +int Screen::selCursorUp(int n) +{ + if (n == 0) { + // Half page + n = _lines / 2; + } else if (n == -1) { + // Full page + n = _lines; + } else if (n == -2) { + // First line + n = _selCuY + _history->getLines(); + } + _selCuY = qMax(-_history->getLines(), _selCuY - n); + return _selCuY; +} + +int Screen::selCursorDown(int n) +{ + if (n == 0) { + // Half page + n = _lines / 2; + } else if (n == -1) { + // Full page + n = _lines; + } else if (n == -2) { + // Last line + n = _lines - 1 - _selCuY; + } + _selCuY = qMin(_lines - 1, _selCuY + n); + return _selCuY; +} + +int Screen::selCursorLeft(int n) +{ + if (n == 0) { + // Home + n = _selCuX; + } + if (_selCuX >= n) { + _selCuX -= n; + } else { + if (_selCuY > -_history->getLines()) { + _selCuY -= 1; + _selCuX = qMax(_columns - n + _selCuX, 0); + } else { + _selCuX = 0; + } + } + return _selCuY; +} + +int Screen::selCursorRight(int n) +{ + if (n == 0) { + // End + n = _columns - _selCuX - 1; + } + if (_selCuX + n < _columns) { + _selCuX += n; + } else { + if (_selCuY < _lines - 1) { + _selCuY += 1; + _selCuX = qMin(n + _selCuX - _columns, _columns - 1); + } else { + _selCuX = _columns - 1; + } + } + return _selCuY; +} + +int Screen::selSetSelectionStart(int mode) +{ + // mode: 0 = character selection + // 1 = line selection + int x = _selCuX; + if (mode == 1) { + x = 0; + } + setSelectionStart(x, _selCuY + _history->getLines(), false); + return 0; +} + +int Screen::selSetSelectionEnd(int mode) +{ + int y = _selCuY + _history->getLines(); + int x = _selCuX; + if (mode == 1) { + int l = _selBegin / _columns; + if (y < l) { + if (_selBegin % _columns == 0) { + setSelectionStart(_columns - 1, l, false); + } + x = 0; + } else { + x = _columns - 1; + if (_selBegin % _columns != 0) { + setSelectionStart(0, l, false); + } + } + } + setSelectionEnd(x, y, false); + Q_EMIT _currentTerminalDisplay->screenWindow()->selectionChanged(); + return 0; +} + void Screen::setMargins(int top, int bot) //=STBM { @@ -767,6 +878,11 @@ void Screen::getImage(Character *dest, int size, int startLine, int endLine) con if (getMode(MODE_Cursor) && cursorIndex < _columns * mergedLines) { dest[cursorIndex].rendition.f.cursor = 1; } + cursorIndex = loc(_selCuX, _selCuY - startLine + _history->getLines()); + + if (getMode(MODE_SelectCursor) && cursorIndex >= 0 && cursorIndex < _columns * mergedLines) { + dest[cursorIndex].rendition.f.cursor = 1; + } } QVector Screen::getLineProperties(int startLine, int endLine) const @@ -841,6 +957,7 @@ void Screen::reset(bool softReset, bool preservePrompt) saveMode(MODE_Insert); // overstroke setMode(MODE_Cursor); // cursor visible + resetMode(MODE_SelectCursor); _topMargin = 0; _bottomMargin = _lines - 1; diff --git a/src/Screen.h b/src/Screen.h index b0587e51d..b5d0c3197 100644 --- a/src/Screen.h +++ b/src/Screen.h @@ -29,7 +29,8 @@ #define MODE_Cursor 4 #define MODE_NewLine 5 #define MODE_AppScreen 6 -#define MODES_SCREEN 7 +#define MODE_SelectCursor 7 +#define MODES_SCREEN 8 #define REPL_None 0 #define REPL_PROMPT 1 @@ -148,6 +149,16 @@ public: void setCursorX(int x); /** Position the cursor at line @p y, column @p x. */ void setCursorYX(int y, int x); + + void initSelCursor(); + int selCursorUp(int n); + int selCursorDown(int n); + int selCursorLeft(int n); + int selCursorRight(int n); + + int selSetSelectionStart(int mode); + int selSetSelectionEnd(int mode); + /** * Sets the margins for scrolling the screen. * @@ -805,6 +816,10 @@ private: int _cuX; int _cuY; + // select mode cursor location + int _selCuX; + int _selCuY; + // cursor color and rendition info CharacterColor _currentForeground; CharacterColor _currentBackground; diff --git a/src/SearchHistoryTask.cpp b/src/SearchHistoryTask.cpp index 4997843b8..0a1667361 100644 --- a/src/SearchHistoryTask.cpp +++ b/src/SearchHistoryTask.cpp @@ -148,10 +148,11 @@ void SearchHistoryTask::executeOnScreenWindow(const QPointer &session, string.clear(); line = endLine; } while (startLine != endLine); - - // if no match was found, clear selection to indicate this - window->clearSelection(); - window->notifyOutputChanged(); + if (!session->getSelectMode()) { + // if no match was found, clear selection to indicate this, + window->clearSelection(); + window->notifyOutputChanged(); + } } Q_EMIT completed(false); diff --git a/src/Vt102Emulation.cpp b/src/Vt102Emulation.cpp index b50a11e51..bcbac76b1 100644 --- a/src/Vt102Emulation.cpp +++ b/src/Vt102Emulation.cpp @@ -2040,8 +2040,11 @@ void Vt102Emulation::sendMouseEvent(int cb, int cx, int cy, int eventType) // We know we are in input mode TerminalDisplay *currentView = _currentScreen->currentTerminalDisplay(); bool isReadOnly = false; - if (currentView != nullptr && currentView->sessionController() != nullptr) { - isReadOnly = currentView->sessionController()->isReadOnly(); + // if (currentView != nullptr && currentView->sessionController() != nullptr) { + // isReadOnly = currentView->sessionController()->isReadOnly(); + // } + if (currentView != nullptr) { + isReadOnly = currentView->getReadOnly(); } auto point = std::make_pair(cy, cx); if (!isReadOnly && _currentScreen->replModeStart() <= point && point <= _currentScreen->replModeEnd()) { @@ -2196,8 +2199,11 @@ void Vt102Emulation::sendKeyEvent(QKeyEvent *event) TerminalDisplay *currentView = _currentScreen->currentTerminalDisplay(); bool isReadOnly = false; - if (currentView != nullptr && currentView->sessionController() != nullptr) { - isReadOnly = currentView->sessionController()->isReadOnly(); + // if (currentView != nullptr && currentView->sessionController() != nullptr) { + // isReadOnly = currentView->sessionController()->isReadOnly(); + // } + if (currentView != nullptr) { + isReadOnly = currentView->getReadOnly(); } // get current states diff --git a/src/session/Session.cpp b/src/session/Session.cpp index f3b8c94ce..19c7ed151 100644 --- a/src/session/Session.cpp +++ b/src/session/Session.cpp @@ -1857,6 +1857,17 @@ void Session::setReadOnly(bool readOnly) Q_EMIT readOnlyChanged(); } } +bool Session::getSelectMode() const +{ + return _selectMode; +} + +void Session::setSelectMode(bool mode) +{ + if (_selectMode != mode) { + _selectMode = mode; + } +} void Session::setColor(const QColor &color) { diff --git a/src/session/Session.h b/src/session/Session.h index 7291602d7..6da867001 100644 --- a/src/session/Session.h +++ b/src/session/Session.h @@ -406,6 +406,9 @@ public: bool isReadOnly() const; void setReadOnly(bool readOnly); + bool getSelectMode() const; + void setSelectMode(bool mode); + // Returns true if the current screen is the secondary/alternate one // or false if it's the primary/normal buffer bool isPrimaryScreen(); @@ -875,6 +878,8 @@ private: bool _isPrimaryScreen = true; QString _currentHostName; + + bool _selectMode = false; }; } diff --git a/src/session/SessionController.cpp b/src/session/SessionController.cpp index 8bd91857c..84ab8d7af 100644 --- a/src/session/SessionController.cpp +++ b/src/session/SessionController.cpp @@ -452,12 +452,14 @@ void SessionController::setupPrimaryScreenSpecificActions(bool use) QAction *clearAction = collection->action(QStringLiteral("clear-history")); QAction *resetAction = collection->action(QStringLiteral("clear-history-and-reset")); QAction *selectAllAction = collection->action(QStringLiteral("select-all")); + QAction *selectModeAction = collection->action(QStringLiteral("select-mode")); 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); + selectModeAction->setEnabled(use); selectLineAction->setEnabled(use); } @@ -716,6 +718,11 @@ void SessionController::setupCommonActions() action->setText(i18n("&Select All")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all"))); + action = collection->addAction(QStringLiteral("select-mode"), this, &SessionController::selectMode); + action->setText(i18n("Select &Mode")); + action->setIcon(QIcon::fromTheme(QStringLiteral("edit-select"))); + collection->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_D)); + action = collection->addAction(QStringLiteral("select-line"), this, &SessionController::selectLine); action->setText(i18n("Select &Line")); @@ -1208,6 +1215,17 @@ void SessionController::selectAll() { view()->selectAll(); } +void SessionController::selectMode() +{ + if (!session().isNull()) { + QAction *readonlyAction = actionCollection()->action(QStringLiteral("view-readonly")); + bool Mode = session()->getSelectMode(); + session()->setSelectMode(!Mode); + readonlyAction->setEnabled(Mode); + view()->setSelectMode(!Mode); + } +} + void SessionController::selectLine() { view()->selectCurrentLine(); @@ -1563,7 +1581,7 @@ void SessionController::searchTextChanged(const QString &text) _searchText = text; if (text.isEmpty()) { - view()->screenWindow()->clearSelection(); + view()->clearMouseSelection(); view()->screenWindow()->scrollTo(_searchStartLine); } @@ -1673,7 +1691,7 @@ void SessionController::changeSearchMatch() Q_ASSERT(_searchFilter); // reset Selection for new case match - view()->screenWindow()->clearSelection(); + view()->clearMouseSelection(); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::showHistoryOptions() diff --git a/src/session/SessionController.h b/src/session/SessionController.h index 39eb56370..d01c7acd5 100644 --- a/src/session/SessionController.h +++ b/src/session/SessionController.h @@ -228,6 +228,7 @@ private Q_SLOTS: void copyInputOutput(); void paste(); void selectAll(); + void selectMode(); void selectLine(); void pasteFromX11Selection(); // shortcut only void copyInputActionsTriggered(QAction *action); diff --git a/src/terminalDisplay/TerminalDisplay.cpp b/src/terminalDisplay/TerminalDisplay.cpp index 9d5e4eafe..3ef00bdb6 100644 --- a/src/terminalDisplay/TerminalDisplay.cpp +++ b/src/terminalDisplay/TerminalDisplay.cpp @@ -2492,6 +2492,23 @@ KMessageWidget *TerminalDisplay::createMessageWidget(const QString &text) return widget; } +void TerminalDisplay::setSelectMode(bool mode) +{ + _readOnly = mode; + Screen *screen = screenWindow()->screen(); + if (mode) { + screen->initSelCursor(); + screen->clearSelection(); + screen->setMode(MODE_SelectCursor); + _actSel = 0; + _selModeModifiers = 0; + _selModeByModifiers = false; + } else { + screen->resetMode(MODE_SelectCursor); + } + screenWindow()->notifyOutputChanged(); +} + void TerminalDisplay::updateReadOnlyState(bool readonly) { if (_readOnly == readonly) { @@ -2513,8 +2530,159 @@ void TerminalDisplay::updateReadOnlyState(bool readonly) _readOnly = readonly; } +#define SELECT_BY_MODIFIERS \ + if (startSelect) { \ + _screenWindow->clearSelection(); \ + _actSel = 2; \ + screen->selSetSelectionStart(false); \ + _selModeByModifiers = true; \ + } + void TerminalDisplay::keyPressEvent(QKeyEvent *event) { + Screen *screen = screenWindow()->screen(); + int histLines = screen->getHistLines(); + bool moved = true; + if (session()->getSelectMode()) { + int y; + bool startSelect = false; + int modifiers = event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier); + if (_selModeModifiers != modifiers) { + if (modifiers == 0) { + if (_selModeByModifiers) { + _actSel = 0; + _selModeModifiers = 0; + _selModeByModifiers = false; + } + } else { + if (event->key() >= Qt::Key_Home && event->key() <= Qt::Key_PageDown) { + startSelect = true; + _selModeModifiers = modifiers; + } + } + } + switch (event->key()) { + case Qt::Key_Left: + case Qt::Key_H: + SELECT_BY_MODIFIERS; + y = screen->selCursorLeft(1); + if (histLines + y < screenWindow()->currentLine()) { + scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine()); + } + break; + case Qt::Key_Up: + case Qt::Key_K: + SELECT_BY_MODIFIERS; + y = screen->selCursorUp(1); + if (histLines + y < screenWindow()->currentLine()) { + scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine()); + } + break; + case Qt::Key_Right: + case Qt::Key_L: + SELECT_BY_MODIFIERS; + y = screen->selCursorRight(1); + if (histLines + y >= screenWindow()->currentLine() + screen->getLines()) { + scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine() - screen->getLines() + 1); + } + break; + case Qt::Key_Down: + case Qt::Key_J: + SELECT_BY_MODIFIERS; + y = screen->selCursorDown(1); + if (histLines + y >= screenWindow()->currentLine() + screen->getLines()) { + scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine() - screen->getLines() + 1); + } + break; + case Qt::Key_Home: + SELECT_BY_MODIFIERS; + screen->selCursorLeft(0); + break; + case Qt::Key_End: + SELECT_BY_MODIFIERS; + screen->selCursorRight(0); + break; + case Qt::Key_V: + if (_actSel == 0 || _selModeByModifiers) { + _screenWindow->clearSelection(); + _actSel = 2; + _lineSelectionMode = event->text() == QStringLiteral("V"); + screen->selSetSelectionStart(_lineSelectionMode); + _selModeByModifiers = 0; + } else { + _actSel = 0; + } + break; + case Qt::Key_PageUp: + SELECT_BY_MODIFIERS; + y = screen->selCursorUp(-_scrollBar->scrollFullPage()); + if (histLines + y < screenWindow()->currentLine()) { + scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine()); + } + break; + case Qt::Key_PageDown: + SELECT_BY_MODIFIERS; + y = screen->selCursorDown(-_scrollBar->scrollFullPage()); + if (histLines + y >= screenWindow()->currentLine() + screen->getLines()) { + scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine() - screen->getLines() + 1); + } + break; + case Qt::Key_F: + case Qt::Key_D: + if (event->modifiers() & Qt::ControlModifier) { + y = screen->selCursorDown(-(event->key() == Qt::Key_F)); + if (histLines + y >= screenWindow()->currentLine() + screen->getLines()) { + scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine() - screen->getLines() + 1); + } + } else { + moved = false; + } + break; + case Qt::Key_B: + case Qt::Key_U: + if (event->modifiers() & Qt::ControlModifier) { + y = screen->selCursorUp(-(event->key() == Qt::Key_B)); + if (histLines + y < screenWindow()->currentLine()) { + scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine()); + } + } else { + moved = false; + } + break; + case Qt::Key_G: + if (event->text() == QStringLiteral("G")) { + y = screen->selCursorDown(-2); + screen->selCursorRight(0); + if (histLines + y >= screenWindow()->currentLine() + screen->getLines()) { + scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine() - screen->getLines() + 1); + } + } else { + y = screen->selCursorUp(-2); + screen->selCursorLeft(0); + if (histLines + y < screenWindow()->currentLine()) { + scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine()); + } + } + break; + default: + moved = false; + break; + } + if (event->text() == QStringLiteral("^")) { + // Might be on different key(), depending on keyboard layout + screen->selCursorLeft(0); + moved = true; + } else if (event->text() == QStringLiteral("$")) { + // Might be on different key(), depending on keyboard layout + screen->selCursorRight(0); + moved = true; + } + if (moved && _actSel > 0) { + screen->selSetSelectionEnd(_lineSelectionMode); + } + screenWindow()->notifyOutputChanged(); + return; + } { auto [charLine, charColumn] = getCharacterPosition(mapFromGlobal(QCursor::pos()), !usesMouseTracking()); @@ -2935,6 +3103,13 @@ int TerminalDisplay::selectionState() const return _actSel; } +void TerminalDisplay::clearMouseSelection() +{ + if (!session()->getSelectMode()) { + screenWindow()->clearSelection(); + } +} + int TerminalDisplay::bidiMap(Character *screenline, QString &line, int *log2line, diff --git a/src/terminalDisplay/TerminalDisplay.h b/src/terminalDisplay/TerminalDisplay.h index 7ac2cd524..649ac7895 100644 --- a/src/terminalDisplay/TerminalDisplay.h +++ b/src/terminalDisplay/TerminalDisplay.h @@ -395,6 +395,13 @@ public: // Used to show/hide the message widget void updateReadOnlyState(bool readonly); + void setSelectMode(bool readonly); + + bool getReadOnly() const + { + return _readOnly; + } + // Get mapping between visual and logical positions in line // returns the index of the last non space character. int bidiMap(Character *screenline, @@ -409,6 +416,10 @@ public: void showNotification(QString text); + // + // Clear mouse selection, but not keyboard selection + void clearMouseSelection(); + public Q_SLOTS: /** * Causes the terminal display to fetch the latest character image from the associated @@ -791,6 +802,9 @@ private: bool _semanticInputClick; UBiDi *ubidi = nullptr; + + int _selModeModifiers; + bool _selModeByModifiers; // Selection started by Shift+Arrow }; }