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