File indexing completed on 2024-04-28 05:50:51

0001 /*
0002     SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>
0003     SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 // Own
0009 #include "terminalDisplay/TerminalDisplay.h"
0010 #include "KonsoleSettings.h"
0011 
0012 // Config
0013 #include "config-konsole.h"
0014 
0015 // Qt
0016 #include <QAccessible>
0017 #include <QAction>
0018 #include <QApplication>
0019 #include <QClipboard>
0020 #include <QDesktopServices>
0021 #include <QDrag>
0022 #include <QElapsedTimer>
0023 #include <QEvent>
0024 #include <QFileInfo>
0025 #include <QKeyEvent>
0026 #include <QLabel>
0027 #include <QMimeData>
0028 #include <QPainter>
0029 #include <QScrollEvent>
0030 #include <QScrollPrepareEvent>
0031 #include <QScroller>
0032 #include <QStyle>
0033 #include <QTimer>
0034 #include <QVBoxLayout>
0035 
0036 // KDE
0037 #include <KColorScheme>
0038 #include <KCursor>
0039 #include <KIO/DropJob>
0040 #include <KIO/StatJob>
0041 #include <KJobWidgets>
0042 #include <KLocalizedString>
0043 #include <KMessageBox>
0044 #include <KMessageWidget>
0045 #include <KShell>
0046 #include <kwidgetsaddons_version.h>
0047 
0048 // Konsole
0049 #include "extras/AutoScrollHandler.h"
0050 #include "extras/CompositeWidgetFocusWatcher.h"
0051 
0052 #include "filterHotSpots/HotSpot.h"
0053 #include "filterHotSpots/TerminalImageFilterChain.h"
0054 
0055 #include "../characters/ExtendedCharTable.h"
0056 #include "../characters/LineBlockCharacters.h"
0057 #include "../decoders/PlainTextDecoder.h"
0058 #include "../widgets/KonsolePrintManager.h"
0059 #include "../widgets/TerminalDisplayAccessible.h"
0060 #include "EscapeSequenceUrlExtractor.h"
0061 #include "PrintOptions.h"
0062 #include "Screen.h"
0063 #include "ViewManager.h" // for colorSchemeForProfile. // TODO: Rewrite this.
0064 #include "WindowSystemInfo.h"
0065 #include "profile/Profile.h"
0066 #include "session/Session.h"
0067 #include "session/SessionController.h"
0068 #include "session/SessionManager.h"
0069 #include "widgets/IncrementalSearchBar.h"
0070 
0071 #include "TerminalColor.h"
0072 #include "TerminalFonts.h"
0073 #include "TerminalPainter.h"
0074 #include "TerminalScrollBar.h"
0075 
0076 #include "unicode/ubidi.h"
0077 #include "unicode/uchar.h"
0078 #include "unicode/ushape.h"
0079 #include "unicode/utypes.h"
0080 
0081 #define MAX_LINE_WIDTH 1024
0082 
0083 using namespace Konsole;
0084 
0085 inline int TerminalDisplay::loc(int x, int y) const
0086 {
0087     if (y < 0 || y > _lines) {
0088         qDebug() << "Y: " << y << "Lines" << _lines;
0089     }
0090     if (x < 0 || x > _columns) {
0091         qDebug() << "X" << x << "Columns" << _columns;
0092     }
0093 
0094     Q_ASSERT(y >= 0 && y < _lines);
0095     Q_ASSERT(x >= 0 && x < _columns);
0096     x = qBound(0, x, _columns - 1);
0097     y = qBound(0, y, _lines - 1);
0098 
0099     return y * _columns + x;
0100 }
0101 
0102 /* ------------------------------------------------------------------------- */
0103 /*                                                                           */
0104 /*                                Colors                                     */
0105 /*                                                                           */
0106 /* ------------------------------------------------------------------------- */
0107 
0108 /* Note that we use ANSI color order (bgr), while IBMPC color order is (rgb)
0109 
0110    Code        0       1       2       3       4       5       6       7
0111    ----------- ------- ------- ------- ------- ------- ------- ------- -------
0112    ANSI  (bgr) Black   Red     Green   Yellow  Blue    Magenta Cyan    White
0113    IBMPC (rgb) Black   Blue    Green   Cyan    Red     Magenta Yellow  White
0114 */
0115 
0116 void TerminalDisplay::setScreenWindow(ScreenWindow *window)
0117 {
0118     // disconnect existing screen window if any
0119     if (!_screenWindow.isNull()) {
0120         disconnect(_screenWindow, nullptr, this, nullptr);
0121     }
0122 
0123     _screenWindow = window;
0124 
0125     if (!_screenWindow.isNull()) {
0126         connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, &Konsole::TerminalDisplay::updateImage);
0127         connect(_screenWindow.data(), &Konsole::ScreenWindow::currentResultLineChanged, this, &Konsole::TerminalDisplay::updateImage);
0128         connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, [this]() {
0129             _filterUpdateRequired = true;
0130         });
0131         connect(_screenWindow.data(), &Konsole::ScreenWindow::screenAboutToChange, this, [this]() {
0132             _iPntSel = QPoint(-1, -1);
0133             _pntSel = QPoint(-1, -1);
0134             _tripleSelBegin = QPoint(-1, -1);
0135         });
0136         connect(_screenWindow.data(), &Konsole::ScreenWindow::scrolled, this, [this]() {
0137             _filterUpdateRequired = true;
0138         });
0139         connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, []() {
0140             QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle);
0141         });
0142         if (_wallpaper->isAnimated()) {
0143             QTimer *frameTimer = new QTimer(this);
0144             connect(frameTimer, &QTimer::timeout, this, [this]() -> void {
0145                 update();
0146             });
0147             frameTimer->start(_wallpaper->getFrameDelay());
0148         }
0149         _screenWindow->setWindowLines(_lines);
0150 
0151         auto profile = SessionManager::instance()->sessionProfile(_sessionController->session());
0152         _screenWindow->screen()->setReflowLines(profile->property<bool>(Profile::ReflowLines));
0153         _screenWindow->screen()->setIgnoreWcWidth(profile->property<bool>(Profile::IgnoreWcWidth));
0154 
0155         if (_screenWindow->screen()->urlExtractor()) {
0156             _screenWindow->screen()->urlExtractor()->setAllowedLinkSchema(profile->escapedLinksSchema());
0157         }
0158     }
0159 }
0160 
0161 static UCharDirection BiDiClass([[maybe_unused]] const void *context, UChar32 c)
0162 {
0163     if (c >= 0x2500 && c <= 0x25ff) {
0164         return U_LEFT_TO_RIGHT;
0165     }
0166     return U_CHAR_DIRECTION_COUNT;
0167 };
0168 
0169 /* ------------------------------------------------------------------------- */
0170 /*                                                                           */
0171 /*                         Accessibility                                     */
0172 /*                                                                           */
0173 /* ------------------------------------------------------------------------- */
0174 
0175 namespace Konsole
0176 {
0177 #ifndef QT_NO_ACCESSIBILITY
0178 /**
0179  * This function installs the factory function which lets Qt instantiate the QAccessibleInterface
0180  * for the TerminalDisplay.
0181  */
0182 QAccessibleInterface *accessibleInterfaceFactory(const QString &key, QObject *object)
0183 {
0184     Q_UNUSED(key)
0185     if (auto *display = qobject_cast<TerminalDisplay *>(object)) {
0186         return new TerminalDisplayAccessible(display);
0187     }
0188     return nullptr;
0189 }
0190 
0191 #endif
0192 }
0193 
0194 /* ------------------------------------------------------------------------- */
0195 /*                                                                           */
0196 /*                         Constructor / Destructor                          */
0197 /*                                                                           */
0198 /* ------------------------------------------------------------------------- */
0199 
0200 int TerminalDisplay::lastViewId = -1;
0201 
0202 TerminalDisplay::TerminalDisplay(QWidget *parent)
0203     : QWidget(parent)
0204     , _verticalLayout(new QVBoxLayout(this))
0205     , _iPntSel(QPoint(-1, -1))
0206     , _pntSel(QPoint(-1, -1))
0207     , _tripleSelBegin(QPoint(-1, -1))
0208     , _wordCharacters(QStringLiteral(":@-./_~"))
0209     , _filterChain(new TerminalImageFilterChain(this))
0210     , _searchBar(new IncrementalSearchBar(this))
0211     , _headerBar(new TerminalHeaderBar(this))
0212     , _terminalFont(std::make_unique<TerminalFont>(this))
0213     , _id(++lastViewId)
0214 {
0215     // terminal applications are not designed with Right-To-Left in mind,
0216     // so the layout is forced to Left-To-Right
0217     setLayoutDirection(Qt::LeftToRight);
0218 
0219     // Auto uppercase and predictive text doesn't make sense for terminal.
0220     setInputMethodHints(Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText);
0221 
0222     _contentRect = QRect(_margin, _margin, 1, 1);
0223 
0224     // create scroll bar for scrolling output up and down
0225     _scrollBar = new TerminalScrollBar(this);
0226     _scrollBar->setAutoFillBackground(false);
0227     // set the scroll bar's slider to occupy the whole area of the scroll bar initially
0228     _scrollBar->setScroll(0, 0);
0229     _scrollBar->setCursor(Qt::ArrowCursor);
0230     _headerBar->setCursor(Qt::ArrowCursor);
0231     connect(_headerBar, &TerminalHeaderBar::requestToggleExpansion, this, &Konsole::TerminalDisplay::requestToggleExpansion);
0232     connect(_headerBar, &TerminalHeaderBar::requestMoveToNewTab, this, [this] {
0233         requestMoveToNewTab(this);
0234     });
0235     connect(_scrollBar, &QScrollBar::sliderMoved, this, &Konsole::TerminalDisplay::viewScrolledByUser);
0236 
0237     // setup timers for blinking text
0238     _blinkTextTimer = new QTimer(this);
0239     _blinkTextTimer->setInterval(TEXT_BLINK_DELAY);
0240     connect(_blinkTextTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkTextEvent);
0241 
0242     // setup timers for blinking cursor
0243     _blinkCursorTimer = new QTimer(this);
0244     _blinkCursorTimer->setInterval(QApplication::cursorFlashTime() / 2);
0245     connect(_blinkCursorTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkCursorEvent);
0246 
0247     // hide mouse cursor on keystroke or idle
0248     KCursor::setAutoHideCursor(this, true);
0249     setMouseTracking(true);
0250 
0251     setUsesMouseTracking(false);
0252     setBracketedPasteMode(false);
0253 
0254     // Enable drag and drop support
0255     setAcceptDrops(true);
0256     _dragInfo.state = diNone;
0257 
0258     setFocusPolicy(Qt::WheelFocus);
0259 
0260     // enable input method support
0261     setAttribute(Qt::WA_InputMethodEnabled, true);
0262 
0263     // this is an important optimization, it tells Qt
0264     // that TerminalDisplay will handle repainting its entire area.
0265     setAttribute(Qt::WA_OpaquePaintEvent);
0266 
0267     setAttribute(Qt::WA_AcceptTouchEvents, true);
0268 
0269     QScrollerProperties prop;
0270     prop.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.3);
0271     prop.setScrollMetric(QScrollerProperties::MaximumVelocity, 1);
0272     // Workaround for QTBUG-88249 (non-flick gestures recognized as accelerating flick)
0273     prop.setScrollMetric(QScrollerProperties::AcceleratingFlickMaximumTime, 0.2);
0274     prop.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
0275     prop.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
0276     prop.setScrollMetric(QScrollerProperties::DragStartDistance, 0.0);
0277     QScroller::scroller(this)->setScrollerProperties(prop);
0278     QScroller::scroller(this)->grabGesture(this);
0279 
0280     // Add the stretch item once, the KMessageWidgets are inserted at index 0.
0281     _verticalLayout->addWidget(_headerBar);
0282     _verticalLayout->addStretch();
0283     _verticalLayout->setSpacing(0);
0284     _verticalLayout->setContentsMargins(0, 0, 0, 0);
0285     setLayout(_verticalLayout);
0286     new AutoScrollHandler(this);
0287 
0288     // Keep this last
0289     CompositeWidgetFocusWatcher *focusWatcher = new CompositeWidgetFocusWatcher(this);
0290     connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged, this, [this](bool focused) {
0291         _hasCompositeFocus = focused;
0292     });
0293     connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged, this, &TerminalDisplay::compositeFocusChanged);
0294     connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged, _headerBar, &TerminalHeaderBar::setFocusIndicatorState);
0295 
0296     connect(&_bell, &TerminalBell::visualBell, this, [this] {
0297         _terminalColor->visualBell();
0298     });
0299 
0300 #ifndef QT_NO_ACCESSIBILITY
0301     QAccessible::installFactory(Konsole::accessibleInterfaceFactory);
0302 #endif
0303 
0304     connect(KonsoleSettings::self(), &KonsoleSettings::configChanged, this, &TerminalDisplay::setupHeaderVisibility);
0305 
0306     _terminalColor = new TerminalColor(this);
0307     connect(_terminalColor, &TerminalColor::onPalette, _scrollBar, &TerminalScrollBar::updatePalette);
0308 
0309     _terminalPainter = new TerminalPainter(this);
0310 
0311     auto ldrawBackground = [this](QPainter &painter, const QRect &rect, const QColor &backgroundColor, bool useOpacitySetting) {
0312         _terminalPainter->drawBackground(painter, rect, backgroundColor, useOpacitySetting);
0313     };
0314     auto ldrawContents = [this](QPainter &paint, const QRect &rect, bool friendly) {
0315         _terminalPainter->drawContents(_image, paint, rect, friendly, _imageSize, _bidiEnabled, _lineProperties);
0316     };
0317     auto lgetBackgroundColor = [this]() {
0318         return _terminalColor->backgroundColor();
0319     };
0320 
0321     _printManager.reset(new KonsolePrintManager(ldrawBackground, ldrawContents, lgetBackgroundColor));
0322     ubidi = ubidi_open();
0323 }
0324 
0325 TerminalDisplay::~TerminalDisplay()
0326 {
0327     disconnect(_blinkTextTimer);
0328     disconnect(_blinkCursorTimer);
0329 
0330     delete[] _image;
0331     delete _filterChain;
0332 
0333     ubidi_close(ubidi);
0334 }
0335 
0336 void TerminalDisplay::setupHeaderVisibility()
0337 {
0338     _headerBar->applyVisibilitySettings();
0339     calcGeometry();
0340 }
0341 
0342 void TerminalDisplay::hideDragTarget()
0343 {
0344     _drawOverlay = false;
0345     update();
0346 }
0347 
0348 void TerminalDisplay::showDragTarget(const QPoint &cursorPos)
0349 {
0350     using EdgeDistance = std::pair<int, Qt::Edge>;
0351     auto closerToEdge = std::min<EdgeDistance>(
0352         {{cursorPos.x(), Qt::LeftEdge}, {cursorPos.y(), Qt::TopEdge}, {width() - cursorPos.x(), Qt::RightEdge}, {height() - cursorPos.y(), Qt::BottomEdge}},
0353         [](const EdgeDistance &left, const EdgeDistance &right) -> bool {
0354             return left.first < right.first;
0355         });
0356     if (_overlayEdge == closerToEdge.second) {
0357         return;
0358     }
0359     _overlayEdge = closerToEdge.second;
0360     _drawOverlay = true;
0361     update();
0362 }
0363 
0364 /* ------------------------------------------------------------------------- */
0365 /*                                                                           */
0366 /*                             Display Operations                            */
0367 /*                                                                           */
0368 /* ------------------------------------------------------------------------- */
0369 
0370 void TerminalDisplay::setKeyboardCursorShape(Enum::CursorShapeEnum shape)
0371 {
0372     _cursorShape = shape;
0373 }
0374 
0375 void TerminalDisplay::setCursorStyle(Enum::CursorShapeEnum shape, bool isBlinking, const QColor &customColor)
0376 {
0377     setKeyboardCursorShape(shape);
0378 
0379     setBlinkingCursorEnabled(isBlinking);
0380 
0381     if (customColor.isValid()) {
0382         _terminalColor->setCursorColor(customColor);
0383     }
0384 
0385     // when the cursor shape and blinking state are changed via the
0386     // Set Cursor Style (DECSCUSR) escape sequences in vim, and if the
0387     // cursor isn't set to blink, the cursor shape doesn't actually
0388     // change until the cursor is moved by the user; calling update()
0389     // makes the cursor shape get updated sooner.
0390     if (!isBlinking) {
0391         update();
0392     }
0393 }
0394 void TerminalDisplay::resetCursorStyle()
0395 {
0396     Q_ASSERT(_sessionController != nullptr);
0397     Q_ASSERT(!_sessionController->session().isNull());
0398 
0399     Profile::Ptr currentProfile = SessionManager::instance()->sessionProfile(_sessionController->session());
0400 
0401     if (currentProfile != nullptr) {
0402         auto shape = static_cast<Enum::CursorShapeEnum>(currentProfile->property<int>(Profile::CursorShape));
0403 
0404         setKeyboardCursorShape(shape);
0405         setBlinkingCursorEnabled(currentProfile->blinkingCursorEnabled());
0406     }
0407 }
0408 
0409 void TerminalDisplay::setWallpaper(const ColorSchemeWallpaper::Ptr &p)
0410 {
0411     _wallpaper = p;
0412 }
0413 
0414 void TerminalDisplay::scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode, int amount)
0415 {
0416     _screenWindow->scrollBy(mode, amount, _scrollBar->scrollFullPage());
0417     _screenWindow->setTrackOutput(_screenWindow->atEndOfOutput());
0418     updateImage();
0419     viewScrolledByUser();
0420 }
0421 
0422 void TerminalDisplay::setRandomSeed(uint randomSeed)
0423 {
0424     _randomSeed = randomSeed;
0425 }
0426 uint TerminalDisplay::randomSeed() const
0427 {
0428     return _randomSeed;
0429 }
0430 
0431 void TerminalDisplay::processFilters()
0432 {
0433     if (_screenWindow.isNull()) {
0434         return;
0435     }
0436 
0437     if (!_filterUpdateRequired) {
0438         return;
0439     }
0440 
0441     const QRegion preUpdateHotSpots = _filterChain->hotSpotRegion();
0442 
0443     // use _screenWindow->getImage() here rather than _image because
0444     // other classes may call processFilters() when this display's
0445     // ScreenWindow emits a scrolled() signal - which will happen before
0446     // updateImage() is called on the display and therefore _image is
0447     // out of date at this point
0448     _filterChain->setImage(_screenWindow->getImage(), _screenWindow->windowLines(), _screenWindow->windowColumns(), _screenWindow->getLineProperties());
0449     _filterChain->process();
0450 
0451     const QRegion postUpdateHotSpots = _filterChain->hotSpotRegion();
0452 
0453     update(preUpdateHotSpots | postUpdateHotSpots);
0454     _filterUpdateRequired = false;
0455 }
0456 
0457 void TerminalDisplay::updateImage()
0458 {
0459     if (_screenWindow.isNull()) {
0460         return;
0461     }
0462 
0463     // Better control over screen resizing visual glitches
0464     _screenWindow->updateCurrentLine();
0465 
0466     // optimization - scroll the existing image where possible and
0467     // avoid expensive text drawing for parts of the image that
0468     // can simply be moved up or down
0469     // disable this shortcut for transparent konsole with scaled pixels, otherwise we get rendering artifacts, see BUG 350651
0470     if (!(WindowSystemInfo::HAVE_TRANSPARENCY && (qApp->devicePixelRatio() > 1.0)) && _wallpaper->isNull() && !_searchBar->isVisible()) {
0471         // if the flow control warning is enabled this will interfere with the
0472         // scrolling optimizations and cause artifacts.  the simple solution here
0473         // is to just disable the optimization whilst it is visible
0474         if (!((_outputSuspendedMessageWidget != nullptr) && _outputSuspendedMessageWidget->isVisible())
0475             && !((_readOnlyMessageWidget != nullptr) && _readOnlyMessageWidget->isVisible())) {
0476             // hide terminal size label to prevent it being scrolled and show again after scroll
0477             const bool viewResizeWidget = (_resizeWidget != nullptr) && _resizeWidget->isVisible();
0478             if (viewResizeWidget) {
0479                 _resizeWidget->hide();
0480             }
0481             _scrollBar->scrollImage(_screenWindow->scrollCount(), _screenWindow->scrollRegion(), _image, _imageSize);
0482             if (viewResizeWidget) {
0483                 _resizeWidget->show();
0484             }
0485         }
0486     }
0487 
0488     if (_image == nullptr) {
0489         // Create _image.
0490         // The emitted changedContentSizeSignal also leads to getImage being recreated, so do this first.
0491         updateImageSize();
0492     }
0493 
0494     Character *const newimg = _screenWindow->getImage();
0495     const int lines = _screenWindow->windowLines();
0496     const int columns = _screenWindow->windowColumns();
0497     QVector<LineProperty> newLineProperties = _screenWindow->getLineProperties();
0498 
0499     _scrollBar->setScroll(_screenWindow->currentLine(), _screenWindow->lineCount());
0500 
0501     Q_ASSERT(_usedLines <= _lines);
0502     Q_ASSERT(_usedColumns <= _columns);
0503 
0504     int y;
0505     int x;
0506     int len;
0507 
0508     const QPoint tL = contentsRect().topLeft();
0509     const int tLx = tL.x();
0510     const int tLy = tL.y();
0511     _hasTextBlinker = false;
0512 
0513     CharacterColor cf; // undefined
0514 
0515     const int linesToUpdate = qBound(0, lines, _lines);
0516     const int columnsToUpdate = qBound(0, columns, _columns);
0517 
0518     auto dirtyMask = new char[columnsToUpdate + 2];
0519     QRegion dirtyRegion;
0520 
0521     for (y = 0; y < linesToUpdate; ++y) {
0522         const Character *currentLine = &_image[y * _columns];
0523         const Character *const newLine = &newimg[y * columns];
0524 
0525         bool updateLine = false;
0526 
0527         // The dirty mask indicates which characters need repainting. We also
0528         // mark surrounding neighbors dirty, in case the character exceeds
0529         // its cell boundaries
0530         memset(dirtyMask, 0, columnsToUpdate + 2);
0531 
0532         for (x = 0; x < columnsToUpdate; ++x) {
0533             if (newLine[x] != currentLine[x]) {
0534                 dirtyMask[x] = 1;
0535             }
0536         }
0537 
0538         if (!_resizing) { // not while _resizing, we're expecting a paintEvent
0539             for (x = 0; x < columnsToUpdate; ++x) {
0540                 _hasTextBlinker |= newLine[x].rendition.f.blink;
0541 
0542                 // Start drawing if this character or the next one differs.
0543                 // We also take the next one into account to handle the situation
0544                 // where characters exceed their cell width.
0545                 if (dirtyMask[x] != 0) {
0546                     if (newLine[x + 0].isRightHalfOfDoubleWide()) {
0547                         continue;
0548                     }
0549                     const bool lineDraw = LineBlockCharacters::canDraw(newLine[x + 0].character);
0550                     const bool doubleWidth = (x + 1 == columnsToUpdate) ? false : newLine[x + 1].isRightHalfOfDoubleWide();
0551                     const RenditionFlags cr = newLine[x].rendition.all;
0552                     const CharacterColor clipboard = newLine[x].backgroundColor;
0553                     if (newLine[x].foregroundColor != cf) {
0554                         cf = newLine[x].foregroundColor;
0555                     }
0556                     const int lln = columnsToUpdate - x;
0557                     for (len = 1; len < lln; ++len) {
0558                         const Character &ch = newLine[x + len];
0559 
0560                         if (ch.isRightHalfOfDoubleWide()) {
0561                             continue; // Skip trailing part of multi-col chars.
0562                         }
0563 
0564                         const bool nextIsDoubleWidth = (x + len + 1 == columnsToUpdate) ? false : newLine[x + len + 1].isRightHalfOfDoubleWide();
0565 
0566                         if (ch.foregroundColor != cf || ch.backgroundColor != clipboard || (ch.rendition.all & ~RE_EXTENDED_CHAR) != (cr & ~RE_EXTENDED_CHAR)
0567                             || (dirtyMask[x + len] == 0) || LineBlockCharacters::canDraw(ch.character) != lineDraw || nextIsDoubleWidth != doubleWidth) {
0568                             break;
0569                         }
0570                     }
0571                     updateLine = true;
0572                     x += len - 1;
0573                 }
0574             }
0575         }
0576 
0577         if (y >= _lineProperties.count() || y >= newLineProperties.count() || _lineProperties[y] != newLineProperties[y]) {
0578             updateLine = true;
0579         }
0580 
0581         // if the characters on the line are different in the old and the new _image
0582         // then this line must be repainted.
0583         if (updateLine) {
0584             // add the area occupied by this line to the region which needs to be
0585             // repainted
0586             QRect dirtyRect = QRect(_contentRect.left() + tLx,
0587                                     _contentRect.top() + tLy + _terminalFont->fontHeight() * y,
0588                                     _terminalFont->fontWidth() * columnsToUpdate,
0589                                     _terminalFont->fontHeight());
0590 
0591             dirtyRegion |= highdpi_adjust_rect(dirtyRect);
0592         }
0593 
0594         // replace the line of characters in the old _image with the
0595         // current line of the new _image
0596         memcpy((void *)currentLine, (const void *)newLine, columnsToUpdate * sizeof(Character));
0597     }
0598     _lineProperties = newLineProperties;
0599 
0600     // if the new _image is smaller than the previous _image, then ensure that the area
0601     // outside the new _image is cleared
0602     if (linesToUpdate < _usedLines) {
0603         dirtyRegion |= highdpi_adjust_rect(QRect(_contentRect.left() + tLx,
0604                                                  _contentRect.top() + tLy + _terminalFont->fontHeight() * linesToUpdate,
0605                                                  _terminalFont->fontWidth() * _columns,
0606                                                  _terminalFont->fontHeight() * (_usedLines - linesToUpdate)));
0607     }
0608     _usedLines = linesToUpdate;
0609 
0610     if (columnsToUpdate < _usedColumns) {
0611         dirtyRegion |= highdpi_adjust_rect(QRect(_contentRect.left() + tLx + columnsToUpdate * _terminalFont->fontWidth(),
0612                                                  _contentRect.top() + tLy,
0613                                                  _terminalFont->fontWidth() * (_usedColumns - columnsToUpdate),
0614                                                  _terminalFont->fontHeight() * _lines));
0615     }
0616     _usedColumns = columnsToUpdate;
0617 
0618     dirtyRegion |= _inputMethodData.previousPreeditRect;
0619 
0620     if ((_screenWindow->currentResultLine() != -1) && (_screenWindow->scrollCount() != 0)) {
0621         // De-highlight previous result region
0622         dirtyRegion |= _searchResultRect;
0623         // Highlight new result region
0624         dirtyRegion |=
0625             highdpi_adjust_rect(QRect(0,
0626                                       _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _terminalFont->fontHeight(),
0627                                       _columns * _terminalFont->fontWidth(),
0628                                       _terminalFont->fontHeight()));
0629     }
0630 
0631     if (_scrollBar->highlightScrolledLines().isEnabled()) {
0632         dirtyRegion |= _terminalPainter->highlightScrolledLinesRegion(_scrollBar);
0633     }
0634     _screenWindow->resetScrollCount();
0635 
0636     // update the parts of the display which have changed
0637     if (_screenWindow->screen()->hasGraphics()) {
0638         update();
0639     } else {
0640         update(dirtyRegion);
0641     }
0642 
0643     if (_allowBlinkingText && _hasTextBlinker && !_blinkTextTimer->isActive()) {
0644         _blinkTextTimer->start();
0645     }
0646     if (!_hasTextBlinker && _blinkTextTimer->isActive()) {
0647         _blinkTextTimer->stop();
0648         _textBlinking = false;
0649     }
0650     delete[] dirtyMask;
0651 
0652 #ifndef QT_NO_ACCESSIBILITY
0653     QAccessibleEvent dataChangeEvent(this, QAccessible::VisibleDataChanged);
0654     QAccessible::updateAccessibility(&dataChangeEvent);
0655     QAccessibleTextCursorEvent cursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX());
0656     QAccessible::updateAccessibility(&cursorEvent);
0657 #endif
0658 }
0659 void TerminalDisplay::showResizeNotification()
0660 {
0661     showNotification(i18n("Size: %1 x %2", _columns, _lines));
0662 }
0663 
0664 void TerminalDisplay::showNotification(QString text)
0665 {
0666     if ((text.isEmpty() || _showTerminalSizeHint) && isVisible()) {
0667         if (_resizeWidget == nullptr) {
0668             _resizeWidget = new QLabel(text, this);
0669             _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().boundingRect(text).width());
0670             _resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height());
0671             _resizeWidget->setAlignment(Qt::AlignCenter);
0672 
0673             _resizeWidget->setStyleSheet(QStringLiteral("background-color:palette(window);border-style:solid;border-width:1px;border-color:palette(dark)"));
0674 
0675             _resizeTimer = new QTimer(this);
0676             _resizeTimer->setInterval(SIZE_HINT_DURATION);
0677             _resizeTimer->setSingleShot(true);
0678             connect(_resizeTimer, &QTimer::timeout, _resizeWidget, &QLabel::hide);
0679         }
0680         _resizeWidget->setText(text);
0681         _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().boundingRect(text).width() + 16);
0682         _resizeWidget->move((width() - _resizeWidget->width()) / 2, (height() - _resizeWidget->height()) / 2 + 20);
0683         _resizeWidget->show();
0684         _resizeTimer->start();
0685     }
0686 }
0687 
0688 void TerminalDisplay::paintEvent(QPaintEvent *pe)
0689 {
0690     QPainter paint(this);
0691 
0692     // Determine which characters should be repainted (1 region unit = 1 character)
0693     QRegion dirtyImageRegion;
0694     const QRegion region = pe->region() & contentsRect();
0695 
0696     for (const QRect &rect : region) {
0697         dirtyImageRegion += widgetToImage(rect);
0698         // We can use the opacity settings only if we are in a top level window which actually supports opacity.
0699         // Many apps that use a konsole part such as kate or dolphin don't for performance reasons.
0700         // This will result in repaint glitches iin wayland due to missing damage information
0701         const bool useOpacity = window() && window()->testAttribute(Qt::WA_TranslucentBackground);
0702         _terminalPainter->drawBackground(paint, rect, _terminalColor->backgroundColor(), useOpacity);
0703     }
0704 
0705     if (_displayVerticalLine) {
0706         const int fontWidth = _terminalFont->fontWidth();
0707         const int x = (fontWidth / 2) + (fontWidth * _displayVerticalLineAtChar);
0708         const QColor lineColor = _terminalColor->foregroundColor();
0709 
0710         paint.setPen(lineColor);
0711         paint.drawLine(QPoint(x, 0), QPoint(x, height()));
0712     }
0713 
0714     // only turn on text anti-aliasing, never turn on normal antialiasing
0715     // set https://bugreports.qt.io/browse/QTBUG-66036
0716     paint.setRenderHint(QPainter::TextAntialiasing, _terminalFont->antialiasText());
0717 
0718     for (const QRect &rect : std::as_const(dirtyImageRegion)) {
0719         _terminalPainter->drawContents(_image, paint, rect, false, _imageSize, _bidiEnabled, _lineProperties, _screenWindow->screen()->ulColorTable());
0720     }
0721 
0722     if (screenWindow()->currentResultLine() != -1) {
0723         _searchResultRect.setRect(0,
0724                                   contentRect().top() + (screenWindow()->currentResultLine() - screenWindow()->currentLine()) * _terminalFont->fontHeight(),
0725                                   columns() * terminalFont()->fontWidth(),
0726                                   _terminalFont->fontHeight());
0727         _searchResultRect = highdpi_adjust_rect(_searchResultRect);
0728         _terminalPainter->drawCurrentResultRect(paint, _searchResultRect);
0729     }
0730 
0731     if (_scrollBar->highlightScrolledLines().isEnabled()) {
0732         _terminalPainter->highlightScrolledLines(paint, _scrollBar->highlightScrolledLines().isTimerActive(), _scrollBar->highlightScrolledLines().rect());
0733     }
0734     _terminalPainter->drawInputMethodPreeditString(paint, preeditRect(), _inputMethodData, _image);
0735     paintFilters(paint);
0736 
0737     const bool drawBorder = _borderWhenActive && hasFocus();
0738     if (drawBorder) {
0739         paint.setPen(_focusBorderColor);
0740         const auto x = _scrollBar->scrollBarPosition() == Enum::ScrollBarLeft ? _scrollBar->width() : 0;
0741         const auto sb = _scrollBar->scrollBarPosition() != Enum::ScrollBarHidden ? _scrollBar->width() : 0;
0742         const auto y = _headerBar->isVisible() ? _headerBar->height() : 0;
0743         paint.drawRect(x, y, width() - sb - 1, height() - y - 1);
0744     }
0745 
0746     const bool drawDimmed = _dimWhenInactive && !hasFocus();
0747     if (drawDimmed) {
0748         const QColor dimColor(0, 0, 0, _dimValue);
0749         for (const QRect &rect : region) {
0750             paint.fillRect(rect, dimColor);
0751         }
0752     }
0753 
0754     if (_drawOverlay) {
0755         const auto y = _headerBar->isVisible() ? _headerBar->height() : 0;
0756         const auto rect = _overlayEdge == Qt::LeftEdge ? QRect(0, y, width() / 2, height())
0757             : _overlayEdge == Qt::TopEdge              ? QRect(0, y, width(), height() / 2)
0758             : _overlayEdge == Qt::RightEdge            ? QRect(width() - width() / 2, y, width() / 2, height())
0759                                                        : QRect(0, height() - height() / 2, width(), height() / 2);
0760 
0761         paint.setRenderHint(QPainter::Antialiasing);
0762         paint.setPen(Qt::NoPen);
0763         paint.setBrush(QColor(100, 100, 100, 127));
0764         paint.drawRect(rect);
0765     }
0766 }
0767 
0768 QPoint TerminalDisplay::cursorPosition() const
0769 {
0770     if (!_screenWindow.isNull()) {
0771         return _screenWindow->cursorPosition();
0772     } else {
0773         return {0, 0};
0774     }
0775 }
0776 
0777 void TerminalDisplay::setVisualCursorPosition(int x)
0778 {
0779     _visualCursorPosition = {x, cursorPosition().y()};
0780 }
0781 
0782 bool TerminalDisplay::isCursorOnDisplay() const
0783 {
0784     return cursorPosition().x() < _columns && cursorPosition().y() < _lines;
0785 }
0786 
0787 FilterChain *TerminalDisplay::filterChain() const
0788 {
0789     return _filterChain;
0790 }
0791 
0792 void TerminalDisplay::paintFilters(QPainter &painter)
0793 {
0794     if (_filterUpdateRequired) {
0795         return;
0796     }
0797 
0798     _filterChain->paint(this, painter);
0799 }
0800 
0801 QRect TerminalDisplay::imageToWidget(const QRect &imageArea) const
0802 {
0803     QRect result;
0804     const int fontWidth = _terminalFont->fontWidth();
0805     const int fontHeight = _terminalFont->fontHeight();
0806     result.setLeft(_contentRect.left() + fontWidth * imageArea.left());
0807     result.setTop(_contentRect.top() + fontHeight * imageArea.top());
0808     result.setWidth(fontWidth * imageArea.width());
0809     result.setHeight(fontHeight * imageArea.height());
0810 
0811     return result;
0812 }
0813 
0814 QRect TerminalDisplay::widgetToImage(const QRect &widgetArea) const
0815 {
0816     QRect result;
0817     const int fontWidth = _terminalFont->fontWidth();
0818     const int fontHeight = _terminalFont->fontHeight();
0819     result.setLeft(qBound(0, (widgetArea.left() - contentsRect().left() - _contentRect.left()) / fontWidth, _usedColumns - 1));
0820     result.setTop(qBound(0, (widgetArea.top() - contentsRect().top() - _contentRect.top()) / fontHeight, _usedLines - 1));
0821     result.setRight(qBound(0, (widgetArea.right() - contentsRect().left() - _contentRect.left()) / fontWidth, _usedColumns - 1));
0822     result.setBottom(qBound(0, (widgetArea.bottom() - contentsRect().top() - _contentRect.top()) / fontHeight, _usedLines - 1));
0823     return result;
0824 }
0825 
0826 /* ------------------------------------------------------------------------- */
0827 /*                                                                           */
0828 /*                          Blinking Text & Cursor                           */
0829 /*                                                                           */
0830 /* ------------------------------------------------------------------------- */
0831 
0832 void TerminalDisplay::setBlinkingCursorEnabled(bool blink)
0833 {
0834     _allowBlinkingCursor = blink;
0835 
0836     if (blink && !_blinkCursorTimer->isActive()) {
0837         _blinkCursorTimer->start();
0838     }
0839 
0840     if (!blink && _blinkCursorTimer->isActive()) {
0841         _blinkCursorTimer->stop();
0842         if (_cursorBlinking) {
0843             // if cursor is blinking(hidden), blink it again to make it show
0844             _cursorBlinking = false;
0845             updateCursor();
0846         }
0847         Q_ASSERT(!_cursorBlinking);
0848     }
0849 }
0850 
0851 void TerminalDisplay::setBlinkingTextEnabled(bool blink)
0852 {
0853     _allowBlinkingText = blink;
0854 
0855     if (blink && !_blinkTextTimer->isActive()) {
0856         _blinkTextTimer->start();
0857     }
0858 
0859     if (!blink && _blinkTextTimer->isActive()) {
0860         _blinkTextTimer->stop();
0861         _textBlinking = false;
0862     }
0863 }
0864 
0865 void TerminalDisplay::focusOutEvent(QFocusEvent *)
0866 {
0867     // trigger a repaint of the cursor so that it is both:
0868     //
0869     //   * visible (in case it was hidden during blinking)
0870     //   * drawn in a focused out state
0871     _cursorBlinking = false;
0872     updateCursor();
0873 
0874     // suppress further cursor blinking
0875     _blinkCursorTimer->stop();
0876     Q_ASSERT(!_cursorBlinking);
0877 
0878     // if text is blinking (hidden), blink it again to make it shown
0879     if (_textBlinking) {
0880         blinkTextEvent();
0881     }
0882 
0883     // suppress further text blinking
0884     _blinkTextTimer->stop();
0885     Q_ASSERT(!_textBlinking);
0886 
0887     // If waiting for a triple click - losing focus cancels that (do the pending copy)
0888     copyToX11Selection(true);
0889 }
0890 
0891 void TerminalDisplay::focusInEvent(QFocusEvent *)
0892 {
0893     if (_allowBlinkingCursor) {
0894         _blinkCursorTimer->start();
0895     }
0896 
0897     updateCursor();
0898 
0899     if (_allowBlinkingText && _hasTextBlinker) {
0900         _blinkTextTimer->start();
0901     }
0902 }
0903 
0904 void TerminalDisplay::blinkTextEvent()
0905 {
0906     Q_ASSERT(_allowBlinkingText);
0907 
0908     _textBlinking = !_textBlinking;
0909 
0910     // TODO: Optimize to only repaint the areas of the widget where there is
0911     // blinking text rather than repainting the whole widget.
0912     update();
0913 }
0914 
0915 void TerminalDisplay::blinkCursorEvent()
0916 {
0917     Q_ASSERT(_allowBlinkingCursor);
0918 
0919     _cursorBlinking = !_cursorBlinking;
0920     updateCursor();
0921 }
0922 
0923 void TerminalDisplay::updateCursor()
0924 {
0925     if (!isCursorOnDisplay()) {
0926         return;
0927     }
0928 
0929     const int cursorLocation = loc(cursorPosition().x(), cursorPosition().y());
0930     Q_ASSERT(cursorLocation < _imageSize);
0931 
0932     int charWidth = _image[cursorLocation].width();
0933     QRect cursorRect = imageToWidget(QRect(_visualCursorPosition, QSize(charWidth, 1)));
0934     update(cursorRect);
0935 }
0936 
0937 /* ------------------------------------------------------------------------- */
0938 /*                                                                           */
0939 /*                          Geometry & Resizing                              */
0940 /*                                                                           */
0941 /* ------------------------------------------------------------------------- */
0942 
0943 void TerminalDisplay::resizeEvent(QResizeEvent *event)
0944 {
0945     Q_UNUSED(event)
0946 
0947     if (contentsRect().isValid()) {
0948         // NOTE: This calls setTabText() in TabbedViewContainer::updateTitle(),
0949         // which might update the widget size again. New resizeEvent
0950         // won't be called, do not rely on new sizes before this call.
0951         updateImageSize();
0952         updateImage();
0953     }
0954 
0955     const auto scrollBarWidth = _scrollBar->scrollBarPosition() != Enum::ScrollBarHidden ? _scrollBar->width() : 0;
0956     const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0;
0957 
0958     const auto x = width() - scrollBarWidth - _searchBar->width();
0959     const auto y = headerHeight;
0960     _searchBar->move(x, y);
0961 }
0962 
0963 void TerminalDisplay::propagateSize()
0964 {
0965     if (_image != nullptr) {
0966         updateImageSize();
0967     }
0968 }
0969 
0970 void TerminalDisplay::updateImageSize()
0971 {
0972     Character *oldImage = _image;
0973     const int oldLines = _lines;
0974     const int oldColumns = _columns;
0975 
0976     makeImage();
0977 
0978     if (oldImage != nullptr) {
0979         // copy the old image to reduce flicker
0980         int lines = qMin(oldLines, _lines);
0981         int columns = qMin(oldColumns, _columns);
0982         for (int line = 0; line < lines; line++) {
0983             memcpy((void *)&_image[_columns * line], (void *)&oldImage[oldColumns * line], columns * sizeof(Character));
0984         }
0985         delete[] oldImage;
0986     }
0987 
0988     if (!_screenWindow.isNull()) {
0989         _screenWindow->setWindowLines(_lines);
0990         _sessionController->setSelectMode(false);
0991     }
0992 
0993     _resizing = (oldLines != _lines) || (oldColumns != _columns);
0994 
0995     if (_resizing) {
0996         _iPntSel = QPoint(-1, -1);
0997         _pntSel = QPoint(-1, -1);
0998         _tripleSelBegin = QPoint(-1, -1);
0999         showResizeNotification();
1000         Q_EMIT changedContentSizeSignal(_contentRect.height(), _contentRect.width()); // expose resizeEvent
1001     }
1002 
1003     _resizing = false;
1004 }
1005 
1006 void TerminalDisplay::makeImage()
1007 {
1008     _wallpaper->load();
1009 
1010     calcGeometry();
1011 
1012     // confirm that array will be of non-zero size, since the painting code
1013     // assumes a non-zero array length
1014     Q_ASSERT(_lines > 0 && _columns > 0);
1015     Q_ASSERT(_usedLines <= _lines && _usedColumns <= _columns);
1016 
1017     _imageSize = _lines * _columns;
1018 
1019     _image = new Character[_imageSize];
1020 
1021     clearImage();
1022 }
1023 
1024 void TerminalDisplay::clearImage()
1025 {
1026     std::fill(_image, _image + _imageSize, Screen::DefaultChar);
1027 }
1028 
1029 void TerminalDisplay::calcGeometry()
1030 {
1031     const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0;
1032 
1033     _scrollBar->resize(_scrollBar->sizeHint().width(), // width
1034                        contentsRect().height() - headerHeight // height
1035     );
1036 
1037     _contentRect = contentsRect().adjusted(
1038         _margin + (_scrollBar->highlightScrolledLines().isEnabled() ? _scrollBar->highlightScrolledLines().HIGHLIGHT_SCROLLED_LINES_WIDTH : 0),
1039         _margin,
1040         -_margin - (_scrollBar->highlightScrolledLines().isEnabled() ? _scrollBar->highlightScrolledLines().HIGHLIGHT_SCROLLED_LINES_WIDTH : 0),
1041         -_margin);
1042 
1043     switch (_scrollBar->scrollBarPosition()) {
1044     case Enum::ScrollBarHidden:
1045         break;
1046     case Enum::ScrollBarLeft:
1047         _contentRect.setLeft(_contentRect.left() + _scrollBar->width());
1048         _scrollBar->move(contentsRect().left(), contentsRect().top() + headerHeight);
1049         break;
1050     case Enum::ScrollBarRight:
1051         _contentRect.setRight(_contentRect.right() - _scrollBar->width());
1052         _scrollBar->move(contentsRect().left() + contentsRect().width() - _scrollBar->width(), contentsRect().top() + headerHeight);
1053         break;
1054     }
1055 
1056     _contentRect.setTop(_contentRect.top() + headerHeight);
1057 
1058     int fontWidth = _terminalFont->fontWidth();
1059 
1060     // ensure that display is always at least one column wide,
1061     // and clamp it to MAX_LINE_WIDTH-1 wide to prevent text shaping buffer overflows
1062     _columns = qBound(1, _contentRect.width() / fontWidth, MAX_LINE_WIDTH - 1);
1063     _usedColumns = qMin(_usedColumns, _columns);
1064 
1065     // ensure that display is always at least one line high
1066     _lines = qMax(1, _contentRect.height() / _terminalFont->fontHeight());
1067     _usedLines = qMin(_usedLines, _lines);
1068 
1069     if (_centerContents) {
1070         QSize unusedPixels = _contentRect.size() - QSize(_columns * fontWidth, _lines * _terminalFont->fontHeight());
1071         _contentRect.adjust(unusedPixels.width() / 2, unusedPixels.height() / 2, 0, 0);
1072     }
1073 }
1074 
1075 // calculate the needed size, this must be synced with calcGeometry()
1076 void TerminalDisplay::setSize(int columns, int lines)
1077 {
1078     const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->sizeHint().width();
1079     const int horizontalMargin = _margin * 2;
1080     const int verticalMargin = _margin * 2;
1081 
1082     QSize newSize = QSize(horizontalMargin + scrollBarWidth + (columns * _terminalFont->fontWidth()), verticalMargin + (lines * _terminalFont->fontHeight()));
1083 
1084     if (newSize != size()) {
1085         _size = newSize;
1086         updateGeometry();
1087     }
1088 }
1089 
1090 QSize TerminalDisplay::sizeHint() const
1091 {
1092     return _size;
1093 }
1094 
1095 // showEvent and hideEvent are reimplemented here so that it appears to other classes that the
1096 // display has been resized when the display is hidden or shown.
1097 //
1098 // TODO: Perhaps it would be better to have separate signals for show and hide instead of using
1099 // the same signal as the one for a content size change
1100 void TerminalDisplay::showEvent(QShowEvent *)
1101 {
1102     propagateSize();
1103     Q_EMIT changedContentSizeSignal(_contentRect.height(), _contentRect.width());
1104 }
1105 void TerminalDisplay::hideEvent(QHideEvent *)
1106 {
1107     Q_EMIT changedContentSizeSignal(_contentRect.height(), _contentRect.width());
1108 }
1109 
1110 void TerminalDisplay::setMargin(int margin)
1111 {
1112     if (margin < 0) {
1113         margin = 0;
1114     }
1115     _margin = margin;
1116     updateImageSize();
1117 }
1118 
1119 void TerminalDisplay::setCenterContents(bool enable)
1120 {
1121     _centerContents = enable;
1122     calcGeometry();
1123     update();
1124 }
1125 
1126 /* ------------------------------------------------------------------------- */
1127 /*                                                                           */
1128 /*                                  Mouse                                    */
1129 /*                                                                           */
1130 /* ------------------------------------------------------------------------- */
1131 
1132 inline int mouseButton(int button, Qt::KeyboardModifiers modifiers)
1133 {
1134     // Modifier keys, we don't support Shift (4), because it's used to bypass Mouse Tracking mode
1135     if (modifiers & Qt::AltModifier) {
1136         button += 8;
1137     }
1138     if (modifiers & Qt::ControlModifier) {
1139         button += 16;
1140     }
1141 
1142     return button;
1143 }
1144 
1145 void TerminalDisplay::mousePressEvent(QMouseEvent *ev)
1146 {
1147     if (!contentsRect().contains(ev->pos())) {
1148         return;
1149     }
1150 
1151     if (!_screenWindow) {
1152         return;
1153     }
1154 
1155     if (_possibleTripleClick && (ev->button() == Qt::LeftButton)) {
1156         mouseTripleClickEvent(ev);
1157         return;
1158     }
1159 
1160     // Ignore clicks on the message widget
1161     if (_readOnlyMessageWidget != nullptr && _readOnlyMessageWidget->isVisible() && _readOnlyMessageWidget->frameGeometry().contains(ev->pos())) {
1162         return;
1163     }
1164 
1165     if (_outputSuspendedMessageWidget != nullptr && _outputSuspendedMessageWidget->isVisible()
1166         && _outputSuspendedMessageWidget->frameGeometry().contains(ev->pos())) {
1167         return;
1168     }
1169 
1170     auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !usesMouseTracking());
1171     QPoint pos = QPoint(charColumn, charLine);
1172 
1173     processFilters();
1174 
1175     _filterChain->mouseMoveEvent(this, ev, charLine, charColumn);
1176     auto hotSpotClick = _filterChain->hotSpotAt(charLine, charColumn);
1177     if (hotSpotClick && hotSpotClick->hasDragOperation() && ev->modifiers() & Qt::Modifier::ALT) {
1178         hotSpotClick->startDrag();
1179         return;
1180     }
1181 
1182     if (ev->button() == Qt::LeftButton) {
1183         // request the software keyboard, if any
1184         if (qApp->autoSipEnabled()) {
1185             auto behavior = QStyle::RequestSoftwareInputPanel(style()->styleHint(QStyle::SH_RequestSoftwareInputPanel));
1186             if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) {
1187                 QEvent event(QEvent::RequestSoftwareInputPanel);
1188                 QApplication::sendEvent(this, &event);
1189             }
1190         }
1191 
1192         if ((!usesMouseTracking() && !ev->modifiers()) || (usesMouseTracking() && ev->modifiers() == Qt::ShiftModifier)) {
1193             _lineSelectionMode = false;
1194             _wordSelectionMode = false;
1195         }
1196 
1197         // The user clicked inside selected text
1198         bool selected = _screenWindow->isSelected(pos.x(), pos.y());
1199 
1200         // Drag only when the Control key is held
1201         if ((!_ctrlRequiredForDrag || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && selected) {
1202             _dragInfo.state = diPending;
1203             _dragInfo.start = ev->pos();
1204         } else {
1205             // No reason to ever start a drag event
1206             _dragInfo.state = diNone;
1207 
1208             _preserveLineBreaks = !(((ev->modifiers() & Qt::ControlModifier) != 0u) && !(ev->modifiers() & Qt::AltModifier));
1209             _columnSelectionMode = ((ev->modifiers() & Qt::AltModifier) != 0u) && ((ev->modifiers() & Qt::ControlModifier) != 0u);
1210 
1211             // There are a couple of use cases when selecting text :
1212             // Normal buffer or Alternate buffer when not using Mouse Tracking:
1213             //  select text or extendSelection or columnSelection or columnSelection + extendSelection
1214             //
1215             // Alternate buffer when using Mouse Tracking and with Shift pressed:
1216             //  select text or columnSelection
1217             if (!usesMouseTracking() && ((ev->modifiers() == Qt::ShiftModifier) || (((ev->modifiers() & Qt::ShiftModifier) != 0u) && _columnSelectionMode))) {
1218                 extendSelection(ev->pos());
1219             } else if ((!usesMouseTracking() && !((ev->modifiers() & Qt::ShiftModifier)))
1220                        || (usesMouseTracking() && ((ev->modifiers() & Qt::ShiftModifier) != 0u))) {
1221                 clearSelection();
1222 
1223                 pos.ry() += _scrollBar->value();
1224                 _iPntSel = _pntSel = pos;
1225                 _actSel = 1; // left mouse button pressed but nothing selected yet.
1226             } else if (usesMouseTracking() && !_readOnly) {
1227                 Q_EMIT mouseSignal(mouseButton(0, ev->modifiers()), charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0);
1228             }
1229         }
1230         if (_semanticInputClick && (ev->modifiers() & Qt::ControlModifier) == 0 && _screenWindow->screen()->replMode() == REPL_INPUT) {
1231             Q_EMIT mouseSignal(mouseButton(0, ev->modifiers()), charColumn, charLine + _scrollBar->value() - _scrollBar->maximum(), 3);
1232         }
1233 
1234     } else if (ev->button() == Qt::MiddleButton) {
1235         processMidButtonClick(ev);
1236     } else if (ev->button() == Qt::RightButton) {
1237         if (!usesMouseTracking() || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) {
1238             Q_EMIT configureRequest(ev->pos());
1239         } else {
1240             if (!_readOnly) {
1241                 Q_EMIT mouseSignal(mouseButton(2, ev->modifiers()), charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0);
1242             }
1243         }
1244     }
1245 }
1246 
1247 QSharedPointer<HotSpot> TerminalDisplay::filterActions(const QPoint &position)
1248 {
1249     auto [charLine, charColumn] = getCharacterPosition(position, false);
1250     return _filterChain->hotSpotAt(charLine, charColumn);
1251 }
1252 
1253 void TerminalDisplay::mouseMoveEvent(QMouseEvent *ev)
1254 {
1255     if (QScroller::scroller(this)->state() != QScroller::Inactive) {
1256         // Touchscreen is handled by scrollEvent()
1257         return;
1258     }
1259 
1260     if (!hasFocus() && KonsoleSettings::focusFollowsMouse()) {
1261         setFocus();
1262     }
1263 
1264     if (_possibleTripleClick && (ev->pos() - _tripleClickPos).manhattanLength() > 20) {
1265         _possibleTripleClick = false;
1266         copyToX11Selection(true);
1267     }
1268     auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !usesMouseTracking());
1269 
1270     processFilters();
1271 
1272     _filterChain->mouseMoveEvent(this, ev, charLine, charColumn);
1273 
1274     // if the program running in the terminal is interested in Mouse Tracking
1275     // events then emit a mouse movement signal, unless the shift key is
1276     // being held down, which overrides this.
1277     if (usesMouseTracking() && !(ev->modifiers() & Qt::ShiftModifier)) {
1278         // Ignore mouse movements that don't change the character position.
1279         if (charLine == _prevCharacterLine && charColumn == _prevCharacterColumn) {
1280             return;
1281         }
1282 
1283         _prevCharacterLine = charLine;
1284         _prevCharacterColumn = charColumn;
1285 
1286         if (!_readOnly) {
1287             int button = 3;
1288             if ((ev->buttons() & Qt::LeftButton) != 0u) {
1289                 button = 0;
1290             }
1291             if ((ev->buttons() & Qt::MiddleButton) != 0u) {
1292                 button = 1;
1293             }
1294             if ((ev->buttons() & Qt::RightButton) != 0u) {
1295                 button = 2;
1296             }
1297 
1298             Q_EMIT mouseSignal(mouseButton(button, ev->modifiers()), charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 1);
1299         }
1300 
1301         return;
1302     }
1303 
1304     // for auto-hiding the cursor, we need mouseTracking
1305     if (ev->buttons() == Qt::NoButton) {
1306         return;
1307     }
1308 
1309     if (_dragInfo.state == diPending) {
1310         // we had a mouse down, but haven't confirmed a drag yet
1311         // if the mouse has moved sufficiently, we will confirm
1312 
1313         const int distance = QApplication::startDragDistance();
1314         if (ev->position().x() > _dragInfo.start.x() + distance || ev->position().x() < _dragInfo.start.x() - distance
1315             || ev->position().y() > _dragInfo.start.y() + distance || ev->position().y() < _dragInfo.start.y() - distance) {
1316             // we've left the drag square, we can start a real drag operation now
1317 
1318             clearSelection();
1319             doDrag();
1320         }
1321         return;
1322     } else if (_dragInfo.state == diDragging) {
1323         // this isn't technically needed because mouseMoveEvent is suppressed during
1324         // Qt drag operations, replaced by dragMoveEvent
1325         return;
1326     }
1327 
1328     if (_actSel == 0) {
1329         return;
1330     }
1331 
1332     // don't extend selection while pasting
1333     if ((ev->buttons() & Qt::MiddleButton) != 0u) {
1334         return;
1335     }
1336 
1337     extendSelection(ev->pos());
1338 }
1339 
1340 void TerminalDisplay::leaveEvent(QEvent *ev)
1341 {
1342     // remove underline from an active link when cursor leaves the widget area,
1343     // also restore regular mouse cursor shape
1344     _filterChain->leaveEvent(this, ev);
1345 }
1346 
1347 void TerminalDisplay::extendSelection(const QPoint &position)
1348 {
1349     if (_screenWindow.isNull()) {
1350         return;
1351     }
1352 
1353     if (_iPntSel.x() < 0 || _iPntSel.y() < 0 || _pntSel.x() < 0 || _pntSel.y() < 0) {
1354         _iPntSel = _pntSel = _screenWindow->cursorPosition();
1355         _iPntSel.ry() += _scrollBar->value();
1356         _pntSel.ry() += _scrollBar->value();
1357     }
1358 
1359     // if ( !contentsRect().contains(ev->pos()) ) return;
1360     const QPoint tL = contentsRect().topLeft();
1361     const int tLx = tL.x();
1362     const int tLy = tL.y();
1363     const int scroll = _scrollBar->value();
1364 
1365     // we're in the process of moving the mouse with the left button pressed
1366     // the mouse cursor will kept caught within the bounds of the text in
1367     // this widget.
1368 
1369     int linesBeyondWidget = 0;
1370 
1371     QRect textBounds(tLx + _contentRect.left(),
1372                      tLy + _contentRect.top(),
1373                      _usedColumns * _terminalFont->fontWidth() - 1,
1374                      _usedLines * _terminalFont->fontHeight() - 1);
1375 
1376     QPoint pos = position;
1377 
1378     // Adjust position within text area bounds.
1379     const QPoint oldpos = pos;
1380 
1381     pos.setX(qBound(textBounds.left(), pos.x(), textBounds.right()));
1382     pos.setY(qBound(textBounds.top(), pos.y(), textBounds.bottom()));
1383 
1384     if (oldpos.y() > textBounds.bottom()) {
1385         linesBeyondWidget = (oldpos.y() - textBounds.bottom()) / _terminalFont->fontHeight();
1386         _scrollBar->setValue(_scrollBar->value() + linesBeyondWidget + 1); // scrollforward
1387     }
1388     if (oldpos.y() < textBounds.top()) {
1389         linesBeyondWidget = (textBounds.top() - oldpos.y()) / _terminalFont->fontHeight();
1390         _scrollBar->setValue(_scrollBar->value() - linesBeyondWidget - 1); // history
1391     }
1392 
1393     auto [charLine, charColumn] = getCharacterPosition(pos, true);
1394 
1395     QPoint here = QPoint(charColumn, charLine);
1396     QPoint ohere;
1397     QPoint iPntSelCorr = _iPntSel;
1398     iPntSelCorr.ry() -= _scrollBar->value();
1399     QPoint pntSelCorr = _pntSel;
1400     pntSelCorr.ry() -= _scrollBar->value();
1401     bool swapping = false;
1402 
1403     if (_wordSelectionMode) {
1404         // Extend to word boundaries
1405         const bool left_not_right = (here.y() < iPntSelCorr.y() || (here.y() == iPntSelCorr.y() && here.x() < iPntSelCorr.x()));
1406         const bool old_left_not_right = (pntSelCorr.y() < iPntSelCorr.y() || (pntSelCorr.y() == iPntSelCorr.y() && pntSelCorr.x() < iPntSelCorr.x()));
1407         swapping = left_not_right != old_left_not_right;
1408 
1409         // Find left (left_not_right ? from here : from start of word)
1410         QPoint left = left_not_right ? here : iPntSelCorr;
1411         // Find left (left_not_right ? from end of word : from here)
1412         QPoint right = left_not_right ? iPntSelCorr : here;
1413 
1414         if (left.y() < 0 || left.y() >= _lines || left.x() < 0 || left.x() >= _columns) {
1415             left = pntSelCorr;
1416         } else {
1417             left = findWordStart(left);
1418         }
1419         if (right.y() < 0 || right.y() >= _lines || right.x() < 0 || right.x() >= _columns) {
1420             right = pntSelCorr;
1421         } else {
1422             right = findWordEnd(right);
1423         }
1424 
1425         // Pick which is start (ohere) and which is extension (here)
1426         if (left_not_right) {
1427             here = left;
1428             ohere = right;
1429         } else {
1430             here = right;
1431             ohere = left;
1432         }
1433         ohere.rx()++;
1434     }
1435 
1436     if (_lineSelectionMode) {
1437         // Extend to complete line
1438         const bool above_not_below = (here.y() < iPntSelCorr.y());
1439         if (above_not_below) {
1440             ohere = findLineEnd(iPntSelCorr);
1441             here = findLineStart(here);
1442         } else {
1443             ohere = findLineStart(iPntSelCorr);
1444             here = findLineEnd(here);
1445         }
1446 
1447         swapping = !(_tripleSelBegin == ohere);
1448         _tripleSelBegin = ohere;
1449 
1450         ohere.rx()++;
1451     }
1452 
1453     int offset = 0;
1454     if (!_wordSelectionMode && !_lineSelectionMode) {
1455         const bool left_not_right = (here.y() < iPntSelCorr.y() || (here.y() == iPntSelCorr.y() && here.x() < iPntSelCorr.x()));
1456         const bool old_left_not_right = (pntSelCorr.y() < iPntSelCorr.y() || (pntSelCorr.y() == iPntSelCorr.y() && pntSelCorr.x() < iPntSelCorr.x()));
1457         swapping = left_not_right != old_left_not_right;
1458 
1459         // Find left (left_not_right ? from here : from start)
1460         const QPoint left = left_not_right ? here : iPntSelCorr;
1461 
1462         // Find right (left_not_right ? from start : from here)
1463         QPoint right = left_not_right ? iPntSelCorr : here;
1464 
1465         // Pick which is start (ohere) and which is extension (here)
1466         if (left_not_right) {
1467             here = left;
1468             ohere = right;
1469             offset = 0;
1470         } else {
1471             here = right;
1472             ohere = left;
1473             offset = -1;
1474         }
1475     }
1476 
1477     if ((here == pntSelCorr) && (scroll == _scrollBar->value())) {
1478         return; // not moved
1479     }
1480 
1481     if (here == ohere) {
1482         return; // It's not left, it's not right.
1483     }
1484 
1485     if (_actSel < 2 || swapping) {
1486         if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) {
1487             _screenWindow->setSelectionStart(ohere.x(), ohere.y(), true);
1488         } else {
1489             _screenWindow->setSelectionStart(ohere.x() - 1 - offset, ohere.y(), false);
1490         }
1491     }
1492 
1493     _actSel = 2; // within selection
1494     _pntSel = here;
1495     _pntSel.ry() += _scrollBar->value();
1496 
1497     if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) {
1498         _screenWindow->setSelectionEnd(here.x(), here.y(), _trimTrailingSpaces);
1499     } else {
1500         _screenWindow->setSelectionEnd(here.x() + offset, here.y(), _trimTrailingSpaces);
1501     }
1502 }
1503 
1504 void TerminalDisplay::mouseReleaseEvent(QMouseEvent *ev)
1505 {
1506     if (_screenWindow.isNull()) {
1507         return;
1508     }
1509 
1510     auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !usesMouseTracking());
1511 
1512     if (ev->button() == Qt::LeftButton) {
1513         if (_dragInfo.state == diPending) {
1514             // We had a drag event pending but never confirmed.  Kill selection
1515             clearSelection();
1516         } else {
1517             if (_actSel > 1) {
1518                 if (_possibleTripleClick) {
1519                     const QString text = _screenWindow->selectedText(currentDecodingOptions());
1520                     if (!text.isEmpty()) {
1521                         _doubleClickSelectedText = text;
1522                         if (_copyTextAsHTML) {
1523                             _doubleClickSelectedHtml = _screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml);
1524                         }
1525                     }
1526                 } else {
1527                     copyToX11Selection();
1528                 }
1529             }
1530 
1531             _actSel = 0;
1532 
1533             // FIXME: emits a release event even if the mouse is
1534             //       outside the range. The procedure used in `mouseMoveEvent'
1535             //       applies here, too.
1536 
1537             if (usesMouseTracking() && !(ev->modifiers() & Qt::ShiftModifier) && !_readOnly) {
1538                 Q_EMIT mouseSignal(mouseButton(0, ev->modifiers()), charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 2);
1539             }
1540         }
1541         _dragInfo.state = diNone;
1542     }
1543 
1544     if (usesMouseTracking() && !_readOnly && (ev->button() == Qt::RightButton || ev->button() == Qt::MiddleButton) && !(ev->modifiers() & Qt::ShiftModifier)) {
1545         Q_EMIT mouseSignal(mouseButton((ev->button() == Qt::MiddleButton ? 1 : 2), ev->modifiers()),
1546                            charColumn + 1,
1547                            charLine + 1 + _scrollBar->value() - _scrollBar->maximum(),
1548                            2);
1549     }
1550 
1551     if (!_screenWindow->screen()->hasSelection()) {
1552         _filterChain->mouseReleaseEvent(this, ev, charLine, charColumn);
1553     }
1554 }
1555 
1556 QPair<int, int> TerminalDisplay::getCharacterPosition(const QPoint &widgetPoint, bool edge) const
1557 {
1558     // the column value returned can be equal to _usedColumns (when edge == true),
1559     // which is the position just after the last character displayed in a line.
1560     //
1561     // this is required so that the user can select characters in the right-most
1562     // column (or left-most for right-to-left input)
1563     const int columnMax = edge ? _usedColumns : _usedColumns - 1;
1564     const int xOffset = edge ? _terminalFont->fontWidth() / 2 : 0;
1565     int line = qBound(0, (widgetPoint.y() - contentsRect().top() - _contentRect.top()) / _terminalFont->fontHeight(), _usedLines - 1);
1566     bool doubleWidth = line < _lineProperties.count() && _lineProperties[line].flags.f.doublewidth;
1567     bool shaped;
1568     int column =
1569         qBound(0, (widgetPoint.x() + xOffset - contentsRect().left() - _contentRect.left()) / _terminalFont->fontWidth() / (doubleWidth ? 2 : 1), columnMax);
1570 
1571     // Visual column to logical
1572     if (_bidiEnabled && column < _usedColumns) {
1573         int log2line[MAX_LINE_WIDTH];
1574         int line2log[MAX_LINE_WIDTH];
1575         uint16_t shapemap[MAX_LINE_WIDTH];
1576         int32_t vis2line[MAX_LINE_WIDTH];
1577         const int pos = loc(0, line);
1578         QString line;
1579         bidiMap(_image + pos, line, log2line, line2log, shapemap, vis2line, shaped, false);
1580         column = line2log[vis2line[column]];
1581     }
1582 
1583     return qMakePair(line, column);
1584 }
1585 
1586 void TerminalDisplay::setExpandedMode(bool expand)
1587 {
1588     _headerBar->setExpandedMode(expand);
1589 }
1590 
1591 void TerminalDisplay::processMidButtonClick(QMouseEvent *ev)
1592 {
1593     if (!usesMouseTracking() || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) {
1594         const bool appendEnter = (ev->modifiers() & Qt::ControlModifier) != 0u;
1595 
1596         // If currently waiting for a triple click, a middle click cancels that - copy now
1597         copyToX11Selection(true);
1598 
1599         if (_middleClickPasteMode == Enum::PasteFromX11Selection) {
1600             pasteFromX11Selection(appendEnter);
1601         } else if (_middleClickPasteMode == Enum::PasteFromClipboard) {
1602             pasteFromClipboard(appendEnter);
1603         } else {
1604             Q_ASSERT(false);
1605         }
1606     } else {
1607         if (!_readOnly) {
1608             auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !usesMouseTracking());
1609             Q_EMIT mouseSignal(mouseButton(1, ev->modifiers()), charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0);
1610         }
1611     }
1612 }
1613 
1614 void TerminalDisplay::mouseDoubleClickEvent(QMouseEvent *ev)
1615 {
1616     // Yes, successive middle click can trigger this event
1617     if (ev->button() == Qt::MiddleButton) {
1618         processMidButtonClick(ev);
1619         return;
1620     }
1621 
1622     if (_screenWindow.isNull()) {
1623         return;
1624     }
1625 
1626     auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !usesMouseTracking());
1627 
1628     QPoint pos(qMin(charColumn, _columns - 1), qMin(charLine, _lines - 1));
1629 
1630     // pass on double click as two clicks.
1631     if (usesMouseTracking() && !(ev->modifiers() & Qt::ShiftModifier)) {
1632         if (!_readOnly) {
1633             // Send just _ONE_ click event, since the first click of the double click
1634             // was already sent by the click handler
1635             Q_EMIT mouseSignal(mouseButton((ev->button() == Qt::LeftButton ? 0 : 2), ev->modifiers()),
1636                                charColumn + 1,
1637                                charLine + 1 + _scrollBar->value() - _scrollBar->maximum(),
1638                                0);
1639         }
1640         return;
1641     }
1642 
1643     if (ev->button() != Qt::LeftButton) {
1644         return;
1645     }
1646 
1647     clearSelection();
1648     _iPntSel = pos;
1649     _iPntSel.ry() += _scrollBar->value();
1650 
1651     _wordSelectionMode = true;
1652     _actSel = 2; // within selection
1653 
1654     // find word boundaries...
1655     {
1656         // find the start of the word
1657         const QPoint bgnSel = findWordStart(pos);
1658         const QPoint endSel = findWordEnd(pos);
1659 
1660         _actSel = 2; // within selection
1661 
1662         _screenWindow->setSelectionStart(bgnSel.x(), bgnSel.y(), false);
1663         _screenWindow->setSelectionEnd(endSel.x(), endSel.y(), _trimTrailingSpaces);
1664     }
1665 
1666     _possibleTripleClick = true;
1667     _tripleClickPos = ev->pos();
1668 
1669     QTimer::singleShot(QApplication::doubleClickInterval(), this, [this]() {
1670         _possibleTripleClick = false;
1671         copyToX11Selection(true); // this will do nothing if another copy happened in the meantime
1672     });
1673 }
1674 
1675 void TerminalDisplay::wheelEvent(QWheelEvent *ev)
1676 {
1677     static QElapsedTimer enable_zoom_timer;
1678     static bool enable_zoom = true;
1679     // Only vertical scrolling is supported
1680     if (qAbs(ev->angleDelta().y()) < qAbs(ev->angleDelta().x())) {
1681         return;
1682     }
1683 
1684     if (enable_zoom_timer.isValid() && enable_zoom_timer.elapsed() > 1000) {
1685         enable_zoom = true;
1686     }
1687 
1688     const int modifiers = ev->modifiers();
1689 
1690     // ctrl+<wheel> for zooming, like in konqueror and firefox
1691     if (((modifiers & Qt::ControlModifier) != 0u) && _mouseWheelZoom && enable_zoom) {
1692         _scrollWheelState.addWheelEvent(ev);
1693 
1694         int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE);
1695         for (; steps > 0; --steps) {
1696             // wheel-up for increasing font size
1697             _terminalFont->increaseFontSize();
1698         }
1699         for (; steps < 0; ++steps) {
1700             // wheel-down for decreasing font size
1701             _terminalFont->decreaseFontSize();
1702         }
1703         return;
1704     } else if (!usesMouseTracking() && (_scrollBar->maximum() > 0)) {
1705         // If the program running in the terminal is not interested in Mouse
1706         // Tracking events, send the event to the scrollbar if the slider
1707         // has room to move
1708 
1709         _scrollWheelState.addWheelEvent(ev);
1710 
1711         _scrollBar->event(ev);
1712 
1713         // Reapply scrollbar position since the scrollbar event handler
1714         // sometimes makes the scrollbar visible when set to hidden.
1715         // Don't call propagateSize and update, since nothing changed.
1716         _scrollBar->applyScrollBarPosition(false);
1717 
1718         Q_ASSERT(_sessionController != nullptr);
1719 
1720         _sessionController->setSearchStartToWindowCurrentLine();
1721         _scrollWheelState.clearAll();
1722     } else if (!_readOnly) {
1723         _scrollWheelState.addWheelEvent(ev);
1724 
1725         Q_ASSERT(!_sessionController->session().isNull());
1726 
1727         if (!usesMouseTracking() && !_sessionController->session()->isPrimaryScreen() && _scrollBar->alternateScrolling()) {
1728             // Send simulated up / down key presses to the terminal program
1729             // for the benefit of programs such as 'less' (which use the alternate screen)
1730 
1731             // assume that each Up / Down key event will cause the terminal application
1732             // to scroll by one line.
1733             //
1734             // to get a reasonable scrolling speed, scroll by one line for every 5 degrees
1735             // of mouse wheel rotation.  Mouse wheels typically move in steps of 15 degrees,
1736             // giving a scroll of 3 lines
1737 
1738             const int lines =
1739                 _scrollWheelState.consumeSteps(static_cast<int>(_terminalFont->fontHeight() * qApp->devicePixelRatio()), ScrollState::degreesToAngle(5));
1740             const int keyCode = lines > 0 ? Qt::Key_Up : Qt::Key_Down;
1741             QKeyEvent keyEvent(QEvent::KeyPress, keyCode, Qt::NoModifier);
1742 
1743             for (int i = 0; i < abs(lines); i++) {
1744                 Q_EMIT keyPressedSignal(&keyEvent);
1745             }
1746         } else if (usesMouseTracking()) {
1747             // terminal program wants notification of mouse activity
1748 
1749             auto [charLine, charColumn] = getCharacterPosition(ev->position().toPoint(), !usesMouseTracking());
1750             const int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE);
1751             const int button = (steps > 0) ? 64 : 65;
1752             for (int i = 0; i < abs(steps); ++i) {
1753                 // Alt+wheel unsupported, Qt transforms it into horizontal wheel, see QTBUG-30948
1754                 Q_EMIT mouseSignal(mouseButton(button, ev->modifiers()), charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0);
1755             }
1756         }
1757     }
1758     enable_zoom_timer.start();
1759     enable_zoom = false;
1760 }
1761 
1762 void TerminalDisplay::viewScrolledByUser()
1763 {
1764     Q_ASSERT(_sessionController != nullptr);
1765     _sessionController->setSearchStartToWindowCurrentLine();
1766 }
1767 
1768 /* Moving left/up from the line containing pnt, return the starting
1769    offset point which the given line is continuously wrapped
1770    (top left corner = 0,0; previous line not visible = 0,-1).
1771 */
1772 QPoint TerminalDisplay::findLineStart(const QPoint &pnt)
1773 {
1774     const int visibleScreenLines = _lineProperties.size();
1775     const int topVisibleLine = _screenWindow->currentLine();
1776     Screen *screen = _screenWindow->screen();
1777     int line = pnt.y();
1778     int lineInHistory = line + topVisibleLine;
1779 
1780     QVector<LineProperty> lineProperties = _lineProperties;
1781 
1782     while (lineInHistory > 0) {
1783         for (; line > 0; line--, lineInHistory--) {
1784             // Does previous line wrap around?
1785             if ((lineProperties[line - 1].flags.f.wrapped) == 0) {
1786                 return {0, lineInHistory - topVisibleLine};
1787             }
1788         }
1789 
1790         if (lineInHistory < 1) {
1791             break;
1792         }
1793 
1794         // _lineProperties is only for the visible screen, so grab new data
1795         int newRegionStart = qMax(0, lineInHistory - visibleScreenLines);
1796         lineProperties = screen->getLineProperties(newRegionStart, lineInHistory - 1);
1797         line = lineInHistory - newRegionStart;
1798     }
1799     return {0, lineInHistory - topVisibleLine};
1800 }
1801 
1802 /* Moving right/down from the line containing pnt, return the ending
1803    offset point which the given line is continuously wrapped.
1804 */
1805 QPoint TerminalDisplay::findLineEnd(const QPoint &pnt)
1806 {
1807     const int visibleScreenLines = _lineProperties.size();
1808     const int topVisibleLine = _screenWindow->currentLine();
1809     const int maxY = _screenWindow->lineCount() - 1;
1810     Screen *screen = _screenWindow->screen();
1811     int line = pnt.y();
1812     int lineInHistory = line + topVisibleLine;
1813 
1814     QVector<LineProperty> lineProperties = _lineProperties;
1815 
1816     while (lineInHistory < maxY) {
1817         for (; line < lineProperties.count() && lineInHistory < maxY; line++, lineInHistory++) {
1818             // Does current line wrap around?
1819             if ((lineProperties[line].flags.f.wrapped) == 0) {
1820                 return {_columns - 1, lineInHistory - topVisibleLine};
1821             }
1822         }
1823 
1824         line = 0;
1825         lineProperties = screen->getLineProperties(lineInHistory, qMin(lineInHistory + visibleScreenLines, maxY));
1826     }
1827     return {_columns - 1, lineInHistory - topVisibleLine};
1828 }
1829 
1830 QPoint TerminalDisplay::findWordStart(const QPoint &pnt)
1831 {
1832     // Don't ask me why x and y are switched ¯\_(ツ)_/¯
1833     QSharedPointer<HotSpot> hotspot = _filterChain->hotSpotAt(pnt.y(), pnt.x());
1834     if (hotspot) {
1835         return QPoint(hotspot->startColumn(), hotspot->startLine());
1836     }
1837 
1838     const int regSize = qMax(_screenWindow->windowLines(), 10);
1839     const int firstVisibleLine = _screenWindow->currentLine();
1840 
1841     Screen *screen = _screenWindow->screen();
1842     Character *image = _image;
1843     Character *tmp_image = nullptr;
1844 
1845     int imgLine = pnt.y();
1846     int x = pnt.x();
1847     int y = imgLine + firstVisibleLine;
1848     int imgLoc = loc(x, imgLine);
1849     QVector<LineProperty> lineProperties = _lineProperties;
1850     const QChar selClass = charClass(image[imgLoc]);
1851     const int imageSize = regSize * _columns;
1852 
1853     while (true) {
1854         for (;; imgLoc--, x--) {
1855             if (imgLoc < 1) {
1856                 // no more chars in this region
1857                 break;
1858             }
1859             if (x > 0) {
1860                 // has previous char on this line
1861                 if (charClass(image[imgLoc - 1]) == selClass) {
1862                     continue;
1863                 }
1864                 goto out;
1865             } else if (imgLine > 0) {
1866                 // not the first line in the session
1867                 if ((lineProperties[imgLine - 1].flags.f.wrapped) != 0) {
1868                     // have continuation on prev line
1869                     if (charClass(image[imgLoc - 1]) == selClass) {
1870                         x = _columns;
1871                         imgLine--;
1872                         y--;
1873                         continue;
1874                     }
1875                 }
1876                 goto out;
1877             } else if (y > 0) {
1878                 // want more data, but need to fetch new region
1879                 break;
1880             } else {
1881                 goto out;
1882             }
1883         }
1884         if (y <= 0) {
1885             // No more data
1886             goto out;
1887         }
1888         int newRegStart = qMax(0, y - regSize + 1);
1889         lineProperties = screen->getLineProperties(newRegStart, y - 1);
1890         imgLine = y - newRegStart;
1891 
1892         delete[] tmp_image;
1893         tmp_image = new Character[imageSize];
1894         image = tmp_image;
1895 
1896         screen->getImage(tmp_image, imageSize, newRegStart, y - 1);
1897         imgLoc = loc(x, imgLine);
1898         if (imgLoc < 1) {
1899             // Reached the start of the session
1900             break;
1901         }
1902     }
1903 out:
1904     delete[] tmp_image;
1905     return {x, y - firstVisibleLine};
1906 }
1907 
1908 QPoint TerminalDisplay::findWordEnd(const QPoint &pnt)
1909 {
1910     const int maxY = _screenWindow->lineCount() - 1;
1911     const int maxX = _columns - 1;
1912 
1913     QSharedPointer<HotSpot> hotspot = _filterChain->hotSpotAt(pnt.y(), pnt.x());
1914     if (hotspot) {
1915         int line = hotspot->endLine();
1916         int col = hotspot->endColumn();
1917 
1918         // Because of how filters work with end of line, we need this hack.
1919         // It really should be fixed in filters, but this is the best we have until then
1920         if (col > 0) {
1921             col--;
1922         } else {
1923             col = maxX;
1924             line--;
1925         }
1926         return {qBound(0, col, maxX), qBound(0, line, maxY)};
1927     }
1928 
1929     const int regSize = qMax(_screenWindow->windowLines(), 10);
1930     const int curLine = _screenWindow->currentLine();
1931     int i = pnt.y();
1932     int x = pnt.x();
1933     int y = i + curLine;
1934     int j = loc(x, i);
1935     QVector<LineProperty> lineProperties = _lineProperties;
1936     Screen *screen = _screenWindow->screen();
1937     Character *image = _image;
1938     Character *tmp_image = nullptr;
1939     const QChar selClass = charClass(image[j]);
1940     const int imageSize = regSize * _columns;
1941 
1942     while (true) {
1943         const int lineCount = lineProperties.count();
1944         for (;; j++, x++) {
1945             if (x < maxX) {
1946                 if (charClass(image[j + 1]) == selClass &&
1947                     // A colon right before whitespace is never part of a word
1948                     !(image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) {
1949                     continue;
1950                 }
1951                 goto out;
1952             } else if (i < lineCount - 1) {
1953                 if (((lineProperties[i].flags.f.wrapped) != 0) && charClass(image[j + 1]) == selClass &&
1954                     // A colon right before whitespace is never part of a word
1955                     !(image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) {
1956                     x = -1;
1957                     i++;
1958                     y++;
1959                     continue;
1960                 }
1961                 goto out;
1962             } else if (y < maxY) {
1963                 if (i < lineCount && ((lineProperties[i].flags.f.wrapped) == 0)) {
1964                     goto out;
1965                 }
1966                 break;
1967             } else {
1968                 goto out;
1969             }
1970         }
1971         int newRegEnd = qMin(y + regSize - 1, maxY);
1972         lineProperties = screen->getLineProperties(y, newRegEnd);
1973         i = 0;
1974         if (tmp_image == nullptr) {
1975             tmp_image = new Character[imageSize];
1976             image = tmp_image;
1977         }
1978         screen->getImage(tmp_image, imageSize, y, newRegEnd);
1979         x--;
1980         j = loc(x, i);
1981     }
1982 out:
1983     y -= curLine;
1984     // In word selection mode don't select @ (64) if at end of word.
1985     if ((image[j].rendition.f.extended == 0) && (QChar(image[j].character) == QLatin1Char('@')) && (y > pnt.y() || x > pnt.x())) {
1986         if (x > 0) {
1987             x--;
1988         } else {
1989             y--;
1990         }
1991     }
1992     delete[] tmp_image;
1993 
1994     return {x, y};
1995 }
1996 
1997 bool TerminalDisplay::isInTerminalRegion(const QPoint &point) const
1998 {
1999     // clang-format off
2000     const bool inMessageSuspendedWidget = _outputSuspendedMessageWidget &&
2001                                           _outputSuspendedMessageWidget->isVisible() &&
2002                                           _outputSuspendedMessageWidget->frameGeometry().contains(point);
2003     // clang-format on
2004     return !(!visibleRegion().contains(point) || _scrollBar->frameGeometry().contains(point) || inMessageSuspendedWidget);
2005 }
2006 
2007 Screen::DecodingOptions TerminalDisplay::currentDecodingOptions()
2008 {
2009     Screen::DecodingOptions decodingOptions;
2010     if (_preserveLineBreaks) {
2011         decodingOptions |= Screen::PreserveLineBreaks;
2012     }
2013     if (_trimLeadingSpaces) {
2014         decodingOptions |= Screen::TrimLeadingWhitespace;
2015     }
2016     if (_trimTrailingSpaces) {
2017         decodingOptions |= Screen::TrimTrailingWhitespace;
2018     }
2019 
2020     return decodingOptions;
2021 }
2022 
2023 void TerminalDisplay::notificationClicked(const QString &xdgActivationToken)
2024 {
2025     Q_EMIT activationRequest(xdgActivationToken);
2026 }
2027 
2028 void TerminalDisplay::mouseTripleClickEvent(QMouseEvent *ev)
2029 {
2030     if (_screenWindow.isNull()) {
2031         return;
2032     }
2033 
2034     auto [charLine, charColumn] = getCharacterPosition(ev->pos(), true);
2035     if (_screenWindow->screen()->hasRepl() && ev->modifiers() & Qt::ControlModifier) {
2036         _screenWindow->screen()->selectReplContigious(charColumn, charLine + _screenWindow->currentLine());
2037         copyToX11Selection();
2038     } else {
2039         selectLine(QPoint(charColumn, charLine), _tripleClickMode == Enum::SelectWholeLine, true);
2040     }
2041 }
2042 
2043 void TerminalDisplay::removeLines(int lines)
2044 {
2045     _iPntSel.ry() -= lines;
2046     _pntSel.ry() -= lines;
2047     _tripleSelBegin.ry() -= lines;
2048 }
2049 
2050 void TerminalDisplay::selectLine(QPoint pos, bool entireLine, bool fromTripleClick)
2051 {
2052     _iPntSel = pos;
2053 
2054     clearSelection();
2055 
2056     _lineSelectionMode = true;
2057     _wordSelectionMode = false;
2058 
2059     _actSel = 2; // within selection
2060 
2061     if (!entireLine) { // Select from cursor to end of line
2062         _tripleSelBegin = findWordStart(_iPntSel);
2063         _screenWindow->setSelectionStart(_tripleSelBegin.x(), _tripleSelBegin.y(), false);
2064     } else {
2065         _tripleSelBegin = findLineStart(_iPntSel);
2066         _screenWindow->setSelectionStart(0, _tripleSelBegin.y(), false);
2067     }
2068 
2069     _iPntSel = findLineEnd(_iPntSel);
2070     _screenWindow->setSelectionEnd(_iPntSel.x(), _iPntSel.y(), _trimTrailingSpaces);
2071 
2072     if (!fromTripleClick) {
2073         copyToX11Selection();
2074     }
2075 
2076     _iPntSel.ry() += _scrollBar->value();
2077 }
2078 
2079 void TerminalDisplay::selectCurrentLine()
2080 {
2081     if (_screenWindow.isNull()) {
2082         return;
2083     }
2084 
2085     selectLine(cursorPosition(), true);
2086 }
2087 
2088 void TerminalDisplay::selectAll()
2089 {
2090     if (_screenWindow.isNull()) {
2091         return;
2092     }
2093 
2094     _preserveLineBreaks = true;
2095     _screenWindow->setSelectionByLineRange(0, _screenWindow->lineCount());
2096     copyToX11Selection();
2097 }
2098 
2099 bool TerminalDisplay::focusNextPrevChild(bool next)
2100 {
2101     // for 'Tab', always disable focus switching among widgets
2102     // for 'Shift+Tab', leave the decision to higher level
2103     if (next) {
2104         return false;
2105     } else {
2106         return QWidget::focusNextPrevChild(next);
2107     }
2108 }
2109 
2110 QChar TerminalDisplay::charClass(const Character &ch) const
2111 {
2112     if (ch.rendition.f.extended != 0) {
2113         ushort extendedCharLength = 0;
2114         const char32_t *chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength);
2115         if ((chars != nullptr) && extendedCharLength > 0) {
2116             const QString s = QString::fromUcs4(chars, extendedCharLength);
2117             if (_wordCharacters.contains(s, Qt::CaseInsensitive)) {
2118                 return QLatin1Char('a');
2119             }
2120             bool letterOrNumber = false;
2121             for (int i = 0; !letterOrNumber && i < s.size(); ++i) {
2122                 letterOrNumber = s.at(i).isLetterOrNumber();
2123             }
2124             return letterOrNumber ? QLatin1Char('a') : s.at(0);
2125         }
2126         return QChar{0};
2127     } else {
2128         const QChar qch(ch.character);
2129         if (qch.isSpace()) {
2130             return QLatin1Char(' ');
2131         }
2132 
2133         if (qch.isLetterOrNumber() || _wordCharacters.contains(qch, Qt::CaseInsensitive)) {
2134             return QLatin1Char('a');
2135         }
2136 
2137         return qch;
2138     }
2139 }
2140 
2141 void TerminalDisplay::setWordCharacters(const QString &wc)
2142 {
2143     _wordCharacters = wc;
2144 }
2145 
2146 void TerminalDisplay::setUsesMouseTracking(bool on)
2147 {
2148     _usesMouseTracking = on;
2149     resetCursor();
2150 }
2151 
2152 void TerminalDisplay::setAllowMouseTracking(bool allow)
2153 {
2154     _allowMouseTracking = allow;
2155     resetCursor();
2156 }
2157 
2158 bool TerminalDisplay::allowsMouseTracking() const
2159 {
2160     return _allowMouseTracking;
2161 }
2162 
2163 void TerminalDisplay::resetCursor()
2164 {
2165     setCursor(usesMouseTracking() ? Qt::ArrowCursor : Qt::IBeamCursor);
2166 }
2167 
2168 bool TerminalDisplay::usesMouseTracking() const
2169 {
2170     return _usesMouseTracking && _allowMouseTracking;
2171 }
2172 
2173 void TerminalDisplay::setBracketedPasteMode(bool on)
2174 {
2175     _bracketedPasteMode = on;
2176 }
2177 
2178 bool TerminalDisplay::bracketedPasteMode() const
2179 {
2180     return _bracketedPasteMode;
2181 }
2182 
2183 /* ------------------------------------------------------------------------- */
2184 /*                                                                           */
2185 /*                               Touch & Scroll                              */
2186 /*                                                                           */
2187 /* ------------------------------------------------------------------------- */
2188 
2189 void TerminalDisplay::scrollPrepareEvent(QScrollPrepareEvent *event)
2190 {
2191     // Ignore scroller events that were triggered in regions that we
2192     // expect to handle the input different (e.g. the find dialog)
2193     if (!isInTerminalRegion(event->startPos().toPoint())) {
2194         return;
2195     }
2196 
2197     const int lineHeight = _terminalFont->fontHeight() + _terminalFont->lineSpacing();
2198     // clang-format off
2199     const QRect scrollableRegion = imageToWidget(QRect(
2200         // Allow a line of overscroll in either direction: We'll be rounding the
2201         // values QScroller gives us and still want to be able to scroll to every line.
2202         0, 0,
2203         0, _screenWindow->lineCount() + 1));
2204     // clang-format on
2205 
2206     // Give Qt the viewport and content window size
2207     event->setViewportSize(contentsRect().size());
2208     event->setContentPosRange(scrollableRegion);
2209     event->setContentPos(QPointF(0.0, _screenWindow->currentLine() * lineHeight));
2210 
2211     event->accept();
2212 }
2213 
2214 void TerminalDisplay::scrollEvent(QScrollEvent *event)
2215 {
2216     const int lineHeight = _terminalFont->fontHeight() + _terminalFont->lineSpacing();
2217     const int targetLine = int(event->contentPos().y() / lineHeight);
2218     const int linesScrolled = targetLine - _screenWindow->currentLine();
2219 
2220     if (linesScrolled != 0) {
2221         scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, linesScrolled);
2222     }
2223 
2224     event->accept();
2225 }
2226 
2227 /* ------------------------------------------------------------------------- */
2228 /*                                                                           */
2229 /*                               Clipboard                                   */
2230 /*                                                                           */
2231 /* ------------------------------------------------------------------------- */
2232 
2233 void TerminalDisplay::doPaste(QString text, bool appendReturn)
2234 {
2235     if (_screenWindow.isNull()) {
2236         return;
2237     }
2238 
2239     if (_readOnly) {
2240         return;
2241     }
2242 
2243     if (appendReturn) {
2244         text.append(QLatin1String("\r"));
2245     }
2246 
2247     if (text.length() > 8000) {
2248         if (KMessageBox::warningContinueCancel(
2249                 window(),
2250                 i18np("Are you sure you want to paste %1 character?", "Are you sure you want to paste %1 characters?", text.length()),
2251                 i18n("Confirm Paste"),
2252                 KStandardGuiItem::cont(),
2253                 KStandardGuiItem::cancel(),
2254                 QStringLiteral("ShowPasteHugeTextWarning"))
2255             == KMessageBox::Cancel) {
2256             return;
2257         }
2258     }
2259 
2260     // Most code in Konsole uses UTF-32. We're filtering
2261     // UTF-16 here, as all control characters can be represented
2262     // in this encoding as single code unit. If you ever need to
2263     // filter anything above 0xFFFF (specific code points or
2264     // categories which contain such code points), convert text to
2265     // UTF-32 using QString::toUcs4() and use QChar static
2266     // methods which take "uint ucs4".
2267     static const QVector<ushort> whitelist = {u'\t', u'\r', u'\n'};
2268     static const auto isUnsafe = [](const QChar &c) {
2269         return (c.category() == QChar::Category::Other_Control && !whitelist.contains(c.unicode()));
2270     };
2271     // Returns control sequence string (e.g. "^C") for control character c
2272     static const auto charToSequence = [](const QChar &c) {
2273         if (c.unicode() <= 0x1F) {
2274             return QStringLiteral("^%1").arg(QChar(u'@' + c.unicode()));
2275         } else if (c.unicode() == 0x7F) {
2276             return QStringLiteral("^?");
2277         } else if (c.unicode() >= 0x80 && c.unicode() <= 0x9F) {
2278             return QStringLiteral("^[%1").arg(QChar(u'@' + c.unicode() - 0x80));
2279         }
2280         return QString();
2281     };
2282 
2283     const QMap<ushort, QString> characterDescriptions = {
2284         {0x0003, i18n("End Of Text/Interrupt: may exit the current process")},
2285         {0x0004, i18n("End Of Transmission: may exit the current process")},
2286         {0x0007, i18n("Bell: will try to emit an audible warning")},
2287         {0x0008, i18n("Backspace")},
2288         {0x0013, i18n("Device Control Three/XOFF: suspends output")},
2289         {0x001a, i18n("Substitute/Suspend: may suspend current process")},
2290         {0x001b, i18n("Escape: used for manipulating terminal state")},
2291         {0x001c, i18n("File Separator/Quit: may abort the current process")},
2292     };
2293 
2294     QStringList unsafeCharacters;
2295     for (const QChar &c : text) {
2296         if (isUnsafe(c)) {
2297             const QString sequence = charToSequence(c);
2298             const QString description = characterDescriptions.value(c.unicode(), QString());
2299             QString entry = QStringLiteral("U+%1").arg(c.unicode(), 4, 16, QLatin1Char('0'));
2300             if (!sequence.isEmpty()) {
2301                 entry += QStringLiteral("\t%1").arg(sequence);
2302             }
2303             if (!description.isEmpty()) {
2304                 entry += QStringLiteral("\t%1").arg(description);
2305             }
2306             unsafeCharacters.append(entry);
2307         }
2308     }
2309     unsafeCharacters.removeDuplicates();
2310 
2311     if (!unsafeCharacters.isEmpty()) {
2312         int result =
2313             KMessageBox::warningTwoActionsCancelList(window(),
2314                                                      i18n("The text you're trying to paste contains hidden control characters, "
2315                                                           "do you want to filter them out?"),
2316                                                      unsafeCharacters,
2317                                                      i18nc("@title", "Confirm Paste"),
2318                                                      KGuiItem(i18nc("@action:button", "Paste &without control characters"), QStringLiteral("filter-symbolic")),
2319                                                      KGuiItem(i18nc("@action:button", "&Paste everything"), QStringLiteral("edit-paste")),
2320                                                      KGuiItem(i18nc("@action:button", "&Cancel"), QStringLiteral("dialog-cancel")),
2321                                                      QStringLiteral("ShowPasteUnprintableWarning"));
2322         switch (result) {
2323         case KMessageBox::Cancel:
2324             return;
2325         case KMessageBox::PrimaryAction: {
2326             QString sanitized;
2327             for (const QChar &c : text) {
2328                 if (!isUnsafe(c)) {
2329                     sanitized.append(c);
2330                 }
2331             }
2332             text = sanitized;
2333         }
2334         case KMessageBox::SecondaryAction:
2335             break;
2336         default:
2337             break;
2338         }
2339     }
2340 
2341     if (!text.isEmpty()) {
2342         // replace CRLF with CR first, fixes issues with pasting multiline
2343         // text from gtk apps (e.g. Firefox), bug 421480
2344         text.replace(QLatin1String("\r\n"), QLatin1String("\r"));
2345 
2346         text.replace(QLatin1Char('\n'), QLatin1Char('\r'));
2347         if (bracketedPasteMode()) {
2348             text.remove(QLatin1String("\033"));
2349             text.prepend(QLatin1String("\033[200~"));
2350             text.append(QLatin1String("\033[201~"));
2351         }
2352         // perform paste by simulating keypress events
2353         QKeyEvent e(QEvent::KeyPress, 0, Qt::NoModifier, text);
2354         Q_EMIT keyPressedSignal(&e);
2355     }
2356 }
2357 
2358 void TerminalDisplay::setAutoCopySelectedText(bool enabled)
2359 {
2360     _autoCopySelectedText = enabled;
2361 }
2362 
2363 void TerminalDisplay::setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode)
2364 {
2365     _middleClickPasteMode = mode;
2366 }
2367 
2368 void TerminalDisplay::setCopyTextAsHTML(bool enabled)
2369 {
2370     _copyTextAsHTML = enabled;
2371 }
2372 
2373 void TerminalDisplay::copyToX11Selection(bool useSavedText)
2374 {
2375     if (_screenWindow.isNull()) {
2376         return;
2377     }
2378 
2379     QString text;
2380     QString html;
2381     if (useSavedText) {
2382         text = _doubleClickSelectedText;
2383         html = _doubleClickSelectedHtml;
2384     } else {
2385         text = _screenWindow->selectedText(currentDecodingOptions());
2386         if (!text.isEmpty() && _copyTextAsHTML) {
2387             html = _screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml);
2388         }
2389     }
2390 
2391     if (text.isEmpty()) {
2392         return;
2393     }
2394 
2395     // Copying the double-click selection *does* double click select + copy.
2396     // Copying another selection *cancels* double-click select + copy.
2397     // In both cases, no double-click copy is pending anymore.
2398     _doubleClickSelectedText.clear();
2399     _doubleClickSelectedHtml.clear();
2400 
2401     QMimeData *mimeData = new QMimeData;
2402     mimeData->setText(text);
2403     if (!html.isEmpty()) {
2404         mimeData->setHtml(html);
2405     }
2406 
2407     if (QApplication::clipboard()->supportsSelection()) {
2408         QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection);
2409     }
2410 
2411     if (_autoCopySelectedText) {
2412         QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
2413     }
2414 }
2415 
2416 void TerminalDisplay::copyToClipboard(Screen::DecodingOptions options)
2417 {
2418     if (_screenWindow.isNull()) {
2419         return;
2420     }
2421 
2422     const QString &text = _screenWindow->selectedText(currentDecodingOptions() | options);
2423     if (text.isEmpty()) {
2424         return;
2425     }
2426 
2427     auto mimeData = new QMimeData;
2428     mimeData->setText(text);
2429 
2430     if (_copyTextAsHTML) {
2431         mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml));
2432     }
2433 
2434     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
2435 }
2436 
2437 void TerminalDisplay::pasteFromClipboard(bool appendEnter)
2438 {
2439     QString text;
2440     const QMimeData *mimeData = QApplication::clipboard()->mimeData(QClipboard::Clipboard);
2441     if (mimeData == nullptr) {
2442         return;
2443     }
2444 
2445     // When pasting urls of local files:
2446     // - remove the scheme part, "file://"
2447     // - paste the path(s) as a space-separated list of strings, which are quoted if needed
2448     if (!mimeData->hasUrls()) { // fast path if there are no urls
2449         text = mimeData->text();
2450     } else { // handle local file urls
2451         const QList<QUrl> list = mimeData->urls();
2452         for (const QUrl &url : list) {
2453             if (url.isLocalFile()) {
2454                 text += KShell::quoteArg(url.toLocalFile());
2455                 text += QLatin1Char(' ');
2456             } else { // can users copy urls of both local and remote files at the same time?
2457                 text = mimeData->text();
2458                 break;
2459             }
2460         }
2461     }
2462 
2463     doPaste(text, appendEnter);
2464 }
2465 
2466 void TerminalDisplay::pasteFromX11Selection(bool appendEnter)
2467 {
2468     if (QApplication::clipboard()->supportsSelection()) {
2469         QString text = QApplication::clipboard()->text(QClipboard::Selection);
2470         doPaste(text, appendEnter);
2471     }
2472 }
2473 
2474 /* ------------------------------------------------------------------------- */
2475 /*                                                                           */
2476 /*                                Input Method                               */
2477 /*                                                                           */
2478 /* ------------------------------------------------------------------------- */
2479 
2480 void TerminalDisplay::inputMethodEvent(QInputMethodEvent *event)
2481 {
2482     if (!event->commitString().isEmpty()) {
2483         QKeyEvent keyEvent(QEvent::KeyPress, 0, Qt::NoModifier, event->commitString());
2484         Q_EMIT keyPressedSignal(&keyEvent);
2485     }
2486 
2487     if (!_readOnly && isCursorOnDisplay()) {
2488         _inputMethodData.preeditString = event->preeditString();
2489         update(preeditRect() | _inputMethodData.previousPreeditRect);
2490     }
2491     event->accept();
2492 }
2493 
2494 QVariant TerminalDisplay::inputMethodQuery(Qt::InputMethodQuery query) const
2495 {
2496     const QPoint cursorPos = cursorPosition();
2497     switch (query) {
2498     case Qt::ImCursorRectangle:
2499         return imageToWidget(QRect(cursorPos.x(), cursorPos.y(), 1, 1));
2500     case Qt::ImFont:
2501         return font();
2502     default:
2503         break;
2504     }
2505 
2506     return QWidget::inputMethodQuery(query);
2507 }
2508 
2509 QRect TerminalDisplay::preeditRect() const
2510 {
2511     const int preeditLength = Character::stringWidth(_inputMethodData.preeditString);
2512 
2513     if (preeditLength == 0) {
2514         return {};
2515     }
2516     const QRect stringRect(_contentRect.left() + _terminalFont->fontWidth() * cursorPosition().x(),
2517                            _contentRect.top() + _terminalFont->fontHeight() * cursorPosition().y(),
2518                            _terminalFont->fontWidth() * preeditLength,
2519                            _terminalFont->fontHeight());
2520 
2521     return stringRect.intersected(_contentRect);
2522 }
2523 
2524 /* ------------------------------------------------------------------------- */
2525 /*                                                                           */
2526 /*                                Keyboard                                   */
2527 /*                                                                           */
2528 /* ------------------------------------------------------------------------- */
2529 
2530 void TerminalDisplay::setFlowControlWarningEnabled(bool enable)
2531 {
2532     _flowControlWarningEnabled = enable;
2533 
2534     // if the dialog is currently visible and the flow control warning has
2535     // been disabled then hide the dialog
2536     if (!enable) {
2537         outputSuspended(false);
2538     }
2539 }
2540 
2541 void TerminalDisplay::outputSuspended(bool suspended)
2542 {
2543     // create the label when this function is first called
2544     if (_outputSuspendedMessageWidget == nullptr) {
2545         // This label includes a link to an English language website
2546         // describing the 'flow control' (Xon/Xoff) feature found in almost
2547         // all terminal emulators.
2548         // If there isn't a suitable article available in the target language the link
2549         // can simply be removed.
2550         _outputSuspendedMessageWidget =
2551             createMessageWidget(i18n("<qt>Output has been "
2552                                      "<a href=\"https://en.wikipedia.org/wiki/Software_flow_control\">suspended</a>"
2553                                      " by pressing Ctrl+S."
2554                                      " Press <b>Ctrl+Q</b> to resume.</qt>"));
2555 
2556         connect(_outputSuspendedMessageWidget, &KMessageWidget::linkActivated, this, [](const QString &url) {
2557             QDesktopServices::openUrl(QUrl(url));
2558         });
2559 
2560         _outputSuspendedMessageWidget->setMessageType(KMessageWidget::Warning);
2561     }
2562 
2563     suspended ? _outputSuspendedMessageWidget->animatedShow() : _outputSuspendedMessageWidget->animatedHide();
2564 }
2565 
2566 KMessageWidget *TerminalDisplay::createMessageWidget(const QString &text)
2567 {
2568     auto *widget = new KMessageWidget(text, this);
2569     widget->setWordWrap(true);
2570     widget->setFocusProxy(this);
2571     widget->setCursor(Qt::ArrowCursor);
2572 
2573     _verticalLayout->insertWidget(1, widget);
2574 
2575     _searchBar->raise();
2576 
2577     return widget;
2578 }
2579 
2580 void TerminalDisplay::setSelectMode(bool mode)
2581 {
2582     Screen *screen = screenWindow()->screen();
2583     _selectMode = mode;
2584     if (mode) {
2585         screen->initSelCursor();
2586         screen->clearSelection();
2587         screen->setMode(MODE_SelectCursor);
2588         _actSel = 0;
2589         _selModeModifiers = 0;
2590         _selModeByModifiers = false;
2591     } else {
2592         screen->resetMode(MODE_SelectCursor);
2593     }
2594     screenWindow()->notifyOutputChanged();
2595 }
2596 
2597 void TerminalDisplay::updateReadOnlyState(bool readonly)
2598 {
2599     if (_readOnly == readonly) {
2600         return;
2601     }
2602 
2603     if (readonly) {
2604         // Lazy create the readonly messagewidget
2605         if (_readOnlyMessageWidget == nullptr) {
2606             _readOnlyMessageWidget = createMessageWidget(i18n("This terminal is read-only."));
2607             _readOnlyMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("object-locked")));
2608         }
2609     }
2610 
2611     if (_readOnlyMessageWidget != nullptr) {
2612         readonly ? _readOnlyMessageWidget->animatedShow() : _readOnlyMessageWidget->animatedHide();
2613     }
2614 
2615     _readOnly = readonly;
2616 }
2617 
2618 #define SELECT_BY_MODIFIERS                                                                                                                                    \
2619     if (startSelect) {                                                                                                                                         \
2620         clearSelection();                                                                                                                                      \
2621         _actSel = 2;                                                                                                                                           \
2622         screen->selSetSelectionStart(false);                                                                                                                   \
2623         _selModeByModifiers = true;                                                                                                                            \
2624     }
2625 
2626 void TerminalDisplay::keyPressEvent(QKeyEvent *event)
2627 {
2628     Screen *screen = screenWindow()->screen();
2629     int histLines = screen->getHistLines();
2630     bool moved = true;
2631     if (session()->getSelectMode()) {
2632         int y;
2633         bool startSelect = false;
2634         int modifiers = event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier);
2635         if (_selModeModifiers != modifiers) {
2636             if (modifiers == 0) {
2637                 if (_selModeByModifiers) {
2638                     _actSel = 0;
2639                     _selModeModifiers = 0;
2640                     _selModeByModifiers = false;
2641                 }
2642             } else {
2643                 if (event->key() >= Qt::Key_Home && event->key() <= Qt::Key_PageDown) {
2644                     startSelect = true;
2645                     _selModeModifiers = modifiers;
2646                 }
2647             }
2648         }
2649         switch (event->key()) {
2650         case Qt::Key_Escape:
2651             sessionController()->setSelectMode(false);
2652             break;
2653         case Qt::Key_Left:
2654         case Qt::Key_H:
2655             SELECT_BY_MODIFIERS
2656             y = screen->selCursorLeft(1);
2657             if (histLines + y < screenWindow()->currentLine()) {
2658                 scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine());
2659             }
2660             break;
2661         case Qt::Key_Up:
2662         case Qt::Key_K:
2663             SELECT_BY_MODIFIERS
2664             y = screen->selCursorUp(1);
2665             if (histLines + y < screenWindow()->currentLine()) {
2666                 scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine());
2667             }
2668             break;
2669         case Qt::Key_Right:
2670         case Qt::Key_L:
2671             SELECT_BY_MODIFIERS
2672             y = screen->selCursorRight(1);
2673             if (histLines + y >= screenWindow()->currentLine() + screen->getLines()) {
2674                 scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine() - screen->getLines() + 1);
2675             }
2676             break;
2677         case Qt::Key_Down:
2678         case Qt::Key_J:
2679             SELECT_BY_MODIFIERS
2680             y = screen->selCursorDown(1);
2681             if (histLines + y >= screenWindow()->currentLine() + screen->getLines()) {
2682                 scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine() - screen->getLines() + 1);
2683             }
2684             break;
2685         case Qt::Key_Home:
2686             SELECT_BY_MODIFIERS
2687             screen->selCursorLeft(0);
2688             break;
2689         case Qt::Key_End:
2690             SELECT_BY_MODIFIERS
2691             screen->selCursorRight(0);
2692             break;
2693         case Qt::Key_V:
2694             if (_actSel == 0 || _selModeByModifiers) {
2695                 clearSelection();
2696                 _actSel = 2;
2697                 _lineSelectionMode = event->text() == QStringLiteral("V");
2698                 screen->selSetSelectionStart(_lineSelectionMode);
2699                 _selModeByModifiers = 0;
2700             } else {
2701                 _actSel = 0;
2702             }
2703             break;
2704         case Qt::Key_PageUp:
2705             SELECT_BY_MODIFIERS
2706             y = screen->selCursorUp(_scrollBar->scrollFullPage() ? -1 : 0);
2707             if (histLines + y < screenWindow()->currentLine()) {
2708                 scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine());
2709             }
2710             break;
2711         case Qt::Key_PageDown:
2712             SELECT_BY_MODIFIERS
2713             y = screen->selCursorDown(_scrollBar->scrollFullPage() ? -1 : 0);
2714             if (histLines + y >= screenWindow()->currentLine() + screen->getLines()) {
2715                 scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine() - screen->getLines() + 1);
2716             }
2717             break;
2718         case Qt::Key_F:
2719         case Qt::Key_D:
2720             if (event->modifiers() & Qt::ControlModifier) {
2721                 y = screen->selCursorDown(-(event->key() == Qt::Key_F));
2722                 if (histLines + y >= screenWindow()->currentLine() + screen->getLines()) {
2723                     scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine() - screen->getLines() + 1);
2724                 }
2725             } else {
2726                 moved = false;
2727             }
2728             break;
2729         case Qt::Key_B:
2730         case Qt::Key_U:
2731             if (event->modifiers() & Qt::ControlModifier) {
2732                 y = screen->selCursorUp(-(event->key() == Qt::Key_B));
2733                 if (histLines + y < screenWindow()->currentLine()) {
2734                     scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine());
2735                 }
2736             } else {
2737                 moved = false;
2738             }
2739             break;
2740         case Qt::Key_G:
2741             if (event->text() == QStringLiteral("G")) {
2742                 y = screen->selCursorDown(-2);
2743                 screen->selCursorRight(0);
2744                 if (histLines + y >= screenWindow()->currentLine() + screen->getLines()) {
2745                     scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine() - screen->getLines() + 1);
2746                 }
2747             } else {
2748                 y = screen->selCursorUp(-2);
2749                 screen->selCursorLeft(0);
2750                 if (histLines + y < screenWindow()->currentLine()) {
2751                     scrollScreenWindow(ScreenWindow::RelativeScrollMode::ScrollLines, histLines + y - screenWindow()->currentLine());
2752                 }
2753             }
2754             break;
2755         default:
2756             moved = false;
2757             break;
2758         }
2759         if (event->text() == QStringLiteral("^")) {
2760             // Might be on different key(), depending on keyboard layout
2761             screen->selCursorLeft(0);
2762             moved = true;
2763         } else if (event->text() == QStringLiteral("$")) {
2764             // Might be on different key(), depending on keyboard layout
2765             screen->selCursorRight(0);
2766             moved = true;
2767         }
2768         if (moved && _actSel > 0) {
2769             screen->selSetSelectionEnd(_lineSelectionMode);
2770         }
2771         screenWindow()->notifyOutputChanged();
2772         return;
2773     }
2774     {
2775         auto [charLine, charColumn] = getCharacterPosition(mapFromGlobal(QCursor::pos()), !usesMouseTracking());
2776 
2777         // Don't process it if the filterchain handled it for us
2778         if (_filterChain->keyPressEvent(this, event, charLine, charColumn)) {
2779             return;
2780         }
2781     }
2782 
2783     if (!_peekPrimaryShortcut.isEmpty() && _peekPrimaryShortcut.matches(QKeySequence(event->key() | event->modifiers()))) {
2784         peekPrimaryRequested(true);
2785     }
2786 
2787 #ifdef Q_OS_MACOS // swap Ctrl and Meta
2788     if (event->modifiers() & Qt::MetaModifier) {
2789         event->setModifiers((event->modifiers() & ~Qt::MetaModifier) | Qt::ControlModifier);
2790     } else if (event->modifiers() & Qt::ControlModifier) {
2791         event->setModifiers((event->modifiers() & ~Qt::ControlModifier) | Qt::MetaModifier);
2792     }
2793 #endif
2794 
2795     if (!_readOnly) {
2796         if (!_possibleTripleClick) {
2797             _actSel = 0; // Key stroke implies a screen update, so TerminalDisplay won't
2798                          // know where the current selection is.
2799         }
2800 
2801         if (_allowBlinkingCursor) {
2802             _blinkCursorTimer->start();
2803             if (_cursorBlinking) {
2804                 // if cursor is blinking(hidden), blink it again to show it
2805                 blinkCursorEvent();
2806             }
2807             Q_ASSERT(!_cursorBlinking);
2808         }
2809     }
2810 
2811     Q_EMIT keyPressedSignal(event);
2812 
2813 #ifndef QT_NO_ACCESSIBILITY
2814     if (!_readOnly) {
2815         QAccessibleTextCursorEvent textCursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX());
2816         QAccessible::updateAccessibility(&textCursorEvent);
2817     }
2818 #endif
2819 
2820     event->accept();
2821 }
2822 
2823 void TerminalDisplay::keyReleaseEvent(QKeyEvent *event)
2824 {
2825     if (_readOnly) {
2826         event->accept();
2827         return;
2828     }
2829 
2830     {
2831         auto [charLine, charColumn] = getCharacterPosition(mapFromGlobal(QCursor::pos()), !usesMouseTracking());
2832         _filterChain->keyReleaseEvent(this, event, charLine, charColumn);
2833     }
2834 
2835     peekPrimaryRequested(false);
2836 
2837     QWidget::keyReleaseEvent(event);
2838 }
2839 
2840 bool TerminalDisplay::handleShortcutOverrideEvent(QKeyEvent *keyEvent)
2841 {
2842     const int modifiers = keyEvent->modifiers();
2843 
2844     //  When a possible shortcut combination is pressed,
2845     //  emit the overrideShortcutCheck() signal to allow the host
2846     //  to decide whether the terminal should override it or not.
2847     if (modifiers != Qt::NoModifier) {
2848         int modifierCount = 0;
2849         unsigned int currentModifier = Qt::ShiftModifier;
2850 
2851         while (currentModifier <= Qt::KeypadModifier) {
2852             if ((modifiers & currentModifier) != 0u) {
2853                 modifierCount++;
2854             }
2855             currentModifier <<= 1;
2856         }
2857         if (modifierCount < 2) {
2858             bool override = false;
2859             Q_EMIT overrideShortcutCheck(keyEvent, override);
2860             if (override) {
2861                 keyEvent->accept();
2862                 return true;
2863             }
2864         }
2865     }
2866 
2867     // Override any of the following shortcuts because
2868     // they are needed by the terminal
2869     int keyCode = keyEvent->key() | modifiers;
2870     switch (keyCode) {
2871         // list is taken from the QLineEdit::event() code
2872     case Qt::Key_Tab:
2873     case Qt::Key_Delete:
2874     case Qt::Key_Home:
2875     case Qt::Key_End:
2876     case Qt::Key_Backspace:
2877     case Qt::Key_Left:
2878     case Qt::Key_Right:
2879     case Qt::Key_Slash:
2880     case Qt::Key_Period:
2881     case Qt::Key_Space:
2882         keyEvent->accept();
2883         return true;
2884     }
2885     return false;
2886 }
2887 
2888 bool TerminalDisplay::event(QEvent *event)
2889 {
2890     bool eventHandled = false;
2891     switch (event->type()) {
2892     case QEvent::ShortcutOverride:
2893         eventHandled = handleShortcutOverrideEvent(static_cast<QKeyEvent *>(event));
2894         break;
2895     case QEvent::PaletteChange:
2896     case QEvent::ApplicationPaletteChange:
2897         if (_terminalColor) {
2898             _terminalColor->onColorsChanged();
2899         }
2900         break;
2901     case QEvent::FocusOut:
2902     case QEvent::FocusIn:
2903         if (_screenWindow != nullptr) {
2904             // force a redraw on focusIn, fixes the
2905             // black screen bug when the view is focused
2906             // but doesn't redraws.
2907             _screenWindow->notifyOutputChanged();
2908         }
2909         update();
2910         break;
2911 
2912     case QEvent::ScrollPrepare:
2913         scrollPrepareEvent(static_cast<QScrollPrepareEvent *>(event));
2914         break;
2915     case QEvent::Scroll:
2916         scrollEvent(static_cast<QScrollEvent *>(event));
2917         break;
2918 
2919     default:
2920         break;
2921     }
2922     return eventHandled ? true : QWidget::event(event);
2923 }
2924 
2925 void TerminalDisplay::contextMenuEvent(QContextMenuEvent *event)
2926 {
2927     // the logic for the mouse case is within MousePressEvent()
2928     if (event->reason() != QContextMenuEvent::Mouse) {
2929         Q_EMIT configureRequest(mapFromGlobal(QCursor::pos()));
2930     }
2931 }
2932 
2933 /* --------------------------------------------------------------------- */
2934 /*                                                                       */
2935 /*                                  Bell                                 */
2936 /*                                                                       */
2937 /* --------------------------------------------------------------------- */
2938 
2939 void TerminalDisplay::bell(const QString &message)
2940 {
2941     _bell.bell(this, message, hasFocus());
2942 }
2943 
2944 /* --------------------------------------------------------------------- */
2945 /*                                                                       */
2946 /* Drag & Drop                                                           */
2947 /*                                                                       */
2948 /* --------------------------------------------------------------------- */
2949 
2950 void TerminalDisplay::dragEnterEvent(QDragEnterEvent *event)
2951 {
2952     // text/plain alone is enough for KDE-apps
2953     // text/uri-list is for supporting some non-KDE apps, such as thunar
2954     //   and pcmanfm
2955     // That also applies in dropEvent()
2956     const auto mimeData = event->mimeData();
2957     if ((!_readOnly) && (mimeData != nullptr) && (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list")))) {
2958         event->acceptProposedAction();
2959     }
2960 }
2961 
2962 namespace
2963 {
2964 QString extractDroppedText(const QList<QUrl> &urls)
2965 {
2966     QString dropText;
2967     for (int i = 0; i < urls.count(); i++) {
2968         KIO::StatJob *job = KIO::mostLocalUrl(urls[i], KIO::HideProgressInfo);
2969         if (!job->exec()) {
2970             continue;
2971         }
2972 
2973         const QUrl url = job->mostLocalUrl();
2974         // in future it may be useful to be able to insert file names with drag-and-drop
2975         // without quoting them (this only affects paths with spaces in)
2976         dropText += KShell::quoteArg(url.isLocalFile() ? url.path() : url.url());
2977 
2978         // Each filename(including the last) should be followed by one space.
2979         dropText += QLatin1Char(' ');
2980     }
2981     return dropText;
2982 }
2983 
2984 void setupCdToUrlAction(const QString &dropText, const QUrl &url, QList<QAction *> &additionalActions, TerminalDisplay *display)
2985 {
2986     KIO::StatJob *job = KIO::mostLocalUrl(url, KIO::HideProgressInfo);
2987     if (!job->exec()) {
2988         return;
2989     }
2990 
2991     const QUrl localUrl = job->mostLocalUrl();
2992     if (!localUrl.isLocalFile()) {
2993         return;
2994     }
2995 
2996     const QFileInfo fileInfo(localUrl.path());
2997     if (!fileInfo.isDir()) {
2998         return;
2999     }
3000 
3001     QAction *cdAction = new QAction(i18n("Change &Directory To"), display);
3002     const QByteArray triggerText = QString(QLatin1String(" cd ") + dropText + QLatin1Char('\n')).toLocal8Bit();
3003     display->connect(cdAction, &QAction::triggered, display, [display, triggerText] {
3004         Q_EMIT display->sendStringToEmu(triggerText);
3005     });
3006     additionalActions.append(cdAction);
3007 }
3008 
3009 }
3010 
3011 void TerminalDisplay::dropEvent(QDropEvent *event)
3012 {
3013     if (_readOnly) {
3014         event->accept();
3015         return;
3016     }
3017 
3018     const auto mimeData = event->mimeData();
3019     if (mimeData == nullptr) {
3020         return;
3021     }
3022     auto urls = mimeData->urls();
3023 
3024     QString dropText;
3025     if (!urls.isEmpty()) {
3026         dropText = extractDroppedText(urls);
3027 
3028         // If our target is local we will open a popup - otherwise the fallback kicks
3029         // in and the URLs will simply be pasted as text.
3030         if (!_dropUrlsAsText && (_sessionController != nullptr) && _sessionController->url().isLocalFile()) {
3031             // A standard popup with Copy, Move and Link as options -
3032             // plus an additional Paste option.
3033 
3034             QAction *pasteAction = new QAction(i18n("&Paste Location"), this);
3035             connect(pasteAction, &QAction::triggered, this, [this, dropText] {
3036                 Q_EMIT sendStringToEmu(dropText.toLocal8Bit());
3037             });
3038 
3039             QList<QAction *> additionalActions;
3040             additionalActions.append(pasteAction);
3041 
3042             if (urls.count() == 1) {
3043                 setupCdToUrlAction(dropText, urls.at(0), additionalActions, this);
3044             }
3045 
3046             QUrl target = QUrl::fromLocalFile(_sessionController->currentDir());
3047 
3048             KIO::DropJob *job = KIO::drop(event, target);
3049             KJobWidgets::setWindow(job, this);
3050             job->setApplicationActions(additionalActions);
3051             return;
3052         }
3053 
3054     } else {
3055         dropText = mimeData->text();
3056     }
3057 
3058     if (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list"))) {
3059         doPaste(dropText, false);
3060     }
3061 
3062     setFocus(Qt::MouseFocusReason);
3063 }
3064 
3065 void TerminalDisplay::doDrag()
3066 {
3067     const QMimeData *clipboardMimeData = QApplication::clipboard()->mimeData(QClipboard::Selection);
3068     if (clipboardMimeData == nullptr) {
3069         return;
3070     }
3071     auto mimeData = new QMimeData();
3072     _dragInfo.state = diDragging;
3073     _dragInfo.dragObject = new QDrag(this);
3074     mimeData->setText(clipboardMimeData->text());
3075     mimeData->setHtml(clipboardMimeData->html());
3076     _dragInfo.dragObject->setMimeData(mimeData);
3077     _dragInfo.dragObject->exec(Qt::CopyAction);
3078 }
3079 
3080 void TerminalDisplay::setSessionController(SessionController *controller)
3081 {
3082     _sessionController = controller;
3083     _headerBar->finishHeaderSetup(controller);
3084 }
3085 
3086 SessionController *TerminalDisplay::sessionController()
3087 {
3088     return _sessionController;
3089 }
3090 
3091 Session::Ptr TerminalDisplay::session() const
3092 {
3093     return _sessionController->session();
3094 }
3095 
3096 IncrementalSearchBar *TerminalDisplay::searchBar() const
3097 {
3098     return _searchBar;
3099 }
3100 
3101 void TerminalDisplay::applyProfile(const Profile::Ptr &profile)
3102 {
3103     // load color scheme
3104     _colorScheme = ViewManager::colorSchemeForProfile(profile);
3105     _terminalColor->applyProfile(profile, _colorScheme, randomSeed());
3106     setWallpaper(_colorScheme->wallpaper());
3107 
3108     // load font
3109     _terminalFont->applyProfile(profile);
3110 
3111     // set scroll-bar position
3112     _scrollBar->setScrollBarPosition(Enum::ScrollBarPositionEnum(profile->property<int>(Profile::ScrollBarPosition)));
3113     _scrollBar->setScrollFullPage(profile->property<bool>(Profile::ScrollFullPage));
3114 
3115     // show hint about terminal size after resizing
3116     _showTerminalSizeHint = profile->showTerminalSizeHint();
3117     _dimWhenInactive = profile->dimWhenInactive();
3118     _borderWhenActive = profile->borderWhenActive();
3119 
3120     // terminal features
3121     setBlinkingCursorEnabled(profile->blinkingCursorEnabled());
3122     setBlinkingTextEnabled(profile->blinkingTextEnabled());
3123     _tripleClickMode = Enum::TripleClickModeEnum(profile->property<int>(Profile::TripleClickMode));
3124     setAutoCopySelectedText(profile->autoCopySelectedText());
3125     _ctrlRequiredForDrag = profile->property<bool>(Profile::CtrlRequiredForDrag);
3126     _dropUrlsAsText = profile->property<bool>(Profile::DropUrlsAsText);
3127     _bidiEnabled = profile->bidiRenderingEnabled();
3128     _bidiLineLTR = profile->bidiLineLTR();
3129     _bidiTableDirOverride = profile->bidiTableDirOverride();
3130     _semanticUpDown = profile->semanticUpDown();
3131     _semanticInputClick = profile->semanticInputClick();
3132     _trimLeadingSpaces = profile->property<bool>(Profile::TrimLeadingSpacesInSelectedText);
3133     _trimTrailingSpaces = profile->property<bool>(Profile::TrimTrailingSpacesInSelectedText);
3134     _openLinksByDirectClick = profile->property<bool>(Profile::OpenLinksByDirectClickEnabled);
3135     setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum(profile->property<int>(Profile::MiddleClickPasteMode)));
3136     setCopyTextAsHTML(profile->property<bool>(Profile::CopyTextAsHTML));
3137 
3138     // highlight lines scrolled into view (must be applied before margin/center)
3139     _scrollBar->setHighlightScrolledLines(profile->property<bool>(Profile::HighlightScrolledLines));
3140 
3141     // reflow lines when terminal resizes
3142     //_screenWindow->screen()->setReflow(profile->property<bool>(Profile::ReflowLines));
3143 
3144     // margin/center
3145     setMargin(profile->property<int>(Profile::TerminalMargin));
3146     setCenterContents(profile->property<bool>(Profile::TerminalCenter));
3147 
3148     // cursor shape
3149     setKeyboardCursorShape(Enum::CursorShapeEnum(profile->property<int>(Profile::CursorShape)));
3150 
3151     // word characters
3152     setWordCharacters(profile->wordCharacters());
3153 
3154     // bell mode
3155     _bell.setBellMode(Enum::BellModeEnum(profile->property<int>(Profile::BellMode)));
3156 
3157     // mouse wheel zoom
3158     _mouseWheelZoom = profile->mouseWheelZoomEnabled();
3159 
3160     _displayVerticalLine = profile->verticalLine();
3161     _displayVerticalLineAtChar = profile->verticalLineAtChar();
3162     _scrollBar->setAlternateScrolling(profile->property<bool>(Profile::AlternateScrolling));
3163     _dimValue = profile->dimValue();
3164     _focusBorderColor = profile->focusBorderColor();
3165 
3166     _filterChain->setUrlHintsModifiers(Qt::KeyboardModifiers(profile->property<int>(Profile::UrlHintsModifiers)));
3167     _filterChain->setReverseUrlHints(profile->property<bool>(Profile::ReverseUrlHints));
3168 
3169     _peekPrimaryShortcut = profile->peekPrimaryKeySequence();
3170 }
3171 
3172 void TerminalDisplay::printScreen()
3173 {
3174     auto lprintContent = [this](QPainter &painter, bool friendly) {
3175         QPoint columnLines(_usedLines, _usedColumns);
3176         auto lfontget = [this]() {
3177             return _terminalFont->getVTFont();
3178         };
3179         auto lfontset = [this](const QFont &f) {
3180             _terminalFont->setVTFont(f);
3181         };
3182 
3183         _printManager->printContent(painter, friendly, columnLines, lfontget, lfontset);
3184     };
3185     _printManager->printRequest(lprintContent, this);
3186 }
3187 
3188 Character TerminalDisplay::getCursorCharacter(int column, int line)
3189 {
3190     return _image[loc(column, line)];
3191 }
3192 
3193 int TerminalDisplay::selectionState() const
3194 {
3195     return _actSel;
3196 }
3197 
3198 void TerminalDisplay::clearMouseSelection()
3199 {
3200     if (!session()->getSelectMode()) {
3201         screenWindow()->clearSelection();
3202     }
3203 }
3204 
3205 int TerminalDisplay::bidiMap(Character *screenline,
3206                              QString &line,
3207                              int *log2line,
3208                              int *line2log,
3209                              uint16_t *shapemap,
3210                              int32_t *vis2line,
3211                              bool &shaped,
3212                              bool shape,
3213                              bool bidi) const
3214 {
3215     const int linewidth = _usedColumns;
3216     uint64_t notSkipped[MAX_LINE_WIDTH / 64] = {};
3217     int i;
3218     int lastNonSpace = 0;
3219     shaped = false;
3220 
3221     // use one string to assign into to avoid temporary allocations
3222     QString convertBuffer;
3223 
3224     for (i = 0; i < linewidth; i++) {
3225         int pos = line.size();
3226         log2line[i] = pos;
3227         line2log[pos] = i;
3228         notSkipped[pos / 64] |= 1ul << (pos % 64);
3229         const Character char_value = screenline[i];
3230         if (char_value.rendition.f.extended != 0) {
3231             // sequence of characters
3232             ushort extendedCharLength = 0;
3233             if (const char32_t *chars = ExtendedCharTable::instance.lookupExtendedChar(char_value.character, extendedCharLength)) {
3234                 Q_ASSERT(extendedCharLength > 1);
3235                 convertBuffer.assign(chars, chars + extendedCharLength);
3236                 line.append(convertBuffer);
3237             }
3238             lastNonSpace = i;
3239         } else {
3240             convertBuffer.assign(&char_value.character, &char_value.character + 1);
3241             line.append(convertBuffer);
3242             if (!line[line.size() - 1].isSpace()) {
3243                 lastNonSpace = i;
3244             }
3245         }
3246     }
3247     log2line[i] = line.size();
3248     // line.truncate(lastNonSpace + 1);
3249     UErrorCode errorCode = U_ZERO_ERROR;
3250     if (shape) {
3251         UChar shaped_line[MAX_LINE_WIDTH];
3252         u_shapeArabic(reinterpret_cast<const UChar *>(line.utf16()),
3253                       line.length(),
3254                       shaped_line,
3255                       MAX_LINE_WIDTH,
3256                       U_SHAPE_AGGREGATE_TASHKEEL_NOOP | U_SHAPE_LENGTH_FIXED_SPACES_NEAR | U_SHAPE_LETTERS_SHAPE,
3257                       &errorCode);
3258         for (int i = 0; i < line.length(); i++) {
3259             shapemap[i] = shaped_line[i];
3260             if (line[i] != shaped_line[i]) {
3261                 shaped = true;
3262             }
3263         }
3264     }
3265     if (!bidi) {
3266         return lastNonSpace;
3267     }
3268     UBiDiLevel paraLevel = _bidiLineLTR ? 0 : UBIDI_DEFAULT_LTR;
3269     if (_bidiTableDirOverride) {
3270         ubidi_setClassCallback(ubidi, BiDiClass, nullptr, nullptr, nullptr, &errorCode);
3271     }
3272     ubidi_setPara(ubidi, reinterpret_cast<const UChar *>(line.utf16()), line.length(), paraLevel, nullptr, &errorCode);
3273     int len = ubidi_getProcessedLength(ubidi);
3274     int32_t semi_vis2line[MAX_LINE_WIDTH];
3275     ubidi_getVisualMap(ubidi, semi_vis2line, &errorCode);
3276     int p = 0;
3277     for (int i = 0; i < len; i++) {
3278         if ((notSkipped[semi_vis2line[i] / 64] & (1ul << (semi_vis2line[i] % 64))) != 0) {
3279             vis2line[p++] = semi_vis2line[i];
3280         }
3281     }
3282     return _bidiLineLTR ? lastNonSpace : linewidth - 1;
3283 }
3284 
3285 void TerminalDisplay::clearSelection()
3286 {
3287     _screenWindow->clearSelection();
3288     _doubleClickSelectedText.clear();
3289     _doubleClickSelectedHtml.clear();
3290 }
3291 
3292 #include "moc_TerminalDisplay.cpp"