[utilities/konsole] /: Add keyboard selection mode
Tomaz Canabrava
null at kde.org
Mon Oct 3 12:02:51 BST 2022
Git commit f26f71db013a5440560fd3be3fa4cf249f1fb8dd by Tomaz Canabrava, on behalf of Matan Ziv-Av.
Committed on 03/10/2022 at 10:46.
Pushed by tcanabrava into branch 'master'.
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
M +25 -2 doc/manual/index.docbook
M +117 -0 src/Screen.cpp
M +16 -1 src/Screen.h
M +5 -4 src/SearchHistoryTask.cpp
M +10 -4 src/Vt102Emulation.cpp
M +11 -0 src/session/Session.cpp
M +5 -0 src/session/Session.h
M +20 -2 src/session/SessionController.cpp
M +1 -0 src/session/SessionController.h
M +175 -0 src/terminalDisplay/TerminalDisplay.cpp
M +14 -0 src/terminalDisplay/TerminalDisplay.h
https://invent.kde.org/utilities/konsole/commit/f26f71db013a5440560fd3be3fa4cf249f1fb8dd
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 @@
<legalnotice>&FDLNotice;</legalnotice>
-<date>2021-10-28</date>
-<releaseinfo>Applications 21.12</releaseinfo>
+<date>2022-09-16</date>
+<releaseinfo>Applications 22.12</releaseinfo>
<abstract><para>&konsole; is &kde;'s terminal emulator.</para></abstract>
@@ -110,6 +110,29 @@ to open this window).
</sect1>
+<sect1 id="selection">
+<title>Selection Mode</title>
+<para>&konsole; has a selction by keyboard mode. In this mode it is possible to move around the scrollback and select text
+without the mouse.</para>
+
+<para>
+Enter and leave this mode by using the keyboard shortcut (<keycombo action="simul">&Ctrl;&Shift;<keycap>D</keycap></keycombo> by default).
+</para>
+
+<para>
+Moving the cursor: Arrows, <keycap>PageUp</keycap>, <keycap>PageDown</keycap>, <keycap>Home</keycap>, <keycap>End</keycap>.
+</para>
+
+<para>Moving the cursor <application>vi</application> style: h,j,k,l, to move one character, <keycap>Ctrl</keycap>+b,f,u,d for page up/down or half page up/down.</para>
+
+<para>
+Select text by using <keycap>Ctrl</keycap> or <keycap>Shift</keycap> with arrows, or by using <keycap>V</keycap> to start selection, moving the cursor and then <keycap>V</keycap> again to end selection.
+<keycombo>&Shift;<keycap>V</keycap></keycombo> selects whole lines, instead of characters.
+</para>
+
+</sect1>
+
+
<sect1 id="profiles">
<title>Profiles</title>
<para>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<LineProperty> 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> &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
};
}
More information about the kde-doc-english
mailing list