Commit f26f71db authored by Matan Ziv-Av's avatar Matan Ziv-Av Committed by Tomaz Canabrava
Browse files

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
parent fb95ecbb
......@@ -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
......
......@@ -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;
......
......@@ -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;
......
......@@ -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);
......
......@@ -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
......
......@@ -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)
{
......
......@@ -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;
};
}
......
......@@ -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()
......
......@@ -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);
......
......@@ -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,
......
......@@ -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
};
}
......
Supports Markdown