1 /*
2     SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>
3     SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 // Own
9 #include "terminalDisplay/TerminalDisplay.h"
10 #include "KonsoleSettings.h"
11 
12 // Config
13 #include "config-konsole.h"
14 
15 // Qt
16 #include <QAccessible>
17 #include <QAction>
18 #include <QApplication>
19 #include <QClipboard>
20 #include <QDesktopServices>
21 #include <QDrag>
22 #include <QElapsedTimer>
23 #include <QEvent>
24 #include <QFileInfo>
25 #include <QKeyEvent>
26 #include <QLabel>
27 #include <QMimeData>
28 #include <QPainter>
29 #include <QPixmap>
30 #include <QStyle>
31 #include <QTimer>
32 #include <QVBoxLayout>
33 
34 // KDE
35 #include <KColorScheme>
36 #include <KCursor>
37 #include <KIO/DropJob>
38 #include <KIO/StatJob>
39 #include <KJobWidgets>
40 #include <KLocalizedString>
41 #include <KMessageBox>
42 #include <KMessageWidget>
43 #include <KNotification>
44 #include <KShell>
45 
46 // Konsole
47 #include "extras/AutoScrollHandler.h"
48 #include "extras/CompositeWidgetFocusWatcher.h"
49 
50 #include "filterHotSpots/EscapeSequenceUrlFilter.h"
51 #include "filterHotSpots/EscapeSequenceUrlFilterHotSpot.h"
52 #include "filterHotSpots/FileFilterHotspot.h"
53 #include "filterHotSpots/Filter.h"
54 #include "filterHotSpots/HotSpot.h"
55 #include "filterHotSpots/TerminalImageFilterChain.h"
56 
57 #include "../characters/ExtendedCharTable.h"
58 #include "../characters/LineBlockCharacters.h"
59 #include "../decoders/PlainTextDecoder.h"
60 #include "../widgets/KonsolePrintManager.h"
61 #include "../widgets/TerminalDisplayAccessible.h"
62 #include "EscapeSequenceUrlExtractor.h"
63 #include "PrintOptions.h"
64 #include "Screen.h"
65 #include "ViewManager.h" // for colorSchemeForProfile. // TODO: Rewrite this.
66 #include "WindowSystemInfo.h"
67 #include "konsoledebug.h"
68 #include "profile/Profile.h"
69 #include "session/Session.h"
70 #include "session/SessionController.h"
71 #include "session/SessionManager.h"
72 #include "widgets/IncrementalSearchBar.h"
73 
74 #include "TerminalColor.h"
75 #include "TerminalFonts.h"
76 #include "TerminalPainter.h"
77 #include "TerminalScrollBar.h"
78 
79 using namespace Konsole;
80 
loc(int x,int y) const81 inline int TerminalDisplay::loc(int x, int y) const
82 {
83     if (y < 0 || y > _lines) {
84         qDebug() << "Y: " << y << "Lines" << _lines;
85     }
86     if (x < 0 || x > _columns) {
87         qDebug() << "X" << x << "Columns" << _columns;
88     }
89 
90     Q_ASSERT(y >= 0 && y < _lines);
91     Q_ASSERT(x >= 0 && x < _columns);
92     x = qBound(0, x, _columns - 1);
93     y = qBound(0, y, _lines - 1);
94 
95     return y * _columns + x;
96 }
97 
98 /* ------------------------------------------------------------------------- */
99 /*                                                                           */
100 /*                                Colors                                     */
101 /*                                                                           */
102 /* ------------------------------------------------------------------------- */
103 
104 /* Note that we use ANSI color order (bgr), while IBMPC color order is (rgb)
105 
106    Code        0       1       2       3       4       5       6       7
107    ----------- ------- ------- ------- ------- ------- ------- ------- -------
108    ANSI  (bgr) Black   Red     Green   Yellow  Blue    Magenta Cyan    White
109    IBMPC (rgb) Black   Blue    Green   Cyan    Red     Magenta Yellow  White
110 */
111 
setScreenWindow(ScreenWindow * window)112 void TerminalDisplay::setScreenWindow(ScreenWindow *window)
113 {
114     // disconnect existing screen window if any
115     if (!_screenWindow.isNull()) {
116         disconnect(_screenWindow, nullptr, this, nullptr);
117     }
118 
119     _screenWindow = window;
120 
121     if (!_screenWindow.isNull()) {
122         connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, &Konsole::TerminalDisplay::updateImage);
123         connect(_screenWindow.data(), &Konsole::ScreenWindow::currentResultLineChanged, this, &Konsole::TerminalDisplay::updateImage);
124         connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, [this]() {
125             _filterUpdateRequired = true;
126         });
127         connect(_screenWindow.data(), &Konsole::ScreenWindow::screenAboutToChange, this, [this]() {
128             _iPntSel = QPoint(-1, -1);
129             _pntSel = QPoint(-1, -1);
130             _tripleSelBegin = QPoint(-1, -1);
131         });
132         connect(_screenWindow.data(), &Konsole::ScreenWindow::scrolled, this, [this]() {
133             _filterUpdateRequired = true;
134         });
135         connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, []() {
136             QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle);
137         });
138         _screenWindow->setWindowLines(_lines);
139 
140         auto profile = SessionManager::instance()->sessionProfile(_sessionController->session());
141         _screenWindow->screen()->urlExtractor()->setAllowedLinkSchema(profile->escapedLinksSchema());
142         _screenWindow->screen()->setReflowLines(profile->property<bool>(Profile::ReflowLines));
143     }
144 }
145 
146 /* ------------------------------------------------------------------------- */
147 /*                                                                           */
148 /*                         Accessibility                                     */
149 /*                                                                           */
150 /* ------------------------------------------------------------------------- */
151 
152 namespace Konsole
153 {
154 #ifndef QT_NO_ACCESSIBILITY
155 /**
156  * This function installs the factory function which lets Qt instantiate the QAccessibleInterface
157  * for the TerminalDisplay.
158  */
accessibleInterfaceFactory(const QString & key,QObject * object)159 QAccessibleInterface *accessibleInterfaceFactory(const QString &key, QObject *object)
160 {
161     Q_UNUSED(key)
162     if (auto *display = qobject_cast<TerminalDisplay *>(object)) {
163         return new TerminalDisplayAccessible(display);
164     }
165     return nullptr;
166 }
167 
168 #endif
169 }
170 
171 /* ------------------------------------------------------------------------- */
172 /*                                                                           */
173 /*                         Constructor / Destructor                          */
174 /*                                                                           */
175 /* ------------------------------------------------------------------------- */
176 
TerminalDisplay(QWidget * parent)177 TerminalDisplay::TerminalDisplay(QWidget *parent)
178     : QWidget(parent)
179     , _screenWindow(nullptr)
180     , _verticalLayout(new QVBoxLayout(this))
181     , _lines(1)
182     , _columns(1)
183     , _prevCharacterLine(-1)
184     , _prevCharacterColumn(-1)
185     , _usedLines(1)
186     , _usedColumns(1)
187     , _contentRect(QRect())
188     , _image(nullptr)
189     , _imageSize(0)
190     , _lineProperties(QVector<LineProperty>())
191     , _randomSeed(0)
192     , _resizing(false)
193     , _showTerminalSizeHint(true)
194     , _bidiEnabled(false)
195     , _usesMouseTracking(false)
196     , _bracketedPasteMode(false)
197     , _iPntSel(QPoint(-1, -1))
198     , _pntSel(QPoint(-1, -1))
199     , _tripleSelBegin(QPoint(-1, -1))
200     , _actSel(0)
201     , _wordSelectionMode(false)
202     , _lineSelectionMode(false)
203     , _preserveLineBreaks(true)
204     , _columnSelectionMode(false)
205     , _autoCopySelectedText(false)
206     , _copyTextAsHTML(true)
207     , _middleClickPasteMode(Enum::PasteFromX11Selection)
208     , _wordCharacters(QStringLiteral(":@-./_~"))
209     , _bell(Enum::NotifyBell)
210     , _allowBlinkingText(true)
211     , _allowBlinkingCursor(false)
212     , _textBlinking(false)
213     , _cursorBlinking(false)
214     , _hasTextBlinker(false)
215     , _openLinksByDirectClick(false)
216     , _ctrlRequiredForDrag(true)
217     , _dropUrlsAsText(false)
218     , _tripleClickMode(Enum::SelectWholeLine)
219     , _possibleTripleClick(false)
220     , _resizeWidget(nullptr)
221     , _resizeTimer(nullptr)
222     , _flowControlWarningEnabled(false)
223     , _outputSuspendedMessageWidget(nullptr)
224     , _size(QSize())
225     , _wallpaper(nullptr)
226     , _filterChain(new TerminalImageFilterChain(this))
227     , _filterUpdateRequired(true)
228     , _cursorShape(Enum::BlockCursor)
229     , _sessionController(nullptr)
230     , _trimLeadingSpaces(false)
231     , _trimTrailingSpaces(false)
232     , _mouseWheelZoom(false)
233     , _margin(1)
234     , _centerContents(false)
235     , _readOnlyMessageWidget(nullptr)
236     , _readOnly(false)
237     , _dimWhenInactive(false)
238     , _scrollWheelState(ScrollState())
239     , _searchBar(new IncrementalSearchBar(this))
240     , _headerBar(new TerminalHeaderBar(this))
241     , _searchResultRect(QRect())
242     , _drawOverlay(false)
243     , _scrollBar(nullptr)
244     , _terminalColor(nullptr)
245     , _terminalFont(std::make_unique<TerminalFont>(this))
246 {
247     // terminal applications are not designed with Right-To-Left in mind,
248     // so the layout is forced to Left-To-Right
249     setLayoutDirection(Qt::LeftToRight);
250 
251     _contentRect = QRect(_margin, _margin, 1, 1);
252 
253     // create scroll bar for scrolling output up and down
254     _scrollBar = new TerminalScrollBar(this);
255     _scrollBar->setAutoFillBackground(false);
256     // set the scroll bar's slider to occupy the whole area of the scroll bar initially
257     _scrollBar->setScroll(0, 0);
258     _scrollBar->setCursor(Qt::ArrowCursor);
259     _headerBar->setCursor(Qt::ArrowCursor);
260     connect(_headerBar, &TerminalHeaderBar::requestToggleExpansion, this, &Konsole::TerminalDisplay::requestToggleExpansion);
261     connect(_headerBar, &TerminalHeaderBar::requestMoveToNewTab, this, [this] {
262         requestMoveToNewTab(this);
263     });
264     connect(_scrollBar, &QScrollBar::sliderMoved, this, &Konsole::TerminalDisplay::viewScrolledByUser);
265 
266     // setup timers for blinking text
267     _blinkTextTimer = new QTimer(this);
268     _blinkTextTimer->setInterval(TEXT_BLINK_DELAY);
269     connect(_blinkTextTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkTextEvent);
270 
271     // setup timers for blinking cursor
272     _blinkCursorTimer = new QTimer(this);
273     _blinkCursorTimer->setInterval(QApplication::cursorFlashTime() / 2);
274     connect(_blinkCursorTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkCursorEvent);
275 
276     // hide mouse cursor on keystroke or idle
277     KCursor::setAutoHideCursor(this, true);
278     setMouseTracking(true);
279 
280     setUsesMouseTracking(false);
281     setBracketedPasteMode(false);
282 
283     // Enable drag and drop support
284     setAcceptDrops(true);
285     _dragInfo.state = diNone;
286 
287     setFocusPolicy(Qt::WheelFocus);
288 
289     // enable input method support
290     setAttribute(Qt::WA_InputMethodEnabled, true);
291 
292     // this is an important optimization, it tells Qt
293     // that TerminalDisplay will handle repainting its entire area.
294     setAttribute(Qt::WA_OpaquePaintEvent);
295 
296     // Add the stretch item once, the KMessageWidgets are inserted at index 0.
297     _verticalLayout->addWidget(_headerBar);
298     _verticalLayout->addStretch();
299     _verticalLayout->setSpacing(0);
300     _verticalLayout->setContentsMargins(0, 0, 0, 0);
301     setLayout(_verticalLayout);
302     new AutoScrollHandler(this);
303 
304     // Keep this last
305     CompositeWidgetFocusWatcher *focusWatcher = new CompositeWidgetFocusWatcher(this);
306     connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged, this, [this](bool focused) {
307         _hasCompositeFocus = focused;
308     });
309     connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged, this, &TerminalDisplay::compositeFocusChanged);
310     connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged, _headerBar, &TerminalHeaderBar::setFocusIndicatorState);
311 
312     connect(&_bell, &TerminalBell::visualBell, this, [this] {
313         _terminalColor->visualBell();
314     });
315 
316 #ifndef QT_NO_ACCESSIBILITY
317     QAccessible::installFactory(Konsole::accessibleInterfaceFactory);
318 #endif
319 
320     connect(KonsoleSettings::self(), &KonsoleSettings::configChanged, this, &TerminalDisplay::setupHeaderVisibility);
321 
322     _terminalColor = new TerminalColor(this);
323     connect(_terminalColor, &TerminalColor::onPalette, _scrollBar, &TerminalScrollBar::updatePalette);
324 
325     _terminalPainter = new TerminalPainter(this);
326     connect(this, &TerminalDisplay::drawContents, _terminalPainter, &TerminalPainter::drawContents);
327     connect(this, &TerminalDisplay::drawCurrentResultRect, _terminalPainter, &TerminalPainter::drawCurrentResultRect);
328     connect(this, &TerminalDisplay::highlightScrolledLines, _terminalPainter, &TerminalPainter::highlightScrolledLines);
329     connect(this, &TerminalDisplay::highlightScrolledLinesRegion, _terminalPainter, &TerminalPainter::highlightScrolledLinesRegion);
330     connect(this, &TerminalDisplay::drawBackground, _terminalPainter, &TerminalPainter::drawBackground);
331     connect(this, &TerminalDisplay::drawCharacters, _terminalPainter, &TerminalPainter::drawCharacters);
332     connect(this, &TerminalDisplay::drawInputMethodPreeditString, _terminalPainter, &TerminalPainter::drawInputMethodPreeditString);
333 
334     auto ldrawBackground = [this](QPainter &painter, const QRect &rect, const QColor &backgroundColor, bool useOpacitySetting) {
335         Q_EMIT drawBackground(painter, rect, backgroundColor, useOpacitySetting);
336     };
337     auto ldrawContents = [this](QPainter &paint, const QRect &rect, bool friendly) {
338         Q_EMIT drawContents(_image, paint, rect, friendly, _imageSize, _bidiEnabled, _lineProperties);
339     };
340     auto lgetBackgroundColor = [this]() {
341         return _terminalColor->backgroundColor();
342     };
343 
344     _printManager.reset(new KonsolePrintManager(ldrawBackground, ldrawContents, lgetBackgroundColor));
345 }
346 
~TerminalDisplay()347 TerminalDisplay::~TerminalDisplay()
348 {
349     disconnect(_blinkTextTimer);
350     disconnect(_blinkCursorTimer);
351 
352     delete[] _image;
353     delete _filterChain;
354 }
355 
setupHeaderVisibility()356 void TerminalDisplay::setupHeaderVisibility()
357 {
358     _headerBar->applyVisibilitySettings();
359     calcGeometry();
360 }
361 
hideDragTarget()362 void TerminalDisplay::hideDragTarget()
363 {
364     _drawOverlay = false;
365     update();
366 }
367 
showDragTarget(const QPoint & cursorPos)368 void TerminalDisplay::showDragTarget(const QPoint &cursorPos)
369 {
370     using EdgeDistance = std::pair<int, Qt::Edge>;
371     auto closerToEdge = std::min<EdgeDistance>(
372         {{cursorPos.x(), Qt::LeftEdge}, {cursorPos.y(), Qt::TopEdge}, {width() - cursorPos.x(), Qt::RightEdge}, {height() - cursorPos.y(), Qt::BottomEdge}},
373         [](const EdgeDistance &left, const EdgeDistance &right) -> bool {
374             return left.first < right.first;
375         });
376     if (_overlayEdge == closerToEdge.second) {
377         return;
378     }
379     _overlayEdge = closerToEdge.second;
380     _drawOverlay = true;
381     update();
382 }
383 
384 /* ------------------------------------------------------------------------- */
385 /*                                                                           */
386 /*                             Display Operations                            */
387 /*                                                                           */
388 /* ------------------------------------------------------------------------- */
389 
setKeyboardCursorShape(Enum::CursorShapeEnum shape)390 void TerminalDisplay::setKeyboardCursorShape(Enum::CursorShapeEnum shape)
391 {
392     _cursorShape = shape;
393 }
394 
setCursorStyle(Enum::CursorShapeEnum shape,bool isBlinking)395 void TerminalDisplay::setCursorStyle(Enum::CursorShapeEnum shape, bool isBlinking)
396 {
397     setKeyboardCursorShape(shape);
398 
399     setBlinkingCursorEnabled(isBlinking);
400 
401     // when the cursor shape and blinking state are changed via the
402     // Set Cursor Style (DECSCUSR) escape sequences in vim, and if the
403     // cursor isn't set to blink, the cursor shape doesn't actually
404     // change until the cursor is moved by the user; calling update()
405     // makes the cursor shape get updated sooner.
406     if (!isBlinking) {
407         update();
408     }
409 }
resetCursorStyle()410 void TerminalDisplay::resetCursorStyle()
411 {
412     Q_ASSERT(_sessionController != nullptr);
413     Q_ASSERT(!_sessionController->session().isNull());
414 
415     Profile::Ptr currentProfile = SessionManager::instance()->sessionProfile(_sessionController->session());
416 
417     if (currentProfile != nullptr) {
418         auto shape = static_cast<Enum::CursorShapeEnum>(currentProfile->property<int>(Profile::CursorShape));
419 
420         setKeyboardCursorShape(shape);
421         setBlinkingCursorEnabled(currentProfile->blinkingCursorEnabled());
422     }
423 }
424 
setWallpaper(const ColorSchemeWallpaper::Ptr & p)425 void TerminalDisplay::setWallpaper(const ColorSchemeWallpaper::Ptr &p)
426 {
427     _wallpaper = p;
428 }
429 
scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode,int amount)430 void TerminalDisplay::scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode, int amount)
431 {
432     _screenWindow->scrollBy(mode, amount, _scrollBar->scrollFullPage());
433     _screenWindow->setTrackOutput(_screenWindow->atEndOfOutput());
434     updateImage();
435     viewScrolledByUser();
436 }
437 
setRandomSeed(uint randomSeed)438 void TerminalDisplay::setRandomSeed(uint randomSeed)
439 {
440     _randomSeed = randomSeed;
441 }
randomSeed() const442 uint TerminalDisplay::randomSeed() const
443 {
444     return _randomSeed;
445 }
446 
processFilters()447 void TerminalDisplay::processFilters()
448 {
449     if (_screenWindow.isNull()) {
450         return;
451     }
452 
453     if (!_filterUpdateRequired) {
454         return;
455     }
456 
457     const QRegion preUpdateHotSpots = _filterChain->hotSpotRegion();
458 
459     // use _screenWindow->getImage() here rather than _image because
460     // other classes may call processFilters() when this display's
461     // ScreenWindow emits a scrolled() signal - which will happen before
462     // updateImage() is called on the display and therefore _image is
463     // out of date at this point
464     _filterChain->setImage(_screenWindow->getImage(), _screenWindow->windowLines(), _screenWindow->windowColumns(), _screenWindow->getLineProperties());
465     _filterChain->process();
466 
467     const QRegion postUpdateHotSpots = _filterChain->hotSpotRegion();
468 
469     update(preUpdateHotSpots | postUpdateHotSpots);
470     _filterUpdateRequired = false;
471 }
472 
updateImage()473 void TerminalDisplay::updateImage()
474 {
475     if (_screenWindow.isNull()) {
476         return;
477     }
478 
479     // Better control over screen resizing visual glitches
480     _screenWindow->updateCurrentLine();
481 
482     // optimization - scroll the existing image where possible and
483     // avoid expensive text drawing for parts of the image that
484     // can simply be moved up or down
485     // disable this shortcut for transparent konsole with scaled pixels, otherwise we get rendering artifacts, see BUG 350651
486     if (!(WindowSystemInfo::HAVE_TRANSPARENCY && (qApp->devicePixelRatio() > 1.0)) && _wallpaper->isNull() && !_searchBar->isVisible()) {
487         // if the flow control warning is enabled this will interfere with the
488         // scrolling optimizations and cause artifacts.  the simple solution here
489         // is to just disable the optimization whilst it is visible
490         if (!((_outputSuspendedMessageWidget != nullptr) && _outputSuspendedMessageWidget->isVisible())
491             && !((_readOnlyMessageWidget != nullptr) && _readOnlyMessageWidget->isVisible())) {
492             // hide terminal size label to prevent it being scrolled and show again after scroll
493             const bool viewResizeWidget = (_resizeWidget != nullptr) && _resizeWidget->isVisible();
494             if (viewResizeWidget) {
495                 _resizeWidget->hide();
496             }
497             _scrollBar->scrollImage(_screenWindow->scrollCount(), _screenWindow->scrollRegion(), _image, _imageSize);
498             if (viewResizeWidget) {
499                 _resizeWidget->show();
500             }
501         }
502     }
503 
504     if (_image == nullptr) {
505         // Create _image.
506         // The emitted changedContentSizeSignal also leads to getImage being recreated, so do this first.
507         updateImageSize();
508     }
509 
510     Character *const newimg = _screenWindow->getImage();
511     const int lines = _screenWindow->windowLines();
512     const int columns = _screenWindow->windowColumns();
513     QVector<LineProperty> newLineProperties = _screenWindow->getLineProperties();
514 
515     _scrollBar->setScroll(_screenWindow->currentLine(), _screenWindow->lineCount());
516 
517     Q_ASSERT(_usedLines <= _lines);
518     Q_ASSERT(_usedColumns <= _columns);
519 
520     int y;
521     int x;
522     int len;
523 
524     const QPoint tL = contentsRect().topLeft();
525     const int tLx = tL.x();
526     const int tLy = tL.y();
527     _hasTextBlinker = false;
528 
529     CharacterColor cf; // undefined
530 
531     const int linesToUpdate = qBound(0, lines, _lines);
532     const int columnsToUpdate = qBound(0, columns, _columns);
533 
534     auto dirtyMask = new char[columnsToUpdate + 2];
535     QRegion dirtyRegion;
536 
537     // debugging variable, this records the number of lines that are found to
538     // be 'dirty' ( ie. have changed from the old _image to the new _image ) and
539     // which therefore need to be repainted
540     int dirtyLineCount = 0;
541 
542     for (y = 0; y < linesToUpdate; ++y) {
543         const Character *currentLine = &_image[y * _columns];
544         const Character *const newLine = &newimg[y * columns];
545 
546         bool updateLine = false;
547 
548         // The dirty mask indicates which characters need repainting. We also
549         // mark surrounding neighbors dirty, in case the character exceeds
550         // its cell boundaries
551         memset(dirtyMask, 0, columnsToUpdate + 2);
552 
553         for (x = 0; x < columnsToUpdate; ++x) {
554             if (newLine[x] != currentLine[x]) {
555                 dirtyMask[x] = 1;
556             }
557         }
558 
559         if (!_resizing) { // not while _resizing, we're expecting a paintEvent
560             for (x = 0; x < columnsToUpdate; ++x) {
561                 _hasTextBlinker |= (newLine[x].rendition & RE_BLINK);
562 
563                 // Start drawing if this character or the next one differs.
564                 // We also take the next one into account to handle the situation
565                 // where characters exceed their cell width.
566                 if (dirtyMask[x] != 0) {
567                     if (newLine[x + 0].character == 0u) {
568                         continue;
569                     }
570                     const bool lineDraw = LineBlockCharacters::canDraw(newLine[x + 0].character);
571                     const bool doubleWidth = (x + 1 == columnsToUpdate) ? false : (newLine[x + 1].character == 0);
572                     const RenditionFlags cr = newLine[x].rendition;
573                     const CharacterColor clipboard = newLine[x].backgroundColor;
574                     if (newLine[x].foregroundColor != cf) {
575                         cf = newLine[x].foregroundColor;
576                     }
577                     const int lln = columnsToUpdate - x;
578                     for (len = 1; len < lln; ++len) {
579                         const Character &ch = newLine[x + len];
580 
581                         if (ch.character == 0u) {
582                             continue; // Skip trailing part of multi-col chars.
583                         }
584 
585                         const bool nextIsDoubleWidth = (x + len + 1 == columnsToUpdate) ? false : (newLine[x + len + 1].character == 0);
586 
587                         if (ch.foregroundColor != cf || ch.backgroundColor != clipboard || (ch.rendition & ~RE_EXTENDED_CHAR) != (cr & ~RE_EXTENDED_CHAR)
588                             || (dirtyMask[x + len] == 0) || LineBlockCharacters::canDraw(ch.character) != lineDraw || nextIsDoubleWidth != doubleWidth) {
589                             break;
590                         }
591                     }
592                     updateLine = true;
593                     x += len - 1;
594                 }
595             }
596         }
597 
598         if (y >= _lineProperties.count() || y >= newLineProperties.count() || _lineProperties[y] != newLineProperties[y]) {
599             updateLine = true;
600         }
601 
602         // if the characters on the line are different in the old and the new _image
603         // then this line must be repainted.
604         if (updateLine) {
605             dirtyLineCount++;
606 
607             // add the area occupied by this line to the region which needs to be
608             // repainted
609             QRect dirtyRect = QRect(_contentRect.left() + tLx,
610                                     _contentRect.top() + tLy + _terminalFont->fontHeight() * y,
611                                     _terminalFont->fontWidth() * columnsToUpdate,
612                                     _terminalFont->fontHeight());
613 
614             dirtyRegion |= dirtyRect;
615         }
616 
617         // replace the line of characters in the old _image with the
618         // current line of the new _image
619         memcpy((void *)currentLine, (const void *)newLine, columnsToUpdate * sizeof(Character));
620     }
621     _lineProperties = newLineProperties;
622 
623     // if the new _image is smaller than the previous _image, then ensure that the area
624     // outside the new _image is cleared
625     if (linesToUpdate < _usedLines) {
626         dirtyRegion |= QRect(_contentRect.left() + tLx,
627                              _contentRect.top() + tLy + _terminalFont->fontHeight() * linesToUpdate,
628                              _terminalFont->fontWidth() * _columns,
629                              _terminalFont->fontHeight() * (_usedLines - linesToUpdate));
630     }
631     _usedLines = linesToUpdate;
632 
633     if (columnsToUpdate < _usedColumns) {
634         dirtyRegion |= QRect(_contentRect.left() + tLx + columnsToUpdate * _terminalFont->fontWidth(),
635                              _contentRect.top() + tLy,
636                              _terminalFont->fontWidth() * (_usedColumns - columnsToUpdate),
637                              _terminalFont->fontHeight() * _lines);
638     }
639     _usedColumns = columnsToUpdate;
640 
641     dirtyRegion |= _inputMethodData.previousPreeditRect;
642 
643     if ((_screenWindow->currentResultLine() != -1) && (_screenWindow->scrollCount() != 0)) {
644         // De-highlight previous result region
645         dirtyRegion |= _searchResultRect;
646         // Highlight new result region
647         dirtyRegion |= QRect(0,
648                              _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _terminalFont->fontHeight(),
649                              _columns * _terminalFont->fontWidth(),
650                              _terminalFont->fontHeight());
651     }
652 
653     if (_scrollBar->highlightScrolledLines().isEnabled()) {
654         dirtyRegion |= Q_EMIT highlightScrolledLinesRegion(dirtyRegion.isEmpty(), _scrollBar);
655     }
656     _screenWindow->resetScrollCount();
657 
658     // update the parts of the display which have changed
659     update(dirtyRegion);
660 
661     if (_allowBlinkingText && _hasTextBlinker && !_blinkTextTimer->isActive()) {
662         _blinkTextTimer->start();
663     }
664     if (!_hasTextBlinker && _blinkTextTimer->isActive()) {
665         _blinkTextTimer->stop();
666         _textBlinking = false;
667     }
668     delete[] dirtyMask;
669 
670 #ifndef QT_NO_ACCESSIBILITY
671     QAccessibleEvent dataChangeEvent(this, QAccessible::VisibleDataChanged);
672     QAccessible::updateAccessibility(&dataChangeEvent);
673     QAccessibleTextCursorEvent cursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX());
674     QAccessible::updateAccessibility(&cursorEvent);
675 #endif
676 }
677 
showResizeNotification()678 void TerminalDisplay::showResizeNotification()
679 {
680     if (_showTerminalSizeHint && isVisible()) {
681         if (_resizeWidget == nullptr) {
682             _resizeWidget = new QLabel(i18n("Size: XXX x XXX"), this);
683             _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().boundingRect(i18n("Size: XXX x XXX")).width());
684             _resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height());
685             _resizeWidget->setAlignment(Qt::AlignCenter);
686 
687             _resizeWidget->setStyleSheet(QStringLiteral("background-color:palette(window);border-style:solid;border-width:1px;border-color:palette(dark)"));
688 
689             _resizeTimer = new QTimer(this);
690             _resizeTimer->setInterval(SIZE_HINT_DURATION);
691             _resizeTimer->setSingleShot(true);
692             connect(_resizeTimer, &QTimer::timeout, _resizeWidget, &QLabel::hide);
693         }
694         QString sizeStr = i18n("Size: %1 x %2", _columns, _lines);
695         _resizeWidget->setText(sizeStr);
696         _resizeWidget->move((width() - _resizeWidget->width()) / 2, (height() - _resizeWidget->height()) / 2 + 20);
697         _resizeWidget->show();
698         _resizeTimer->start();
699     }
700 }
701 
paintEvent(QPaintEvent * pe)702 void TerminalDisplay::paintEvent(QPaintEvent *pe)
703 {
704     QPainter paint(this);
705 
706     // Determine which characters should be repainted (1 region unit = 1 character)
707     QRegion dirtyImageRegion;
708     const QRegion region = pe->region() & contentsRect();
709 
710     for (const QRect &rect : region) {
711         dirtyImageRegion += widgetToImage(rect);
712         Q_EMIT drawBackground(paint, rect, _terminalColor->backgroundColor(), true /* use opacity setting */);
713     }
714 
715     if (_displayVerticalLine) {
716         const int fontWidth = _terminalFont->fontWidth();
717         const int x = (fontWidth / 2) + (fontWidth * _displayVerticalLineAtChar);
718         const QColor lineColor = _terminalColor->foregroundColor();
719 
720         paint.setPen(lineColor);
721         paint.drawLine(QPoint(x, 0), QPoint(x, height()));
722     }
723 
724     // only turn on text anti-aliasing, never turn on normal antialiasing
725     // set https://bugreports.qt.io/browse/QTBUG-66036
726     paint.setRenderHint(QPainter::TextAntialiasing, _terminalFont->antialiasText());
727 
728     for (const QRect &rect : qAsConst(dirtyImageRegion)) {
729         Q_EMIT drawContents(_image, paint, rect, false, _imageSize, _bidiEnabled, _lineProperties);
730     }
731     Q_EMIT drawCurrentResultRect(paint, _searchResultRect);
732     if (_scrollBar->highlightScrolledLines().isEnabled()) {
733         Q_EMIT highlightScrolledLines(paint, _scrollBar->highlightScrolledLines().isTimerActive(), _scrollBar->highlightScrolledLines().rect());
734     }
735     Q_EMIT drawInputMethodPreeditString(paint, preeditRect(), _inputMethodData, _image);
736     paintFilters(paint);
737 
738     const bool drawDimmed = _dimWhenInactive && !hasFocus();
739     if (drawDimmed) {
740         const QColor dimColor(0, 0, 0, _dimValue);
741         for (const QRect &rect : region) {
742             paint.fillRect(rect, dimColor);
743         }
744     }
745 
746     if (_drawOverlay) {
747         const auto y = _headerBar->isVisible() ? _headerBar->height() : 0;
748         const auto rect = _overlayEdge == Qt::LeftEdge ? QRect(0, y, width() / 2, height())
749             : _overlayEdge == Qt::TopEdge              ? QRect(0, y, width(), height() / 2)
750             : _overlayEdge == Qt::RightEdge            ? QRect(width() - width() / 2, y, width() / 2, height())
751                                                        : QRect(0, height() - height() / 2, width(), height() / 2);
752 
753         paint.setRenderHint(QPainter::Antialiasing);
754         paint.setPen(Qt::NoPen);
755         paint.setBrush(QColor(100, 100, 100, 127));
756         paint.drawRect(rect);
757     }
758 }
759 
cursorPosition() const760 QPoint TerminalDisplay::cursorPosition() const
761 {
762     if (!_screenWindow.isNull()) {
763         return _screenWindow->cursorPosition();
764     } else {
765         return {0, 0};
766     }
767 }
768 
isCursorOnDisplay() const769 bool TerminalDisplay::isCursorOnDisplay() const
770 {
771     return cursorPosition().x() < _columns && cursorPosition().y() < _lines;
772 }
773 
filterChain() const774 FilterChain *TerminalDisplay::filterChain() const
775 {
776     return _filterChain;
777 }
778 
paintFilters(QPainter & painter)779 void TerminalDisplay::paintFilters(QPainter &painter)
780 {
781     if (_filterUpdateRequired) {
782         return;
783     }
784 
785     _filterChain->paint(this, painter);
786 }
787 
imageToWidget(const QRect & imageArea) const788 QRect TerminalDisplay::imageToWidget(const QRect &imageArea) const
789 {
790     QRect result;
791     const int fontWidth = _terminalFont->fontWidth();
792     const int fontHeight = _terminalFont->fontHeight();
793     result.setLeft(_contentRect.left() + fontWidth * imageArea.left());
794     result.setTop(_contentRect.top() + fontHeight * imageArea.top());
795     result.setWidth(fontWidth * imageArea.width());
796     result.setHeight(fontHeight * imageArea.height());
797 
798     return result;
799 }
800 
widgetToImage(const QRect & widgetArea) const801 QRect TerminalDisplay::widgetToImage(const QRect &widgetArea) const
802 {
803     QRect result;
804     const int fontWidth = _terminalFont->fontWidth();
805     const int fontHeight = _terminalFont->fontHeight();
806     result.setLeft(qBound(0, (widgetArea.left() - contentsRect().left() - _contentRect.left()) / fontWidth, _usedColumns - 1));
807     result.setTop(qBound(0, (widgetArea.top() - contentsRect().top() - _contentRect.top()) / fontHeight, _usedLines - 1));
808     result.setRight(qBound(0, (widgetArea.right() - contentsRect().left() - _contentRect.left()) / fontWidth, _usedColumns - 1));
809     result.setBottom(qBound(0, (widgetArea.bottom() - contentsRect().top() - _contentRect.top()) / fontHeight, _usedLines - 1));
810     return result;
811 }
812 
813 /* ------------------------------------------------------------------------- */
814 /*                                                                           */
815 /*                          Blinking Text & Cursor                           */
816 /*                                                                           */
817 /* ------------------------------------------------------------------------- */
818 
setBlinkingCursorEnabled(bool blink)819 void TerminalDisplay::setBlinkingCursorEnabled(bool blink)
820 {
821     _allowBlinkingCursor = blink;
822 
823     if (blink && !_blinkCursorTimer->isActive()) {
824         _blinkCursorTimer->start();
825     }
826 
827     if (!blink && _blinkCursorTimer->isActive()) {
828         _blinkCursorTimer->stop();
829         if (_cursorBlinking) {
830             // if cursor is blinking(hidden), blink it again to make it show
831             _cursorBlinking = false;
832             updateCursor();
833         }
834         Q_ASSERT(!_cursorBlinking);
835     }
836 }
837 
setBlinkingTextEnabled(bool blink)838 void TerminalDisplay::setBlinkingTextEnabled(bool blink)
839 {
840     _allowBlinkingText = blink;
841 
842     if (blink && !_blinkTextTimer->isActive()) {
843         _blinkTextTimer->start();
844     }
845 
846     if (!blink && _blinkTextTimer->isActive()) {
847         _blinkTextTimer->stop();
848         _textBlinking = false;
849     }
850 }
851 
focusOutEvent(QFocusEvent *)852 void TerminalDisplay::focusOutEvent(QFocusEvent *)
853 {
854     // trigger a repaint of the cursor so that it is both:
855     //
856     //   * visible (in case it was hidden during blinking)
857     //   * drawn in a focused out state
858     _cursorBlinking = false;
859     updateCursor();
860 
861     // suppress further cursor blinking
862     _blinkCursorTimer->stop();
863     Q_ASSERT(!_cursorBlinking);
864 
865     // if text is blinking (hidden), blink it again to make it shown
866     if (_textBlinking) {
867         blinkTextEvent();
868     }
869 
870     // suppress further text blinking
871     _blinkTextTimer->stop();
872     Q_ASSERT(!_textBlinking);
873 }
874 
focusInEvent(QFocusEvent *)875 void TerminalDisplay::focusInEvent(QFocusEvent *)
876 {
877     if (_allowBlinkingCursor) {
878         _blinkCursorTimer->start();
879     }
880 
881     updateCursor();
882 
883     if (_allowBlinkingText && _hasTextBlinker) {
884         _blinkTextTimer->start();
885     }
886 }
887 
blinkTextEvent()888 void TerminalDisplay::blinkTextEvent()
889 {
890     Q_ASSERT(_allowBlinkingText);
891 
892     _textBlinking = !_textBlinking;
893 
894     // TODO: Optimize to only repaint the areas of the widget where there is
895     // blinking text rather than repainting the whole widget.
896     update();
897 }
898 
blinkCursorEvent()899 void TerminalDisplay::blinkCursorEvent()
900 {
901     Q_ASSERT(_allowBlinkingCursor);
902 
903     _cursorBlinking = !_cursorBlinking;
904     updateCursor();
905 }
906 
updateCursor()907 void TerminalDisplay::updateCursor()
908 {
909     if (!isCursorOnDisplay()) {
910         return;
911     }
912 
913     const int cursorLocation = loc(cursorPosition().x(), cursorPosition().y());
914     Q_ASSERT(cursorLocation < _imageSize);
915 
916     int charWidth = _image[cursorLocation].width();
917     QRect cursorRect = imageToWidget(QRect(cursorPosition(), QSize(charWidth, 1)));
918     update(cursorRect);
919 }
920 
921 /* ------------------------------------------------------------------------- */
922 /*                                                                           */
923 /*                          Geometry & Resizing                              */
924 /*                                                                           */
925 /* ------------------------------------------------------------------------- */
926 
resizeEvent(QResizeEvent * event)927 void TerminalDisplay::resizeEvent(QResizeEvent *event)
928 {
929     Q_UNUSED(event)
930 
931     if (contentsRect().isValid()) {
932         // NOTE: This calls setTabText() in TabbedViewContainer::updateTitle(),
933         // which might update the widget size again. New resizeEvent
934         // won't be called, do not rely on new sizes before this call.
935         updateImageSize();
936         updateImage();
937     }
938 
939     const auto scrollBarWidth = _scrollBar->scrollBarPosition() != Enum::ScrollBarHidden ? _scrollBar->width() : 0;
940     const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0;
941 
942     const auto x = width() - scrollBarWidth - _searchBar->width();
943     const auto y = headerHeight;
944     _searchBar->move(x, y);
945 }
946 
propagateSize()947 void TerminalDisplay::propagateSize()
948 {
949     if (_image != nullptr) {
950         updateImageSize();
951     }
952 }
953 
updateImageSize()954 void TerminalDisplay::updateImageSize()
955 {
956     Character *oldImage = _image;
957     const int oldLines = _lines;
958     const int oldColumns = _columns;
959 
960     makeImage();
961 
962     if (oldImage != nullptr) {
963         // copy the old image to reduce flicker
964         int lines = qMin(oldLines, _lines);
965         int columns = qMin(oldColumns, _columns);
966         for (int line = 0; line < lines; line++) {
967             memcpy((void *)&_image[_columns * line], (void *)&oldImage[oldColumns * line], columns * sizeof(Character));
968         }
969         delete[] oldImage;
970     }
971 
972     if (!_screenWindow.isNull()) {
973         _screenWindow->setWindowLines(_lines);
974     }
975 
976     _resizing = (oldLines != _lines) || (oldColumns != _columns);
977 
978     if (_resizing) {
979         showResizeNotification();
980         Q_EMIT changedContentSizeSignal(_contentRect.height(), _contentRect.width()); // expose resizeEvent
981     }
982 
983     _resizing = false;
984 }
985 
makeImage()986 void TerminalDisplay::makeImage()
987 {
988     _wallpaper->load();
989 
990     calcGeometry();
991 
992     // confirm that array will be of non-zero size, since the painting code
993     // assumes a non-zero array length
994     Q_ASSERT(_lines > 0 && _columns > 0);
995     Q_ASSERT(_usedLines <= _lines && _usedColumns <= _columns);
996 
997     _imageSize = _lines * _columns;
998 
999     _image = new Character[_imageSize];
1000 
1001     clearImage();
1002 }
1003 
clearImage()1004 void TerminalDisplay::clearImage()
1005 {
1006     std::fill(_image, _image + _imageSize, Screen::DefaultChar);
1007 }
1008 
calcGeometry()1009 void TerminalDisplay::calcGeometry()
1010 {
1011     const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0;
1012 
1013     _scrollBar->resize(_scrollBar->sizeHint().width(), // width
1014                        contentsRect().height() - headerHeight // height
1015     );
1016 
1017     _contentRect = contentsRect().adjusted(
1018         _margin + (_scrollBar->highlightScrolledLines().isEnabled() ? _scrollBar->highlightScrolledLines().HIGHLIGHT_SCROLLED_LINES_WIDTH : 0),
1019         _margin,
1020         -_margin - (_scrollBar->highlightScrolledLines().isEnabled() ? _scrollBar->highlightScrolledLines().HIGHLIGHT_SCROLLED_LINES_WIDTH : 0),
1021         -_margin);
1022 
1023     switch (_scrollBar->scrollBarPosition()) {
1024     case Enum::ScrollBarHidden:
1025         break;
1026     case Enum::ScrollBarLeft:
1027         _contentRect.setLeft(_contentRect.left() + _scrollBar->width());
1028         _scrollBar->move(contentsRect().left(), contentsRect().top() + headerHeight);
1029         break;
1030     case Enum::ScrollBarRight:
1031         _contentRect.setRight(_contentRect.right() - _scrollBar->width());
1032         _scrollBar->move(contentsRect().left() + contentsRect().width() - _scrollBar->width(), contentsRect().top() + headerHeight);
1033         break;
1034     }
1035 
1036     _contentRect.setTop(_contentRect.top() + headerHeight);
1037 
1038     int fontWidth = _terminalFont->fontWidth();
1039 
1040     // ensure that display is always at least one column wide
1041     _columns = qMax(1, _contentRect.width() / fontWidth);
1042     _usedColumns = qMin(_usedColumns, _columns);
1043 
1044     // ensure that display is always at least one line high
1045     _lines = qMax(1, _contentRect.height() / _terminalFont->fontHeight());
1046     _usedLines = qMin(_usedLines, _lines);
1047 
1048     if (_centerContents) {
1049         QSize unusedPixels = _contentRect.size() - QSize(_columns * fontWidth, _lines * _terminalFont->fontHeight());
1050         _contentRect.adjust(unusedPixels.width() / 2, unusedPixels.height() / 2, 0, 0);
1051     }
1052 }
1053 
1054 // calculate the needed size, this must be synced with calcGeometry()
setSize(int columns,int lines)1055 void TerminalDisplay::setSize(int columns, int lines)
1056 {
1057     const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->sizeHint().width();
1058     const int horizontalMargin = _margin * 2;
1059     const int verticalMargin = _margin * 2;
1060 
1061     QSize newSize = QSize(horizontalMargin + scrollBarWidth + (columns * _terminalFont->fontWidth()), verticalMargin + (lines * _terminalFont->fontHeight()));
1062 
1063     if (newSize != size()) {
1064         _size = newSize;
1065         updateGeometry();
1066     }
1067 }
1068 
sizeHint() const1069 QSize TerminalDisplay::sizeHint() const
1070 {
1071     return _size;
1072 }
1073 
1074 // showEvent and hideEvent are reimplemented here so that it appears to other classes that the
1075 // display has been resized when the display is hidden or shown.
1076 //
1077 // TODO: Perhaps it would be better to have separate signals for show and hide instead of using
1078 // the same signal as the one for a content size change
showEvent(QShowEvent *)1079 void TerminalDisplay::showEvent(QShowEvent *)
1080 {
1081     propagateSize();
1082     Q_EMIT changedContentSizeSignal(_contentRect.height(), _contentRect.width());
1083 }
hideEvent(QHideEvent *)1084 void TerminalDisplay::hideEvent(QHideEvent *)
1085 {
1086     Q_EMIT changedContentSizeSignal(_contentRect.height(), _contentRect.width());
1087 }
1088 
setMargin(int margin)1089 void TerminalDisplay::setMargin(int margin)
1090 {
1091     if (margin < 0) {
1092         margin = 0;
1093     }
1094     _margin = margin;
1095     updateImageSize();
1096 }
1097 
setCenterContents(bool enable)1098 void TerminalDisplay::setCenterContents(bool enable)
1099 {
1100     _centerContents = enable;
1101     calcGeometry();
1102     update();
1103 }
1104 
1105 /* ------------------------------------------------------------------------- */
1106 /*                                                                           */
1107 /*                                  Mouse                                    */
1108 /*                                                                           */
1109 /* ------------------------------------------------------------------------- */
mousePressEvent(QMouseEvent * ev)1110 void TerminalDisplay::mousePressEvent(QMouseEvent *ev)
1111 {
1112     if (!contentsRect().contains(ev->pos())) {
1113         return;
1114     }
1115 
1116     if (!_screenWindow) {
1117         return;
1118     }
1119 
1120     if (_possibleTripleClick && (ev->button() == Qt::LeftButton)) {
1121         mouseTripleClickEvent(ev);
1122         return;
1123     }
1124 
1125     // Ignore clicks on the message widget
1126     if (_readOnlyMessageWidget != nullptr && _readOnlyMessageWidget->isVisible() && _readOnlyMessageWidget->frameGeometry().contains(ev->pos())) {
1127         return;
1128     }
1129 
1130     if (_outputSuspendedMessageWidget != nullptr && _outputSuspendedMessageWidget->isVisible()
1131         && _outputSuspendedMessageWidget->frameGeometry().contains(ev->pos())) {
1132         return;
1133     }
1134 
1135     auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !_usesMouseTracking);
1136     QPoint pos = QPoint(charColumn, charLine);
1137 
1138     processFilters();
1139 
1140     _filterChain->mouseMoveEvent(this, ev, charLine, charColumn);
1141     auto hotSpotClick = _filterChain->hotSpotAt(charLine, charColumn);
1142     if (hotSpotClick && hotSpotClick->hasDragOperation() && ev->modifiers() & Qt::Modifier::ALT) {
1143         hotSpotClick->startDrag();
1144         return;
1145     }
1146 
1147     if (ev->button() == Qt::LeftButton) {
1148         // request the software keyboard, if any
1149         if (qApp->autoSipEnabled()) {
1150             auto behavior = QStyle::RequestSoftwareInputPanel(style()->styleHint(QStyle::SH_RequestSoftwareInputPanel));
1151             if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) {
1152                 QEvent event(QEvent::RequestSoftwareInputPanel);
1153                 QApplication::sendEvent(this, &event);
1154             }
1155         }
1156 
1157         if (!ev->modifiers()) {
1158             _lineSelectionMode = false;
1159             _wordSelectionMode = false;
1160         }
1161 
1162         // The user clicked inside selected text
1163         bool selected = _screenWindow->isSelected(pos.x(), pos.y());
1164 
1165         // Drag only when the Control key is held
1166         if ((!_ctrlRequiredForDrag || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && selected) {
1167             _dragInfo.state = diPending;
1168             _dragInfo.start = ev->pos();
1169         } else {
1170             // No reason to ever start a drag event
1171             _dragInfo.state = diNone;
1172 
1173             _preserveLineBreaks = !(((ev->modifiers() & Qt::ControlModifier) != 0u) && !(ev->modifiers() & Qt::AltModifier));
1174             _columnSelectionMode = ((ev->modifiers() & Qt::AltModifier) != 0u) && ((ev->modifiers() & Qt::ControlModifier) != 0u);
1175 
1176             // There are a couple of use cases when selecting text :
1177             // Normal buffer or Alternate buffer when not using Mouse Tracking:
1178             //  select text or extendSelection or columnSelection or columnSelection + extendSelection
1179             //
1180             // Alternate buffer when using Mouse Tracking and with Shift pressed:
1181             //  select text or columnSelection
1182             if (!_usesMouseTracking && ((ev->modifiers() == Qt::ShiftModifier) || (((ev->modifiers() & Qt::ShiftModifier) != 0u) && _columnSelectionMode))) {
1183                 extendSelection(ev->pos());
1184             } else if ((!_usesMouseTracking && !((ev->modifiers() & Qt::ShiftModifier)))
1185                        || (_usesMouseTracking && ((ev->modifiers() & Qt::ShiftModifier) != 0u))) {
1186                 _screenWindow->clearSelection();
1187 
1188                 pos.ry() += _scrollBar->value();
1189                 _iPntSel = _pntSel = pos;
1190                 _actSel = 1; // left mouse button pressed but nothing selected yet.
1191             } else if (_usesMouseTracking && !_readOnly) {
1192                 Q_EMIT mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0);
1193             }
1194         }
1195     } else if (ev->button() == Qt::MiddleButton) {
1196         processMidButtonClick(ev);
1197     } else if (ev->button() == Qt::RightButton) {
1198         if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) {
1199             Q_EMIT configureRequest(ev->pos());
1200         } else {
1201             if (!_readOnly) {
1202                 Q_EMIT mouseSignal(2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0);
1203             }
1204         }
1205     }
1206 }
1207 
filterActions(const QPoint & position)1208 QSharedPointer<HotSpot> TerminalDisplay::filterActions(const QPoint &position)
1209 {
1210     auto [charLine, charColumn] = getCharacterPosition(position, false);
1211     return _filterChain->hotSpotAt(charLine, charColumn);
1212 }
1213 
mouseMoveEvent(QMouseEvent * ev)1214 void TerminalDisplay::mouseMoveEvent(QMouseEvent *ev)
1215 {
1216     if (!hasFocus() && KonsoleSettings::focusFollowsMouse()) {
1217         setFocus();
1218     }
1219 
1220     auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !_usesMouseTracking);
1221 
1222     // Ignore mouse movements that don't change the character position,
1223     //   but don't ignore the ones generated by AutoScrollHandler (which
1224     //   allow to extend the selection by dragging the mouse outside the
1225     //   display).
1226     if (charLine == _prevCharacterLine && charColumn == _prevCharacterColumn && contentsRect().contains(ev->pos())) {
1227         return;
1228     }
1229 
1230     _prevCharacterLine = charLine;
1231     _prevCharacterColumn = charColumn;
1232 
1233     processFilters();
1234 
1235     _filterChain->mouseMoveEvent(this, ev, charLine, charColumn);
1236 
1237     // if the program running in the terminal is interested in Mouse Tracking
1238     // events then emit a mouse movement signal, unless the shift key is
1239     // being held down, which overrides this.
1240     if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) {
1241         if (!_readOnly) {
1242             int button = 3;
1243             if ((ev->buttons() & Qt::LeftButton) != 0u) {
1244                 button = 0;
1245             }
1246             if ((ev->buttons() & Qt::MiddleButton) != 0u) {
1247                 button = 1;
1248             }
1249             if ((ev->buttons() & Qt::RightButton) != 0u) {
1250                 button = 2;
1251             }
1252 
1253             Q_EMIT mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 1);
1254         }
1255 
1256         return;
1257     }
1258 
1259     // for auto-hiding the cursor, we need mouseTracking
1260     if (ev->buttons() == Qt::NoButton) {
1261         return;
1262     }
1263 
1264     if (_dragInfo.state == diPending) {
1265         // we had a mouse down, but haven't confirmed a drag yet
1266         // if the mouse has moved sufficiently, we will confirm
1267 
1268         const int distance = QApplication::startDragDistance();
1269         if (ev->x() > _dragInfo.start.x() + distance || ev->x() < _dragInfo.start.x() - distance || ev->y() > _dragInfo.start.y() + distance
1270             || ev->y() < _dragInfo.start.y() - distance) {
1271             // we've left the drag square, we can start a real drag operation now
1272 
1273             _screenWindow->clearSelection();
1274             doDrag();
1275         }
1276         return;
1277     } else if (_dragInfo.state == diDragging) {
1278         // this isn't technically needed because mouseMoveEvent is suppressed during
1279         // Qt drag operations, replaced by dragMoveEvent
1280         return;
1281     }
1282 
1283     if (_actSel == 0) {
1284         return;
1285     }
1286 
1287     // don't extend selection while pasting
1288     if ((ev->buttons() & Qt::MiddleButton) != 0u) {
1289         return;
1290     }
1291 
1292     extendSelection(ev->pos());
1293 }
1294 
leaveEvent(QEvent * ev)1295 void TerminalDisplay::leaveEvent(QEvent *ev)
1296 {
1297     // remove underline from an active link when cursor leaves the widget area,
1298     // also restore regular mouse cursor shape
1299     _filterChain->leaveEvent(this, ev);
1300 }
1301 
extendSelection(const QPoint & position)1302 void TerminalDisplay::extendSelection(const QPoint &position)
1303 {
1304     if (_screenWindow.isNull()) {
1305         return;
1306     }
1307 
1308     if (_iPntSel.x() < 0 || _iPntSel.y() < 0 || _pntSel.x() < 0 || _pntSel.y() < 0) {
1309         _iPntSel = _pntSel = position;
1310         return;
1311     }
1312 
1313     // if ( !contentsRect().contains(ev->pos()) ) return;
1314     const QPoint tL = contentsRect().topLeft();
1315     const int tLx = tL.x();
1316     const int tLy = tL.y();
1317     const int scroll = _scrollBar->value();
1318 
1319     // we're in the process of moving the mouse with the left button pressed
1320     // the mouse cursor will kept caught within the bounds of the text in
1321     // this widget.
1322 
1323     int linesBeyondWidget = 0;
1324 
1325     QRect textBounds(tLx + _contentRect.left(),
1326                      tLy + _contentRect.top(),
1327                      _usedColumns * _terminalFont->fontWidth() - 1,
1328                      _usedLines * _terminalFont->fontHeight() - 1);
1329 
1330     QPoint pos = position;
1331 
1332     // Adjust position within text area bounds.
1333     const QPoint oldpos = pos;
1334 
1335     pos.setX(qBound(textBounds.left(), pos.x(), textBounds.right()));
1336     pos.setY(qBound(textBounds.top(), pos.y(), textBounds.bottom()));
1337 
1338     if (oldpos.y() > textBounds.bottom()) {
1339         linesBeyondWidget = (oldpos.y() - textBounds.bottom()) / _terminalFont->fontHeight();
1340         _scrollBar->setValue(_scrollBar->value() + linesBeyondWidget + 1); // scrollforward
1341     }
1342     if (oldpos.y() < textBounds.top()) {
1343         linesBeyondWidget = (textBounds.top() - oldpos.y()) / _terminalFont->fontHeight();
1344         _scrollBar->setValue(_scrollBar->value() - linesBeyondWidget - 1); // history
1345     }
1346 
1347     auto [charLine, charColumn] = getCharacterPosition(pos, true);
1348 
1349     QPoint here = QPoint(charColumn, charLine);
1350     QPoint ohere;
1351     QPoint iPntSelCorr = _iPntSel;
1352     iPntSelCorr.ry() -= _scrollBar->value();
1353     QPoint pntSelCorr = _pntSel;
1354     pntSelCorr.ry() -= _scrollBar->value();
1355     bool swapping = false;
1356 
1357     if (_wordSelectionMode) {
1358         // Extend to word boundaries
1359         const bool left_not_right = (here.y() < iPntSelCorr.y() || (here.y() == iPntSelCorr.y() && here.x() < iPntSelCorr.x()));
1360         const bool old_left_not_right = (pntSelCorr.y() < iPntSelCorr.y() || (pntSelCorr.y() == iPntSelCorr.y() && pntSelCorr.x() < iPntSelCorr.x()));
1361         swapping = left_not_right != old_left_not_right;
1362 
1363         // Find left (left_not_right ? from here : from start of word)
1364         QPoint left = left_not_right ? here : iPntSelCorr;
1365         // Find left (left_not_right ? from end of word : from here)
1366         QPoint right = left_not_right ? iPntSelCorr : here;
1367 
1368         if (left.y() < 0 || left.y() >= _lines || left.x() < 0 || left.x() >= _columns) {
1369             left = pntSelCorr;
1370         } else {
1371             left = findWordStart(left);
1372         }
1373         if (right.y() < 0 || right.y() >= _lines || right.x() < 0 || right.x() >= _columns) {
1374             right = pntSelCorr;
1375         } else {
1376             right = findWordEnd(right);
1377         }
1378 
1379         // Pick which is start (ohere) and which is extension (here)
1380         if (left_not_right) {
1381             here = left;
1382             ohere = right;
1383         } else {
1384             here = right;
1385             ohere = left;
1386         }
1387         ohere.rx()++;
1388     }
1389 
1390     if (_lineSelectionMode) {
1391         // Extend to complete line
1392         const bool above_not_below = (here.y() < iPntSelCorr.y());
1393         if (above_not_below) {
1394             ohere = findLineEnd(iPntSelCorr);
1395             here = findLineStart(here);
1396         } else {
1397             ohere = findLineStart(iPntSelCorr);
1398             here = findLineEnd(here);
1399         }
1400 
1401         swapping = !(_tripleSelBegin == ohere);
1402         _tripleSelBegin = ohere;
1403 
1404         ohere.rx()++;
1405     }
1406 
1407     int offset = 0;
1408     if (!_wordSelectionMode && !_lineSelectionMode) {
1409         const bool left_not_right = (here.y() < iPntSelCorr.y() || (here.y() == iPntSelCorr.y() && here.x() < iPntSelCorr.x()));
1410         const bool old_left_not_right = (pntSelCorr.y() < iPntSelCorr.y() || (pntSelCorr.y() == iPntSelCorr.y() && pntSelCorr.x() < iPntSelCorr.x()));
1411         swapping = left_not_right != old_left_not_right;
1412 
1413         // Find left (left_not_right ? from here : from start)
1414         const QPoint left = left_not_right ? here : iPntSelCorr;
1415 
1416         // Find right (left_not_right ? from start : from here)
1417         QPoint right = left_not_right ? iPntSelCorr : here;
1418 
1419         // Pick which is start (ohere) and which is extension (here)
1420         if (left_not_right) {
1421             here = left;
1422             ohere = right;
1423             offset = 0;
1424         } else {
1425             here = right;
1426             ohere = left;
1427             offset = -1;
1428         }
1429     }
1430 
1431     if ((here == pntSelCorr) && (scroll == _scrollBar->value())) {
1432         return; // not moved
1433     }
1434 
1435     if (here == ohere) {
1436         return; // It's not left, it's not right.
1437     }
1438 
1439     if (_actSel < 2 || swapping) {
1440         if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) {
1441             _screenWindow->setSelectionStart(ohere.x(), ohere.y(), true);
1442         } else {
1443             _screenWindow->setSelectionStart(ohere.x() - 1 - offset, ohere.y(), false);
1444         }
1445     }
1446 
1447     _actSel = 2; // within selection
1448     _pntSel = here;
1449     _pntSel.ry() += _scrollBar->value();
1450 
1451     if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) {
1452         _screenWindow->setSelectionEnd(here.x(), here.y());
1453     } else {
1454         _screenWindow->setSelectionEnd(here.x() + offset, here.y());
1455     }
1456 }
1457 
mouseReleaseEvent(QMouseEvent * ev)1458 void TerminalDisplay::mouseReleaseEvent(QMouseEvent *ev)
1459 {
1460     if (_screenWindow.isNull()) {
1461         return;
1462     }
1463 
1464     auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !_usesMouseTracking);
1465 
1466     if (ev->button() == Qt::LeftButton) {
1467         if (_dragInfo.state == diPending) {
1468             // We had a drag event pending but never confirmed.  Kill selection
1469             _screenWindow->clearSelection();
1470         } else {
1471             if (_actSel > 1) {
1472                 copyToX11Selection();
1473             }
1474 
1475             _actSel = 0;
1476 
1477             // FIXME: emits a release event even if the mouse is
1478             //       outside the range. The procedure used in `mouseMoveEvent'
1479             //       applies here, too.
1480 
1481             if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier) && !_readOnly) {
1482                 Q_EMIT mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 2);
1483             }
1484         }
1485         _dragInfo.state = diNone;
1486     }
1487 
1488     if (_usesMouseTracking && !_readOnly && (ev->button() == Qt::RightButton || ev->button() == Qt::MiddleButton) && !(ev->modifiers() & Qt::ShiftModifier)) {
1489         Q_EMIT mouseSignal(ev->button() == Qt::MiddleButton ? 1 : 2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 2);
1490     }
1491 
1492     if (!_screenWindow->screen()->hasSelection()) {
1493         _filterChain->mouseReleaseEvent(this, ev, charLine, charColumn);
1494     }
1495 }
1496 
getCharacterPosition(const QPoint & widgetPoint,bool edge) const1497 QPair<int, int> TerminalDisplay::getCharacterPosition(const QPoint &widgetPoint, bool edge) const
1498 {
1499     // the column value returned can be equal to _usedColumns (when edge == true),
1500     // which is the position just after the last character displayed in a line.
1501     //
1502     // this is required so that the user can select characters in the right-most
1503     // column (or left-most for right-to-left input)
1504     const int columnMax = edge ? _usedColumns : _usedColumns - 1;
1505     const int xOffset = edge ? _terminalFont->fontWidth() / 2 : 0;
1506     int line = qBound(0, (widgetPoint.y() - contentsRect().top() - _contentRect.top()) / _terminalFont->fontHeight(), _usedLines - 1);
1507     bool doubleWidth = line < _lineProperties.count() && _lineProperties[line] & LINE_DOUBLEWIDTH;
1508     int column =
1509         qBound(0, (widgetPoint.x() + xOffset - contentsRect().left() - _contentRect.left()) / _terminalFont->fontWidth() / (doubleWidth ? 2 : 1), columnMax);
1510 
1511     return qMakePair(line, column);
1512 }
1513 
setExpandedMode(bool expand)1514 void TerminalDisplay::setExpandedMode(bool expand)
1515 {
1516     _headerBar->setExpandedMode(expand);
1517 }
1518 
processMidButtonClick(QMouseEvent * ev)1519 void TerminalDisplay::processMidButtonClick(QMouseEvent *ev)
1520 {
1521     if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) {
1522         const bool appendEnter = (ev->modifiers() & Qt::ControlModifier) != 0u;
1523 
1524         if (_middleClickPasteMode == Enum::PasteFromX11Selection) {
1525             pasteFromX11Selection(appendEnter);
1526         } else if (_middleClickPasteMode == Enum::PasteFromClipboard) {
1527             pasteFromClipboard(appendEnter);
1528         } else {
1529             Q_ASSERT(false);
1530         }
1531     } else {
1532         if (!_readOnly) {
1533             auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !_usesMouseTracking);
1534             Q_EMIT mouseSignal(1, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0);
1535         }
1536     }
1537 }
1538 
mouseDoubleClickEvent(QMouseEvent * ev)1539 void TerminalDisplay::mouseDoubleClickEvent(QMouseEvent *ev)
1540 {
1541     // Yes, successive middle click can trigger this event
1542     if (ev->button() == Qt::MiddleButton) {
1543         processMidButtonClick(ev);
1544         return;
1545     }
1546 
1547     if (_screenWindow.isNull()) {
1548         return;
1549     }
1550 
1551     auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !_usesMouseTracking);
1552 
1553     QPoint pos(qMin(charColumn, _columns - 1), qMin(charLine, _lines - 1));
1554 
1555     // pass on double click as two clicks.
1556     if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) {
1557         if (!_readOnly) {
1558             // Send just _ONE_ click event, since the first click of the double click
1559             // was already sent by the click handler
1560             Q_EMIT mouseSignal(ev->button() == Qt::LeftButton ? 0 : 2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0);
1561         }
1562         return;
1563     }
1564 
1565     if (ev->button() != Qt::LeftButton) {
1566         return;
1567     }
1568 
1569     _screenWindow->clearSelection();
1570     _iPntSel = pos;
1571     _iPntSel.ry() += _scrollBar->value();
1572 
1573     _wordSelectionMode = true;
1574     _actSel = 2; // within selection
1575 
1576     // find word boundaries...
1577     {
1578         // find the start of the word
1579         const QPoint bgnSel = findWordStart(pos);
1580         const QPoint endSel = findWordEnd(pos);
1581 
1582         _actSel = 2; // within selection
1583 
1584         _screenWindow->setSelectionStart(bgnSel.x(), bgnSel.y(), false);
1585         _screenWindow->setSelectionEnd(endSel.x(), endSel.y());
1586 
1587         copyToX11Selection();
1588     }
1589 
1590     _possibleTripleClick = true;
1591 
1592     QTimer::singleShot(QApplication::doubleClickInterval(), this, [this]() {
1593         _possibleTripleClick = false;
1594     });
1595 }
1596 
wheelEvent(QWheelEvent * ev)1597 void TerminalDisplay::wheelEvent(QWheelEvent *ev)
1598 {
1599     static QElapsedTimer enable_zoom_timer;
1600     static bool enable_zoom = true;
1601     // Only vertical scrolling is supported
1602     if (qAbs(ev->angleDelta().y()) < qAbs(ev->angleDelta().x())) {
1603         return;
1604     }
1605 
1606     if (enable_zoom_timer.isValid() && enable_zoom_timer.elapsed() > 1000) {
1607         enable_zoom = true;
1608     }
1609 
1610     const int modifiers = ev->modifiers();
1611 
1612     // ctrl+<wheel> for zooming, like in konqueror and firefox
1613     if (((modifiers & Qt::ControlModifier) != 0u) && _mouseWheelZoom && enable_zoom) {
1614         _scrollWheelState.addWheelEvent(ev);
1615 
1616         int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE);
1617         for (; steps > 0; --steps) {
1618             // wheel-up for increasing font size
1619             _terminalFont->increaseFontSize();
1620         }
1621         for (; steps < 0; ++steps) {
1622             // wheel-down for decreasing font size
1623             _terminalFont->decreaseFontSize();
1624         }
1625         return;
1626     } else if (!_usesMouseTracking && (_scrollBar->maximum() > 0)) {
1627         // If the program running in the terminal is not interested in Mouse
1628         // Tracking events, send the event to the scrollbar if the slider
1629         // has room to move
1630 
1631         _scrollWheelState.addWheelEvent(ev);
1632 
1633         _scrollBar->event(ev);
1634 
1635         // Reapply scrollbar position since the scrollbar event handler
1636         // sometimes makes the scrollbar visible when set to hidden.
1637         // Don't call propagateSize and update, since nothing changed.
1638         _scrollBar->applyScrollBarPosition(false);
1639 
1640         Q_ASSERT(_sessionController != nullptr);
1641 
1642         _sessionController->setSearchStartToWindowCurrentLine();
1643         _scrollWheelState.clearAll();
1644     } else if (!_readOnly) {
1645         _scrollWheelState.addWheelEvent(ev);
1646 
1647         Q_ASSERT(!_sessionController->session().isNull());
1648 
1649         if (!_usesMouseTracking && !_sessionController->session()->isPrimaryScreen() && _scrollBar->alternateScrolling()) {
1650             // Send simulated up / down key presses to the terminal program
1651             // for the benefit of programs such as 'less' (which use the alternate screen)
1652 
1653             // assume that each Up / Down key event will cause the terminal application
1654             // to scroll by one line.
1655             //
1656             // to get a reasonable scrolling speed, scroll by one line for every 5 degrees
1657             // of mouse wheel rotation.  Mouse wheels typically move in steps of 15 degrees,
1658             // giving a scroll of 3 lines
1659 
1660             const int lines =
1661                 _scrollWheelState.consumeSteps(static_cast<int>(_terminalFont->fontHeight() * qApp->devicePixelRatio()), ScrollState::degreesToAngle(5));
1662             const int keyCode = lines > 0 ? Qt::Key_Up : Qt::Key_Down;
1663             QKeyEvent keyEvent(QEvent::KeyPress, keyCode, Qt::NoModifier);
1664 
1665             for (int i = 0; i < abs(lines); i++) {
1666                 Q_EMIT keyPressedSignal(&keyEvent);
1667             }
1668         } else if (_usesMouseTracking) {
1669             // terminal program wants notification of mouse activity
1670 
1671             auto [charLine, charColumn] = getCharacterPosition(ev->position().toPoint(), !_usesMouseTracking);
1672             const int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE);
1673             const int button = (steps > 0) ? 4 : 5;
1674             for (int i = 0; i < abs(steps); ++i) {
1675                 Q_EMIT mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0);
1676             }
1677         }
1678     }
1679     enable_zoom_timer.start();
1680     enable_zoom = false;
1681 }
1682 
viewScrolledByUser()1683 void TerminalDisplay::viewScrolledByUser()
1684 {
1685     Q_ASSERT(_sessionController != nullptr);
1686     _sessionController->setSearchStartToWindowCurrentLine();
1687 }
1688 
1689 /* Moving left/up from the line containing pnt, return the starting
1690    offset point which the given line is continuously wrapped
1691    (top left corner = 0,0; previous line not visible = 0,-1).
1692 */
findLineStart(const QPoint & pnt)1693 QPoint TerminalDisplay::findLineStart(const QPoint &pnt)
1694 {
1695     const int visibleScreenLines = _lineProperties.size();
1696     const int topVisibleLine = _screenWindow->currentLine();
1697     Screen *screen = _screenWindow->screen();
1698     int line = pnt.y();
1699     int lineInHistory = line + topVisibleLine;
1700 
1701     QVector<LineProperty> lineProperties = _lineProperties;
1702 
1703     while (lineInHistory > 0) {
1704         for (; line > 0; line--, lineInHistory--) {
1705             // Does previous line wrap around?
1706             if ((lineProperties[line - 1] & LINE_WRAPPED) == 0) {
1707                 return {0, lineInHistory - topVisibleLine};
1708             }
1709         }
1710 
1711         if (lineInHistory < 1) {
1712             break;
1713         }
1714 
1715         // _lineProperties is only for the visible screen, so grab new data
1716         int newRegionStart = qMax(0, lineInHistory - visibleScreenLines);
1717         lineProperties = screen->getLineProperties(newRegionStart, lineInHistory - 1);
1718         line = lineInHistory - newRegionStart;
1719     }
1720     return {0, lineInHistory - topVisibleLine};
1721 }
1722 
1723 /* Moving right/down from the line containing pnt, return the ending
1724    offset point which the given line is continuously wrapped.
1725 */
findLineEnd(const QPoint & pnt)1726 QPoint TerminalDisplay::findLineEnd(const QPoint &pnt)
1727 {
1728     const int visibleScreenLines = _lineProperties.size();
1729     const int topVisibleLine = _screenWindow->currentLine();
1730     const int maxY = _screenWindow->lineCount() - 1;
1731     Screen *screen = _screenWindow->screen();
1732     int line = pnt.y();
1733     int lineInHistory = line + topVisibleLine;
1734 
1735     QVector<LineProperty> lineProperties = _lineProperties;
1736 
1737     while (lineInHistory < maxY) {
1738         for (; line < lineProperties.count() && lineInHistory < maxY; line++, lineInHistory++) {
1739             // Does current line wrap around?
1740             if ((lineProperties[line] & LINE_WRAPPED) == 0) {
1741                 return {_columns - 1, lineInHistory - topVisibleLine};
1742             }
1743         }
1744 
1745         line = 0;
1746         lineProperties = screen->getLineProperties(lineInHistory, qMin(lineInHistory + visibleScreenLines, maxY));
1747     }
1748     return {_columns - 1, lineInHistory - topVisibleLine};
1749 }
1750 
findWordStart(const QPoint & pnt)1751 QPoint TerminalDisplay::findWordStart(const QPoint &pnt)
1752 {
1753     // Don't ask me why x and y are switched ¯\_(ツ)_/¯
1754     QSharedPointer<HotSpot> hotspot = _filterChain->hotSpotAt(pnt.y(), pnt.x());
1755     if (hotspot) {
1756         return QPoint(hotspot->startColumn(), hotspot->startLine());
1757     }
1758 
1759     const int regSize = qMax(_screenWindow->windowLines(), 10);
1760     const int firstVisibleLine = _screenWindow->currentLine();
1761 
1762     Screen *screen = _screenWindow->screen();
1763     Character *image = _image;
1764     Character *tmp_image = nullptr;
1765 
1766     int imgLine = pnt.y();
1767     int x = pnt.x();
1768     int y = imgLine + firstVisibleLine;
1769     int imgLoc = loc(x, imgLine);
1770     QVector<LineProperty> lineProperties = _lineProperties;
1771     const QChar selClass = charClass(image[imgLoc]);
1772     const int imageSize = regSize * _columns;
1773 
1774     while (true) {
1775         for (;; imgLoc--, x--) {
1776             if (imgLoc < 1) {
1777                 // no more chars in this region
1778                 break;
1779             }
1780             if (x > 0) {
1781                 // has previous char on this line
1782                 if (charClass(image[imgLoc - 1]) == selClass) {
1783                     continue;
1784                 }
1785                 goto out;
1786             } else if (imgLine > 0) {
1787                 // not the first line in the session
1788                 if ((lineProperties[imgLine - 1] & LINE_WRAPPED) != 0) {
1789                     // have continuation on prev line
1790                     if (charClass(image[imgLoc - 1]) == selClass) {
1791                         x = _columns;
1792                         imgLine--;
1793                         y--;
1794                         continue;
1795                     }
1796                 }
1797                 goto out;
1798             } else if (y > 0) {
1799                 // want more data, but need to fetch new region
1800                 break;
1801             } else {
1802                 goto out;
1803             }
1804         }
1805         if (y <= 0) {
1806             // No more data
1807             goto out;
1808         }
1809         int newRegStart = qMax(0, y - regSize + 1);
1810         lineProperties = screen->getLineProperties(newRegStart, y - 1);
1811         imgLine = y - newRegStart;
1812 
1813         delete[] tmp_image;
1814         tmp_image = new Character[imageSize];
1815         image = tmp_image;
1816 
1817         screen->getImage(tmp_image, imageSize, newRegStart, y - 1);
1818         imgLoc = loc(x, imgLine);
1819         if (imgLoc < 1) {
1820             // Reached the start of the session
1821             break;
1822         }
1823     }
1824 out:
1825     delete[] tmp_image;
1826     return {x, y - firstVisibleLine};
1827 }
1828 
findWordEnd(const QPoint & pnt)1829 QPoint TerminalDisplay::findWordEnd(const QPoint &pnt)
1830 {
1831     QSharedPointer<HotSpot> hotspot = _filterChain->hotSpotAt(pnt.y(), pnt.x());
1832     if (hotspot) {
1833         return QPoint(hotspot->endColumn() - 1, hotspot->endLine());
1834     }
1835 
1836     const int regSize = qMax(_screenWindow->windowLines(), 10);
1837     const int curLine = _screenWindow->currentLine();
1838     int i = pnt.y();
1839     int x = pnt.x();
1840     int y = i + curLine;
1841     int j = loc(x, i);
1842     QVector<LineProperty> lineProperties = _lineProperties;
1843     Screen *screen = _screenWindow->screen();
1844     Character *image = _image;
1845     Character *tmp_image = nullptr;
1846     const QChar selClass = charClass(image[j]);
1847     const int imageSize = regSize * _columns;
1848     const int maxY = _screenWindow->lineCount() - 1;
1849     const int maxX = _columns - 1;
1850 
1851     while (true) {
1852         const int lineCount = lineProperties.count();
1853         for (;; j++, x++) {
1854             if (x < maxX) {
1855                 if (charClass(image[j + 1]) == selClass &&
1856                     // A colon right before whitespace is never part of a word
1857                     !(image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) {
1858                     continue;
1859                 }
1860                 goto out;
1861             } else if (i < lineCount - 1) {
1862                 if (((lineProperties[i] & LINE_WRAPPED) != 0) && charClass(image[j + 1]) == selClass &&
1863                     // A colon right before whitespace is never part of a word
1864                     !(image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) {
1865                     x = -1;
1866                     i++;
1867                     y++;
1868                     continue;
1869                 }
1870                 goto out;
1871             } else if (y < maxY) {
1872                 if (i < lineCount && ((lineProperties[i] & LINE_WRAPPED) == 0)) {
1873                     goto out;
1874                 }
1875                 break;
1876             } else {
1877                 goto out;
1878             }
1879         }
1880         int newRegEnd = qMin(y + regSize - 1, maxY);
1881         lineProperties = screen->getLineProperties(y, newRegEnd);
1882         i = 0;
1883         if (tmp_image == nullptr) {
1884             tmp_image = new Character[imageSize];
1885             image = tmp_image;
1886         }
1887         screen->getImage(tmp_image, imageSize, y, newRegEnd);
1888         x--;
1889         j = loc(x, i);
1890     }
1891 out:
1892     y -= curLine;
1893     // In word selection mode don't select @ (64) if at end of word.
1894     if (((image[j].rendition & RE_EXTENDED_CHAR) == 0) && (QChar(image[j].character) == QLatin1Char('@')) && (y > pnt.y() || x > pnt.x())) {
1895         if (x > 0) {
1896             x--;
1897         } else {
1898             y--;
1899         }
1900     }
1901     delete[] tmp_image;
1902 
1903     return {x, y};
1904 }
1905 
currentDecodingOptions()1906 Screen::DecodingOptions TerminalDisplay::currentDecodingOptions()
1907 {
1908     Screen::DecodingOptions decodingOptions;
1909     if (_preserveLineBreaks) {
1910         decodingOptions |= Screen::PreserveLineBreaks;
1911     }
1912     if (_trimLeadingSpaces) {
1913         decodingOptions |= Screen::TrimLeadingWhitespace;
1914     }
1915     if (_trimTrailingSpaces) {
1916         decodingOptions |= Screen::TrimTrailingWhitespace;
1917     }
1918 
1919     return decodingOptions;
1920 }
1921 
mouseTripleClickEvent(QMouseEvent * ev)1922 void TerminalDisplay::mouseTripleClickEvent(QMouseEvent *ev)
1923 {
1924     if (_screenWindow.isNull()) {
1925         return;
1926     }
1927 
1928     auto [charLine, charColumn] = getCharacterPosition(ev->pos(), true);
1929     selectLine(QPoint(charColumn, charLine), _tripleClickMode == Enum::SelectWholeLine);
1930 }
1931 
selectLine(QPoint pos,bool entireLine)1932 void TerminalDisplay::selectLine(QPoint pos, bool entireLine)
1933 {
1934     _iPntSel = pos;
1935 
1936     _screenWindow->clearSelection();
1937 
1938     _lineSelectionMode = true;
1939     _wordSelectionMode = false;
1940 
1941     _actSel = 2; // within selection
1942 
1943     if (!entireLine) { // Select from cursor to end of line
1944         _tripleSelBegin = findWordStart(_iPntSel);
1945         _screenWindow->setSelectionStart(_tripleSelBegin.x(), _tripleSelBegin.y(), false);
1946     } else {
1947         _tripleSelBegin = findLineStart(_iPntSel);
1948         _screenWindow->setSelectionStart(0, _tripleSelBegin.y(), false);
1949     }
1950 
1951     _iPntSel = findLineEnd(_iPntSel);
1952     _screenWindow->setSelectionEnd(_iPntSel.x(), _iPntSel.y());
1953 
1954     copyToX11Selection();
1955 
1956     _iPntSel.ry() += _scrollBar->value();
1957 }
1958 
selectCurrentLine()1959 void TerminalDisplay::selectCurrentLine()
1960 {
1961     if (_screenWindow.isNull()) {
1962         return;
1963     }
1964 
1965     selectLine(cursorPosition(), true);
1966 }
1967 
selectAll()1968 void TerminalDisplay::selectAll()
1969 {
1970     if (_screenWindow.isNull()) {
1971         return;
1972     }
1973 
1974     _preserveLineBreaks = true;
1975     _screenWindow->setSelectionByLineRange(0, _screenWindow->lineCount());
1976     copyToX11Selection();
1977 }
1978 
focusNextPrevChild(bool next)1979 bool TerminalDisplay::focusNextPrevChild(bool next)
1980 {
1981     // for 'Tab', always disable focus switching among widgets
1982     // for 'Shift+Tab', leave the decision to higher level
1983     if (next) {
1984         return false;
1985     } else {
1986         return QWidget::focusNextPrevChild(next);
1987     }
1988 }
1989 
charClass(const Character & ch) const1990 QChar TerminalDisplay::charClass(const Character &ch) const
1991 {
1992     if ((ch.rendition & RE_EXTENDED_CHAR) != 0) {
1993         ushort extendedCharLength = 0;
1994         const uint *chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength);
1995         if ((chars != nullptr) && extendedCharLength > 0) {
1996             const QString s = QString::fromUcs4(chars, extendedCharLength);
1997             if (_wordCharacters.contains(s, Qt::CaseInsensitive)) {
1998                 return QLatin1Char('a');
1999             }
2000             bool letterOrNumber = false;
2001             for (int i = 0; !letterOrNumber && i < s.size(); ++i) {
2002                 letterOrNumber = s.at(i).isLetterOrNumber();
2003             }
2004             return letterOrNumber ? QLatin1Char('a') : s.at(0);
2005         }
2006         return 0;
2007     } else {
2008         const QChar qch(ch.character);
2009         if (qch.isSpace()) {
2010             return QLatin1Char(' ');
2011         }
2012 
2013         if (qch.isLetterOrNumber() || _wordCharacters.contains(qch, Qt::CaseInsensitive)) {
2014             return QLatin1Char('a');
2015         }
2016 
2017         return qch;
2018     }
2019 }
2020 
setWordCharacters(const QString & wc)2021 void TerminalDisplay::setWordCharacters(const QString &wc)
2022 {
2023     _wordCharacters = wc;
2024 }
2025 
setUsesMouseTracking(bool on)2026 void TerminalDisplay::setUsesMouseTracking(bool on)
2027 {
2028     _usesMouseTracking = on;
2029     resetCursor();
2030 }
2031 
resetCursor()2032 void TerminalDisplay::resetCursor()
2033 {
2034     setCursor(_usesMouseTracking ? Qt::ArrowCursor : Qt::IBeamCursor);
2035 }
2036 
usesMouseTracking() const2037 bool TerminalDisplay::usesMouseTracking() const
2038 {
2039     return _usesMouseTracking;
2040 }
2041 
setBracketedPasteMode(bool on)2042 void TerminalDisplay::setBracketedPasteMode(bool on)
2043 {
2044     _bracketedPasteMode = on;
2045 }
bracketedPasteMode() const2046 bool TerminalDisplay::bracketedPasteMode() const
2047 {
2048     return _bracketedPasteMode;
2049 }
2050 
2051 /* ------------------------------------------------------------------------- */
2052 /*                                                                           */
2053 /*                               Clipboard                                   */
2054 /*                                                                           */
2055 /* ------------------------------------------------------------------------- */
2056 
doPaste(QString text,bool appendReturn)2057 void TerminalDisplay::doPaste(QString text, bool appendReturn)
2058 {
2059     if (_screenWindow.isNull()) {
2060         return;
2061     }
2062 
2063     if (_readOnly) {
2064         return;
2065     }
2066 
2067     if (appendReturn) {
2068         text.append(QLatin1String("\r"));
2069     }
2070 
2071     if (text.length() > 8000) {
2072         if (KMessageBox::warningContinueCancel(
2073                 window(),
2074                 i18np("Are you sure you want to paste %1 character?", "Are you sure you want to paste %1 characters?", text.length()),
2075                 i18n("Confirm Paste"),
2076                 KStandardGuiItem::cont(),
2077                 KStandardGuiItem::cancel(),
2078                 QStringLiteral("ShowPasteHugeTextWarning"))
2079             == KMessageBox::Cancel) {
2080             return;
2081         }
2082     }
2083 
2084     // Most code in Konsole uses UTF-32. We're filtering
2085     // UTF-16 here, as all control characters can be represented
2086     // in this encoding as single code unit. If you ever need to
2087     // filter anything above 0xFFFF (specific code points or
2088     // categories which contain such code points), convert text to
2089     // UTF-32 using QString::toUcs4() and use QChar static
2090     // methods which take "uint ucs4".
2091     static const QVector<ushort> whitelist = {u'\t', u'\r', u'\n'};
2092     static const auto isUnsafe = [](const QChar &c) {
2093         return (c.category() == QChar::Category::Other_Control && !whitelist.contains(c.unicode()));
2094     };
2095     // Returns control sequence string (e.g. "^C") for control character c
2096     static const auto charToSequence = [](const QChar &c) {
2097         if (c.unicode() <= 0x1F) {
2098             return QStringLiteral("^%1").arg(QChar(u'@' + c.unicode()));
2099         } else if (c.unicode() == 0x7F) {
2100             return QStringLiteral("^?");
2101         } else if (c.unicode() >= 0x80 && c.unicode() <= 0x9F) {
2102             return QStringLiteral("^[%1").arg(QChar(u'@' + c.unicode() - 0x80));
2103         }
2104         return QString();
2105     };
2106 
2107     const QMap<ushort, QString> characterDescriptions = {
2108         {0x0003, i18n("End Of Text/Interrupt: may exit the current process")},
2109         {0x0004, i18n("End Of Transmission: may exit the current process")},
2110         {0x0007, i18n("Bell: will try to emit an audible warning")},
2111         {0x0008, i18n("Backspace")},
2112         {0x0013, i18n("Device Control Three/XOFF: suspends output")},
2113         {0x001a, i18n("Substitute/Suspend: may suspend current process")},
2114         {0x001b, i18n("Escape: used for manipulating terminal state")},
2115         {0x001c, i18n("File Separator/Quit: may abort the current process")},
2116     };
2117 
2118     QStringList unsafeCharacters;
2119     for (const QChar &c : text) {
2120         if (isUnsafe(c)) {
2121             const QString sequence = charToSequence(c);
2122             const QString description = characterDescriptions.value(c.unicode(), QString());
2123             QString entry = QStringLiteral("U+%1").arg(c.unicode(), 4, 16, QLatin1Char('0'));
2124             if (!sequence.isEmpty()) {
2125                 entry += QStringLiteral("\t%1").arg(sequence);
2126             }
2127             if (!description.isEmpty()) {
2128                 entry += QStringLiteral("\t%1").arg(description);
2129             }
2130             unsafeCharacters.append(entry);
2131         }
2132     }
2133     unsafeCharacters.removeDuplicates();
2134 
2135     if (!unsafeCharacters.isEmpty()) {
2136         int result =
2137             KMessageBox::warningYesNoCancelList(window(),
2138                                                 i18n("The text you're trying to paste contains hidden control characters, "
2139                                                      "do you want to filter them out?"),
2140                                                 unsafeCharacters,
2141                                                 i18nc("@title", "Confirm Paste"),
2142                                                 KGuiItem(i18nc("@action:button", "Paste &without control characters"), QStringLiteral("filter-symbolic")),
2143                                                 KGuiItem(i18nc("@action:button", "&Paste everything"), QStringLiteral("edit-paste")),
2144                                                 KGuiItem(i18nc("@action:button", "&Cancel"), QStringLiteral("dialog-cancel")),
2145                                                 QStringLiteral("ShowPasteUnprintableWarning"));
2146         switch (result) {
2147         case KMessageBox::Cancel:
2148             return;
2149         case KMessageBox::Yes: {
2150             QString sanitized;
2151             for (const QChar &c : text) {
2152                 if (!isUnsafe(c)) {
2153                     sanitized.append(c);
2154                 }
2155             }
2156             text = sanitized;
2157         }
2158         case KMessageBox::No:
2159             break;
2160         default:
2161             break;
2162         }
2163     }
2164 
2165     if (!text.isEmpty()) {
2166         // replace CRLF with CR first, fixes issues with pasting multiline
2167         // text from gtk apps (e.g. Firefox), bug 421480
2168         text.replace(QLatin1String("\r\n"), QLatin1String("\r"));
2169 
2170         text.replace(QLatin1Char('\n'), QLatin1Char('\r'));
2171         if (bracketedPasteMode()) {
2172             text.remove(QLatin1String("\033"));
2173             text.prepend(QLatin1String("\033[200~"));
2174             text.append(QLatin1String("\033[201~"));
2175         }
2176         // perform paste by simulating keypress events
2177         QKeyEvent e(QEvent::KeyPress, 0, Qt::NoModifier, text);
2178         Q_EMIT keyPressedSignal(&e);
2179     }
2180 }
2181 
setAutoCopySelectedText(bool enabled)2182 void TerminalDisplay::setAutoCopySelectedText(bool enabled)
2183 {
2184     _autoCopySelectedText = enabled;
2185 }
2186 
setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode)2187 void TerminalDisplay::setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode)
2188 {
2189     _middleClickPasteMode = mode;
2190 }
2191 
setCopyTextAsHTML(bool enabled)2192 void TerminalDisplay::setCopyTextAsHTML(bool enabled)
2193 {
2194     _copyTextAsHTML = enabled;
2195 }
2196 
copyToX11Selection()2197 void TerminalDisplay::copyToX11Selection()
2198 {
2199     if (_screenWindow.isNull()) {
2200         return;
2201     }
2202 
2203     const QString &text = _screenWindow->selectedText(currentDecodingOptions());
2204     if (text.isEmpty()) {
2205         return;
2206     }
2207 
2208     auto mimeData = new QMimeData;
2209     mimeData->setText(text);
2210 
2211     if (_copyTextAsHTML) {
2212         mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml));
2213     }
2214 
2215     if (QApplication::clipboard()->supportsSelection()) {
2216         QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection);
2217     }
2218 
2219     if (_autoCopySelectedText) {
2220         QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
2221     }
2222 }
2223 
copyToClipboard()2224 void TerminalDisplay::copyToClipboard()
2225 {
2226     if (_screenWindow.isNull()) {
2227         return;
2228     }
2229 
2230     const QString &text = _screenWindow->selectedText(currentDecodingOptions());
2231     if (text.isEmpty()) {
2232         return;
2233     }
2234 
2235     auto mimeData = new QMimeData;
2236     mimeData->setText(text);
2237 
2238     if (_copyTextAsHTML) {
2239         mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml));
2240     }
2241 
2242     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
2243 }
2244 
pasteFromClipboard(bool appendEnter)2245 void TerminalDisplay::pasteFromClipboard(bool appendEnter)
2246 {
2247     QString text;
2248     const QMimeData *mimeData = QApplication::clipboard()->mimeData(QClipboard::Clipboard);
2249 
2250     // When pasting urls of local files:
2251     // - remove the scheme part, "file://"
2252     // - paste the path(s) as a space-separated list of strings, which are quoted if needed
2253     if (!mimeData->hasUrls()) { // fast path if there are no urls
2254         text = mimeData->text();
2255     } else { // handle local file urls
2256         const QList<QUrl> list = mimeData->urls();
2257         for (const QUrl &url : list) {
2258             if (url.isLocalFile()) {
2259                 text += KShell::quoteArg(url.toLocalFile());
2260                 text += QLatin1Char(' ');
2261             } else { // can users copy urls of both local and remote files at the same time?
2262                 text = mimeData->text();
2263                 break;
2264             }
2265         }
2266     }
2267 
2268     doPaste(text, appendEnter);
2269 }
2270 
pasteFromX11Selection(bool appendEnter)2271 void TerminalDisplay::pasteFromX11Selection(bool appendEnter)
2272 {
2273     if (QApplication::clipboard()->supportsSelection()) {
2274         QString text = QApplication::clipboard()->text(QClipboard::Selection);
2275         doPaste(text, appendEnter);
2276     }
2277 }
2278 
2279 /* ------------------------------------------------------------------------- */
2280 /*                                                                           */
2281 /*                                Input Method                               */
2282 /*                                                                           */
2283 /* ------------------------------------------------------------------------- */
2284 
inputMethodEvent(QInputMethodEvent * event)2285 void TerminalDisplay::inputMethodEvent(QInputMethodEvent *event)
2286 {
2287     if (!event->commitString().isEmpty()) {
2288         QKeyEvent keyEvent(QEvent::KeyPress, 0, Qt::NoModifier, event->commitString());
2289         Q_EMIT keyPressedSignal(&keyEvent);
2290     }
2291 
2292     if (!_readOnly && isCursorOnDisplay()) {
2293         _inputMethodData.preeditString = event->preeditString();
2294         update(preeditRect() | _inputMethodData.previousPreeditRect);
2295     }
2296     event->accept();
2297 }
2298 
inputMethodQuery(Qt::InputMethodQuery query) const2299 QVariant TerminalDisplay::inputMethodQuery(Qt::InputMethodQuery query) const
2300 {
2301     const QPoint cursorPos = cursorPosition();
2302     switch (query) {
2303     case Qt::ImCursorRectangle:
2304         return imageToWidget(QRect(cursorPos.x(), cursorPos.y(), 1, 1));
2305     case Qt::ImFont:
2306         return font();
2307     case Qt::ImCursorPosition:
2308         // return the cursor position within the current line
2309         return cursorPos.x();
2310     case Qt::ImSurroundingText: {
2311         // return the text from the current line
2312         QString lineText;
2313         QTextStream stream(&lineText);
2314         PlainTextDecoder decoder;
2315         decoder.begin(&stream);
2316         if (isCursorOnDisplay()) {
2317             decoder.decodeLine(&_image[loc(0, cursorPos.y())], _usedColumns, LINE_DEFAULT);
2318         }
2319         decoder.end();
2320         return lineText;
2321     }
2322     case Qt::ImCurrentSelection:
2323         return QString();
2324     default:
2325         break;
2326     }
2327 
2328     return QVariant();
2329 }
2330 
preeditRect() const2331 QRect TerminalDisplay::preeditRect() const
2332 {
2333     const int preeditLength = Character::stringWidth(_inputMethodData.preeditString);
2334 
2335     if (preeditLength == 0) {
2336         return {};
2337     }
2338     const QRect stringRect(_contentRect.left() + _terminalFont->fontWidth() * cursorPosition().x(),
2339                            _contentRect.top() + _terminalFont->fontHeight() * cursorPosition().y(),
2340                            _terminalFont->fontWidth() * preeditLength,
2341                            _terminalFont->fontHeight());
2342 
2343     return stringRect.intersected(_contentRect);
2344 }
2345 
2346 /* ------------------------------------------------------------------------- */
2347 /*                                                                           */
2348 /*                                Keyboard                                   */
2349 /*                                                                           */
2350 /* ------------------------------------------------------------------------- */
2351 
setFlowControlWarningEnabled(bool enable)2352 void TerminalDisplay::setFlowControlWarningEnabled(bool enable)
2353 {
2354     _flowControlWarningEnabled = enable;
2355 
2356     // if the dialog is currently visible and the flow control warning has
2357     // been disabled then hide the dialog
2358     if (!enable) {
2359         outputSuspended(false);
2360     }
2361 }
2362 
outputSuspended(bool suspended)2363 void TerminalDisplay::outputSuspended(bool suspended)
2364 {
2365     // create the label when this function is first called
2366     if (_outputSuspendedMessageWidget == nullptr) {
2367         // This label includes a link to an English language website
2368         // describing the 'flow control' (Xon/Xoff) feature found in almost
2369         // all terminal emulators.
2370         // If there isn't a suitable article available in the target language the link
2371         // can simply be removed.
2372         _outputSuspendedMessageWidget =
2373             createMessageWidget(i18n("<qt>Output has been "
2374                                      "<a href=\"https://en.wikipedia.org/wiki/Software_flow_control\">suspended</a>"
2375                                      " by pressing Ctrl+S."
2376                                      " Press <b>Ctrl+Q</b> to resume.</qt>"));
2377 
2378         connect(_outputSuspendedMessageWidget, &KMessageWidget::linkActivated, this, [](const QString &url) {
2379             QDesktopServices::openUrl(QUrl(url));
2380         });
2381 
2382         _outputSuspendedMessageWidget->setMessageType(KMessageWidget::Warning);
2383     }
2384 
2385     suspended ? _outputSuspendedMessageWidget->animatedShow() : _outputSuspendedMessageWidget->animatedHide();
2386 }
2387 
dismissOutputSuspendedMessage()2388 void TerminalDisplay::dismissOutputSuspendedMessage()
2389 {
2390     outputSuspended(false);
2391 }
2392 
createMessageWidget(const QString & text)2393 KMessageWidget *TerminalDisplay::createMessageWidget(const QString &text)
2394 {
2395     auto *widget = new KMessageWidget(text, this);
2396     widget->setWordWrap(true);
2397     widget->setFocusProxy(this);
2398     widget->setCursor(Qt::ArrowCursor);
2399 
2400     _verticalLayout->insertWidget(1, widget);
2401 
2402     _searchBar->raise();
2403 
2404     return widget;
2405 }
2406 
updateReadOnlyState(bool readonly)2407 void TerminalDisplay::updateReadOnlyState(bool readonly)
2408 {
2409     if (_readOnly == readonly) {
2410         return;
2411     }
2412 
2413     if (readonly) {
2414         // Lazy create the readonly messagewidget
2415         if (_readOnlyMessageWidget == nullptr) {
2416             _readOnlyMessageWidget = createMessageWidget(i18n("This terminal is read-only."));
2417             _readOnlyMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("object-locked")));
2418         }
2419     }
2420 
2421     if (_readOnlyMessageWidget != nullptr) {
2422         readonly ? _readOnlyMessageWidget->animatedShow() : _readOnlyMessageWidget->animatedHide();
2423     }
2424 
2425     _readOnly = readonly;
2426 }
2427 
keyPressEvent(QKeyEvent * event)2428 void TerminalDisplay::keyPressEvent(QKeyEvent *event)
2429 {
2430     {
2431         auto [charLine, charColumn] = getCharacterPosition(mapFromGlobal(QCursor::pos()), !_usesMouseTracking);
2432 
2433         // Don't process it if the filterchain handled it for us
2434         if (_filterChain->keyPressEvent(this, event, charLine, charColumn)) {
2435             return;
2436         }
2437     }
2438 
2439     if (!_peekPrimaryShortcut.isEmpty() && _peekPrimaryShortcut.matches(QKeySequence(event->key() | event->modifiers()))) {
2440         peekPrimaryRequested(true);
2441     }
2442 
2443     if (!_readOnly) {
2444         _actSel = 0; // Key stroke implies a screen update, so TerminalDisplay won't
2445                      // know where the current selection is.
2446 
2447         if (_allowBlinkingCursor) {
2448             _blinkCursorTimer->start();
2449             if (_cursorBlinking) {
2450                 // if cursor is blinking(hidden), blink it again to show it
2451                 blinkCursorEvent();
2452             }
2453             Q_ASSERT(!_cursorBlinking);
2454         }
2455     }
2456 
2457     Q_EMIT keyPressedSignal(event);
2458 
2459 #ifndef QT_NO_ACCESSIBILITY
2460     if (!_readOnly) {
2461         QAccessibleTextCursorEvent textCursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX());
2462         QAccessible::updateAccessibility(&textCursorEvent);
2463     }
2464 #endif
2465 
2466     event->accept();
2467 }
2468 
keyReleaseEvent(QKeyEvent * event)2469 void TerminalDisplay::keyReleaseEvent(QKeyEvent *event)
2470 {
2471     if (_readOnly) {
2472         event->accept();
2473         return;
2474     }
2475 
2476     {
2477         auto [charLine, charColumn] = getCharacterPosition(mapFromGlobal(QCursor::pos()), !_usesMouseTracking);
2478         _filterChain->keyReleaseEvent(this, event, charLine, charColumn);
2479     }
2480 
2481     peekPrimaryRequested(false);
2482 
2483     QWidget::keyReleaseEvent(event);
2484 }
2485 
handleShortcutOverrideEvent(QKeyEvent * keyEvent)2486 bool TerminalDisplay::handleShortcutOverrideEvent(QKeyEvent *keyEvent)
2487 {
2488     const int modifiers = keyEvent->modifiers();
2489 
2490     //  When a possible shortcut combination is pressed,
2491     //  emit the overrideShortcutCheck() signal to allow the host
2492     //  to decide whether the terminal should override it or not.
2493     if (modifiers != Qt::NoModifier) {
2494         int modifierCount = 0;
2495         unsigned int currentModifier = Qt::ShiftModifier;
2496 
2497         while (currentModifier <= Qt::KeypadModifier) {
2498             if ((modifiers & currentModifier) != 0u) {
2499                 modifierCount++;
2500             }
2501             currentModifier <<= 1;
2502         }
2503         if (modifierCount < 2) {
2504             bool override = false;
2505             Q_EMIT overrideShortcutCheck(keyEvent, override);
2506             if (override) {
2507                 keyEvent->accept();
2508                 return true;
2509             }
2510         }
2511     }
2512 
2513     // Override any of the following shortcuts because
2514     // they are needed by the terminal
2515     int keyCode = keyEvent->key() | modifiers;
2516     switch (keyCode) {
2517         // list is taken from the QLineEdit::event() code
2518     case Qt::Key_Tab:
2519     case Qt::Key_Delete:
2520     case Qt::Key_Home:
2521     case Qt::Key_End:
2522     case Qt::Key_Backspace:
2523     case Qt::Key_Left:
2524     case Qt::Key_Right:
2525     case Qt::Key_Slash:
2526     case Qt::Key_Period:
2527     case Qt::Key_Space:
2528         keyEvent->accept();
2529         return true;
2530     }
2531     return false;
2532 }
2533 
event(QEvent * event)2534 bool TerminalDisplay::event(QEvent *event)
2535 {
2536     bool eventHandled = false;
2537     switch (event->type()) {
2538     case QEvent::ShortcutOverride:
2539         eventHandled = handleShortcutOverrideEvent(static_cast<QKeyEvent *>(event));
2540         break;
2541     case QEvent::PaletteChange:
2542     case QEvent::ApplicationPaletteChange:
2543         if (_terminalColor) {
2544             _terminalColor->onColorsChanged();
2545         }
2546         break;
2547     case QEvent::FocusOut:
2548     case QEvent::FocusIn:
2549         if (_screenWindow != nullptr) {
2550             // force a redraw on focusIn, fixes the
2551             // black screen bug when the view is focused
2552             // but doesn't redraws.
2553             _screenWindow->notifyOutputChanged();
2554         }
2555         update();
2556         break;
2557     default:
2558         break;
2559     }
2560     return eventHandled ? true : QWidget::event(event);
2561 }
2562 
contextMenuEvent(QContextMenuEvent * event)2563 void TerminalDisplay::contextMenuEvent(QContextMenuEvent *event)
2564 {
2565     // the logic for the mouse case is within MousePressEvent()
2566     if (event->reason() != QContextMenuEvent::Mouse) {
2567         Q_EMIT configureRequest(mapFromGlobal(QCursor::pos()));
2568     }
2569 }
2570 
2571 /* --------------------------------------------------------------------- */
2572 /*                                                                       */
2573 /*                                  Bell                                 */
2574 /*                                                                       */
2575 /* --------------------------------------------------------------------- */
2576 
bell(const QString & message)2577 void TerminalDisplay::bell(const QString &message)
2578 {
2579     _bell.bell(message, hasFocus());
2580 }
2581 
2582 /* --------------------------------------------------------------------- */
2583 /*                                                                       */
2584 /* Drag & Drop                                                           */
2585 /*                                                                       */
2586 /* --------------------------------------------------------------------- */
2587 
dragEnterEvent(QDragEnterEvent * event)2588 void TerminalDisplay::dragEnterEvent(QDragEnterEvent *event)
2589 {
2590     // text/plain alone is enough for KDE-apps
2591     // text/uri-list is for supporting some non-KDE apps, such as thunar
2592     //   and pcmanfm
2593     // That also applies in dropEvent()
2594     const auto mimeData = event->mimeData();
2595     if ((!_readOnly) && (mimeData != nullptr) && (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list")))) {
2596         event->acceptProposedAction();
2597     }
2598 }
2599 
2600 namespace
2601 {
extractDroppedText(const QList<QUrl> & urls)2602 QString extractDroppedText(const QList<QUrl> &urls)
2603 {
2604     QString dropText;
2605     for (int i = 0; i < urls.count(); i++) {
2606         KIO::StatJob *job = KIO::mostLocalUrl(urls[i], KIO::HideProgressInfo);
2607         if (!job->exec()) {
2608             continue;
2609         }
2610 
2611         const QUrl url = job->mostLocalUrl();
2612         // in future it may be useful to be able to insert file names with drag-and-drop
2613         // without quoting them (this only affects paths with spaces in)
2614         dropText += KShell::quoteArg(url.isLocalFile() ? url.path() : url.url());
2615 
2616         // Each filename(including the last) should be followed by one space.
2617         dropText += QLatin1Char(' ');
2618     }
2619     return dropText;
2620 }
2621 
setupCdToUrlAction(const QString & dropText,const QUrl & url,QList<QAction * > & additionalActions,TerminalDisplay * display)2622 void setupCdToUrlAction(const QString &dropText, const QUrl &url, QList<QAction *> &additionalActions, TerminalDisplay *display)
2623 {
2624     KIO::StatJob *job = KIO::mostLocalUrl(url, KIO::HideProgressInfo);
2625     if (!job->exec()) {
2626         return;
2627     }
2628 
2629     const QUrl localUrl = job->mostLocalUrl();
2630     if (!localUrl.isLocalFile()) {
2631         return;
2632     }
2633 
2634     const QFileInfo fileInfo(localUrl.path());
2635     if (!fileInfo.isDir()) {
2636         return;
2637     }
2638 
2639     QAction *cdAction = new QAction(i18n("Change &Directory To"), display);
2640     const QByteArray triggerText = QString(QLatin1String(" cd ") + dropText + QLatin1Char('\n')).toLocal8Bit();
2641     display->connect(cdAction, &QAction::triggered, display, [display, triggerText] {
2642         Q_EMIT display->sendStringToEmu(triggerText);
2643     });
2644     additionalActions.append(cdAction);
2645 }
2646 
2647 }
2648 
dropEvent(QDropEvent * event)2649 void TerminalDisplay::dropEvent(QDropEvent *event)
2650 {
2651     if (_readOnly) {
2652         event->accept();
2653         return;
2654     }
2655 
2656     const auto mimeData = event->mimeData();
2657     if (mimeData == nullptr) {
2658         return;
2659     }
2660     auto urls = mimeData->urls();
2661 
2662     QString dropText;
2663     if (!urls.isEmpty()) {
2664         dropText = extractDroppedText(urls);
2665 
2666         // If our target is local we will open a popup - otherwise the fallback kicks
2667         // in and the URLs will simply be pasted as text.
2668         if (!_dropUrlsAsText && (_sessionController != nullptr) && _sessionController->url().isLocalFile()) {
2669             // A standard popup with Copy, Move and Link as options -
2670             // plus an additional Paste option.
2671 
2672             QAction *pasteAction = new QAction(i18n("&Paste Location"), this);
2673             connect(pasteAction, &QAction::triggered, this, [this, dropText] {
2674                 Q_EMIT sendStringToEmu(dropText.toLocal8Bit());
2675             });
2676 
2677             QList<QAction *> additionalActions;
2678             additionalActions.append(pasteAction);
2679 
2680             if (urls.count() == 1) {
2681                 setupCdToUrlAction(dropText, urls.at(0), additionalActions, this);
2682             }
2683 
2684             QUrl target = QUrl::fromLocalFile(_sessionController->currentDir());
2685 
2686             KIO::DropJob *job = KIO::drop(event, target);
2687             KJobWidgets::setWindow(job, this);
2688             job->setApplicationActions(additionalActions);
2689             return;
2690         }
2691 
2692     } else {
2693         dropText = mimeData->text();
2694     }
2695 
2696     if (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list"))) {
2697         Q_EMIT sendStringToEmu(dropText.toLocal8Bit());
2698     }
2699 
2700     setFocus(Qt::MouseFocusReason);
2701 }
2702 
doDrag()2703 void TerminalDisplay::doDrag()
2704 {
2705     const QMimeData *clipboardMimeData = QApplication::clipboard()->mimeData(QClipboard::Selection);
2706     if (clipboardMimeData == nullptr) {
2707         return;
2708     }
2709     auto mimeData = new QMimeData();
2710     _dragInfo.state = diDragging;
2711     _dragInfo.dragObject = new QDrag(this);
2712     mimeData->setText(clipboardMimeData->text());
2713     mimeData->setHtml(clipboardMimeData->html());
2714     _dragInfo.dragObject->setMimeData(mimeData);
2715     _dragInfo.dragObject->exec(Qt::CopyAction);
2716 }
2717 
setSessionController(SessionController * controller)2718 void TerminalDisplay::setSessionController(SessionController *controller)
2719 {
2720     _sessionController = controller;
2721     _headerBar->finishHeaderSetup(controller);
2722 }
2723 
sessionController()2724 SessionController *TerminalDisplay::sessionController()
2725 {
2726     return _sessionController;
2727 }
2728 
session() const2729 Session::Ptr TerminalDisplay::session() const
2730 {
2731     return _sessionController->session();
2732 }
2733 
searchBar() const2734 IncrementalSearchBar *TerminalDisplay::searchBar() const
2735 {
2736     return _searchBar;
2737 }
2738 
applyProfile(const Profile::Ptr & profile)2739 void TerminalDisplay::applyProfile(const Profile::Ptr &profile)
2740 {
2741     // load color scheme
2742     _colorScheme = ViewManager::colorSchemeForProfile(profile);
2743     _terminalColor->applyProfile(profile, _colorScheme, randomSeed());
2744     setWallpaper(_colorScheme->wallpaper());
2745 
2746     // load font
2747     _terminalFont->applyProfile(profile);
2748 
2749     // set scroll-bar position
2750     _scrollBar->setScrollBarPosition(Enum::ScrollBarPositionEnum(profile->property<int>(Profile::ScrollBarPosition)));
2751     _scrollBar->setScrollFullPage(profile->property<bool>(Profile::ScrollFullPage));
2752 
2753     // show hint about terminal size after resizing
2754     _showTerminalSizeHint = profile->showTerminalSizeHint();
2755     _dimWhenInactive = profile->dimWhenInactive();
2756 
2757     // terminal features
2758     setBlinkingCursorEnabled(profile->blinkingCursorEnabled());
2759     setBlinkingTextEnabled(profile->blinkingTextEnabled());
2760     _tripleClickMode = Enum::TripleClickModeEnum(profile->property<int>(Profile::TripleClickMode));
2761     setAutoCopySelectedText(profile->autoCopySelectedText());
2762     _ctrlRequiredForDrag = profile->property<bool>(Profile::CtrlRequiredForDrag);
2763     _dropUrlsAsText = profile->property<bool>(Profile::DropUrlsAsText);
2764     _bidiEnabled = profile->bidiRenderingEnabled();
2765     _trimLeadingSpaces = profile->property<bool>(Profile::TrimLeadingSpacesInSelectedText);
2766     _trimTrailingSpaces = profile->property<bool>(Profile::TrimTrailingSpacesInSelectedText);
2767     _openLinksByDirectClick = profile->property<bool>(Profile::OpenLinksByDirectClickEnabled);
2768     setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum(profile->property<int>(Profile::MiddleClickPasteMode)));
2769     setCopyTextAsHTML(profile->property<bool>(Profile::CopyTextAsHTML));
2770 
2771     // highlight lines scrolled into view (must be applied before margin/center)
2772     _scrollBar->setHighlightScrolledLines(profile->property<bool>(Profile::HighlightScrolledLines));
2773 
2774     // reflow lines when terminal resizes
2775     //_screenWindow->screen()->setReflow(profile->property<bool>(Profile::ReflowLines));
2776 
2777     // margin/center
2778     setMargin(profile->property<int>(Profile::TerminalMargin));
2779     setCenterContents(profile->property<bool>(Profile::TerminalCenter));
2780 
2781     // cursor shape
2782     setKeyboardCursorShape(Enum::CursorShapeEnum(profile->property<int>(Profile::CursorShape)));
2783 
2784     // word characters
2785     setWordCharacters(profile->wordCharacters());
2786 
2787     // bell mode
2788     _bell.setBellMode(Enum::BellModeEnum(profile->property<int>(Profile::BellMode)));
2789 
2790     // mouse wheel zoom
2791     _mouseWheelZoom = profile->mouseWheelZoomEnabled();
2792 
2793     _displayVerticalLine = profile->verticalLine();
2794     _displayVerticalLineAtChar = profile->verticalLineAtChar();
2795     _scrollBar->setAlternateScrolling(profile->property<bool>(Profile::AlternateScrolling));
2796     _dimValue = profile->dimValue();
2797 
2798     _filterChain->setUrlHintsModifiers(Qt::KeyboardModifiers(profile->property<int>(Profile::UrlHintsModifiers)));
2799     _filterChain->setReverseUrlHints(profile->property<bool>(Profile::ReverseUrlHints));
2800 
2801     _peekPrimaryShortcut = profile->peekPrimaryKeySequence();
2802 }
2803 
printScreen()2804 void TerminalDisplay::printScreen()
2805 {
2806     auto lprintContent = [this](QPainter &painter, bool friendly) {
2807         QPoint columnLines(_usedLines, _usedColumns);
2808         auto lfontget = [this]() {
2809             return _terminalFont->getVTFont();
2810         };
2811         auto lfontset = [this](const QFont &f) {
2812             _terminalFont->setVTFont(f);
2813         };
2814 
2815         _printManager->printContent(painter, friendly, columnLines, lfontget, lfontset);
2816     };
2817     _printManager->printRequest(lprintContent, this);
2818 }
2819 
getCursorCharacter(int column,int line)2820 Character TerminalDisplay::getCursorCharacter(int column, int line)
2821 {
2822     return _image[loc(column, line)];
2823 }
2824 
selectionState() const2825 int TerminalDisplay::selectionState() const
2826 {
2827     return _actSel;
2828 }
2829