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"