You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

2757 lines
95 KiB

/*
SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>
SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
// Own
#include "terminalDisplay/TerminalDisplay.h"
#include "KonsoleSettings.h"
// Config
#include "config-konsole.h"
// Qt
#include <QApplication>
#include <QClipboard>
#include <QKeyEvent>
#include <QEvent>
#include <QFileInfo>
#include <QVBoxLayout>
#include <QAction>
#include <QLabel>
#include <QMimeData>
#include <QPainter>
#include <QPixmap>
#include <QStyle>
#include <QTimer>
#include <QDrag>
#include <QDesktopServices>
#include <QAccessible>
#include <QElapsedTimer>
// KDE
#include <KShell>
#include <KColorScheme>
#include <KCursor>
#include <KLocalizedString>
#include <KNotification>
#include <KIO/DropJob>
#include <KJobWidgets>
#include <KMessageBox>
#include <KMessageWidget>
#include <KIO/StatJob>
// Konsole
#include "extras/CompositeWidgetFocusWatcher.h"
#include "extras/AutoScrollHandler.h"
#include "filterHotSpots/Filter.h"
#include "filterHotSpots/TerminalImageFilterChain.h"
#include "filterHotSpots/HotSpot.h"
#include "filterHotSpots/FileFilterHotspot.h"
#include "filterHotSpots/EscapeSequenceUrlFilter.h"
#include "filterHotSpots/EscapeSequenceUrlFilterHotSpot.h"
#include "konsoledebug.h"
#include "../decoders/PlainTextDecoder.h"
#include "Screen.h"
#include "../characters/ExtendedCharTable.h"
#include "../widgets/TerminalDisplayAccessible.h"
#include "session/SessionController.h"
#include "session/SessionManager.h"
#include "session/Session.h"
#include "WindowSystemInfo.h"
#include "widgets/IncrementalSearchBar.h"
#include "profile/Profile.h"
#include "ViewManager.h" // for colorSchemeForProfile. // TODO: Rewrite this.
#include "../characters/LineBlockCharacters.h"
#include "PrintOptions.h"
#include "../widgets/KonsolePrintManager.h"
#include "EscapeSequenceUrlExtractor.h"
#include "TerminalPainter.h"
#include "TerminalScrollBar.h"
#include "TerminalColor.h"
#include "TerminalFonts.h"
#include "TerminalClipboard.h"
using namespace Konsole;
inline int TerminalDisplay::loc(int x, int y) const {
if (y < 0 || y > _lines) {
qDebug() << "Y: " << y << "Lines" << _lines;
}
if (x < 0 || x > _columns ) {
qDebug() << "X" << x << "Columns" << _columns;
}
Q_ASSERT(y >= 0 && y < _lines);
Q_ASSERT(x >= 0 && x < _columns);
x = qBound(0, x, _columns - 1);
y = qBound(0, y, _lines - 1);
return y * _columns + x;
}
/* ------------------------------------------------------------------------- */
/* */
/* Colors */
/* */
/* ------------------------------------------------------------------------- */
/* Note that we use ANSI color order (bgr), while IBMPC color order is (rgb)
Code 0 1 2 3 4 5 6 7
----------- ------- ------- ------- ------- ------- ------- ------- -------
ANSI (bgr) Black Red Green Yellow Blue Magenta Cyan White
IBMPC (rgb) Black Blue Green Cyan Red Magenta Yellow White
*/
void TerminalDisplay::setScreenWindow(ScreenWindow* window)
{
// disconnect existing screen window if any
if (!_screenWindow.isNull()) {
disconnect(_screenWindow , nullptr , this , nullptr);
}
_screenWindow = window;
if (!_screenWindow.isNull()) {
connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateImage);
connect(_screenWindow.data() , &Konsole::ScreenWindow::currentResultLineChanged , this , &Konsole::TerminalDisplay::updateImage);
connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, [this]() {
_filterUpdateRequired = true;
});
connect(_screenWindow.data(), &Konsole::ScreenWindow::screenAboutToChange, this, [this]() {
_iPntSel = QPoint(-1, -1);
_pntSel = QPoint(-1, -1);
_tripleSelBegin = QPoint(-1, -1);
});
connect(_screenWindow.data(), &Konsole::ScreenWindow::scrolled, this, [this]() {
_filterUpdateRequired = true;
});
connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, []() {
QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle);
});
_screenWindow->setWindowLines(_lines);
auto profile = SessionManager::instance()->sessionProfile(_sessionController->session());
_screenWindow->screen()->urlExtractor()->setAllowedLinkSchema(profile->escapedLinksSchema());
_screenWindow->screen()->setReflowLines(profile->property<bool>(Profile::ReflowLines));
}
}
/* ------------------------------------------------------------------------- */
/* */
/* Accessibility */
/* */
/* ------------------------------------------------------------------------- */
namespace Konsole
{
#ifndef QT_NO_ACCESSIBILITY
/**
* This function installs the factory function which lets Qt instantiate the QAccessibleInterface
* for the TerminalDisplay.
*/
QAccessibleInterface* accessibleInterfaceFactory(const QString &key, QObject *object)
{
Q_UNUSED(key)
if (auto *display = qobject_cast<TerminalDisplay*>(object)) {
return new TerminalDisplayAccessible(display);
}
return nullptr;
}
#endif
}
/* ------------------------------------------------------------------------- */
/* */
/* Constructor / Destructor */
/* */
/* ------------------------------------------------------------------------- */
TerminalDisplay::TerminalDisplay(QWidget* parent)
: QWidget(parent)
, _screenWindow(nullptr)
, _verticalLayout(new QVBoxLayout(this))
, _lines(1)
, _columns(1)
, _prevCharacterLine(-1)
, _prevCharacterColumn(-1)
, _usedLines(1)
, _usedColumns(1)
, _contentRect(QRect())
, _image(nullptr)
, _imageSize(0)
, _lineProperties(QVector<LineProperty>())
, _randomSeed(0)
, _resizing(false)
, _showTerminalSizeHint(true)
, _bidiEnabled(false)
, _usesMouseTracking(false)
, _bracketedPasteMode(false)
, _iPntSel(QPoint(-1, -1))
, _pntSel(QPoint(-1, -1))
, _tripleSelBegin(QPoint(-1, -1))
, _actSel(0)
, _wordSelectionMode(false)
, _lineSelectionMode(false)
, _preserveLineBreaks(true)
, _columnSelectionMode(false)
, _autoCopySelectedText(false)
, _copyTextAsHTML(true)
, _middleClickPasteMode(Enum::PasteFromX11Selection)
, _wordCharacters(QStringLiteral(":@-./_~"))
, _bell(Enum::NotifyBell)
, _allowBlinkingText(true)
, _allowBlinkingCursor(false)
, _textBlinking(false)
, _cursorBlinking(false)
, _hasTextBlinker(false)
, _openLinksByDirectClick(false)
, _ctrlRequiredForDrag(true)
, _dropUrlsAsText(false)
, _tripleClickMode(Enum::SelectWholeLine)
, _possibleTripleClick(false)
, _resizeWidget(nullptr)
, _resizeTimer(nullptr)
, _flowControlWarningEnabled(false)
, _outputSuspendedMessageWidget(nullptr)
, _size(QSize())
, _wallpaper(nullptr)
, _filterChain(new TerminalImageFilterChain(this))
, _filterUpdateRequired(true)
, _cursorShape(Enum::BlockCursor)
, _sessionController(nullptr)
, _trimLeadingSpaces(false)
, _trimTrailingSpaces(false)
, _mouseWheelZoom(false)
, _margin(1)
, _centerContents(false)
, _readOnlyMessageWidget(nullptr)
, _readOnly(false)
, _dimWhenInactive(false)
, _scrollWheelState(ScrollState())
, _searchBar(new IncrementalSearchBar(this))
, _headerBar(new TerminalHeaderBar(this))
, _searchResultRect(QRect())
, _drawOverlay(false)
, _scrollBar(nullptr)
, _terminalColor(nullptr)
, _terminalFont(std::make_unique<TerminalFont>(this))
{
// terminal applications are not designed with Right-To-Left in mind,
// so the layout is forced to Left-To-Right
setLayoutDirection(Qt::LeftToRight);
_contentRect = QRect(_margin, _margin, 1, 1);
// create scroll bar for scrolling output up and down
_scrollBar = new TerminalScrollBar(this);
_scrollBar->setAutoFillBackground(false);
// set the scroll bar's slider to occupy the whole area of the scroll bar initially
_scrollBar->setScroll(0, 0);
_scrollBar->setCursor(Qt::ArrowCursor);
_headerBar->setCursor(Qt::ArrowCursor);
connect(_headerBar, &TerminalHeaderBar::requestToggleExpansion, this, &Konsole::TerminalDisplay::requestToggleExpansion);
connect(_headerBar, &TerminalHeaderBar::requestMoveToNewTab, this, [this]{requestMoveToNewTab(this);});
connect(_scrollBar, &QScrollBar::sliderMoved, this, &Konsole::TerminalDisplay::viewScrolledByUser);
// setup timers for blinking text
_blinkTextTimer = new QTimer(this);
_blinkTextTimer->setInterval(TEXT_BLINK_DELAY);
connect(_blinkTextTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkTextEvent);
// setup timers for blinking cursor
_blinkCursorTimer = new QTimer(this);
_blinkCursorTimer->setInterval(QApplication::cursorFlashTime() / 2);
connect(_blinkCursorTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkCursorEvent);
// hide mouse cursor on keystroke or idle
KCursor::setAutoHideCursor(this, true);
setMouseTracking(true);
setUsesMouseTracking(false);
setBracketedPasteMode(false);
// Enable drag and drop support
setAcceptDrops(true);
_dragInfo.state = diNone;
setFocusPolicy(Qt::WheelFocus);
// enable input method support
setAttribute(Qt::WA_InputMethodEnabled, true);
// this is an important optimization, it tells Qt
// that TerminalDisplay will handle repainting its entire area.
setAttribute(Qt::WA_OpaquePaintEvent);
// Add the stretch item once, the KMessageWidgets are inserted at index 0.
_verticalLayout->addWidget(_headerBar);
_verticalLayout->addStretch();
_verticalLayout->setSpacing(0);
_verticalLayout->setContentsMargins(0, 0, 0, 0);
setLayout(_verticalLayout);
new AutoScrollHandler(this);
// Keep this last
CompositeWidgetFocusWatcher *focusWatcher = new CompositeWidgetFocusWatcher(this);
connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged,
this, [this](bool focused) {_hasCompositeFocus = focused;});
connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged,
this, &TerminalDisplay::compositeFocusChanged);
connect(focusWatcher, &CompositeWidgetFocusWatcher::compositeFocusChanged,
_headerBar, &TerminalHeaderBar::setFocusIndicatorState);
connect(&_bell, &TerminalBell::visualBell, this, [this] {
_terminalColor->visualBell();
});
#ifndef QT_NO_ACCESSIBILITY
QAccessible::installFactory(Konsole::accessibleInterfaceFactory);
#endif
connect(KonsoleSettings::self(), &KonsoleSettings::configChanged, this, &TerminalDisplay::setupHeaderVisibility);
_terminalColor = new TerminalColor(this);
connect(_terminalColor, &TerminalColor::onPalette, _scrollBar, &TerminalScrollBar::setPalette);
_terminalPainter = new TerminalPainter(this);
connect(this, &TerminalDisplay::drawContents, _terminalPainter, &TerminalPainter::drawContents);
connect(this, &TerminalDisplay::drawCurrentResultRect, _terminalPainter, &TerminalPainter::drawCurrentResultRect);
connect(this, &TerminalDisplay::highlightScrolledLines, _terminalPainter, &TerminalPainter::highlightScrolledLines);
connect(this, &TerminalDisplay::highlightScrolledLinesRegion, _terminalPainter, &TerminalPainter::highlightScrolledLinesRegion);
connect(this, &TerminalDisplay::drawBackground, _terminalPainter, &TerminalPainter::drawBackground);
connect(this, &TerminalDisplay::drawCharacters, _terminalPainter, &TerminalPainter::drawCharacters);
connect(this, &TerminalDisplay::drawInputMethodPreeditString, _terminalPainter, &TerminalPainter::drawInputMethodPreeditString);
auto ldrawBackground = [this](QPainter &painter,
const QRect &rect, const QColor &backgroundColor, bool useOpacitySetting) {
Q_EMIT drawBackground(painter, rect, backgroundColor, useOpacitySetting);
};
auto ldrawContents = [this](QPainter &paint, const QRect &rect, bool friendly) {
Q_EMIT drawContents(_image, paint, rect, friendly, _imageSize, _bidiEnabled, _lineProperties);
};
auto lgetBackgroundColor = [this]() {
return _terminalColor->backgroundColor();
};
_printManager.reset(new KonsolePrintManager(ldrawBackground, ldrawContents, lgetBackgroundColor));
}
TerminalDisplay::~TerminalDisplay()
{
disconnect(_blinkTextTimer);
disconnect(_blinkCursorTimer);
delete[] _image;
delete _filterChain;
}
void TerminalDisplay::setupHeaderVisibility()
{
_headerBar->applyVisibilitySettings();
calcGeometry();
}
void TerminalDisplay::hideDragTarget()
{
_drawOverlay = false;
update();
}
void TerminalDisplay::showDragTarget(const QPoint& cursorPos)
{
using EdgeDistance = std::pair<int, Qt::Edge>;
auto closerToEdge = std::min<EdgeDistance>(
{
{cursorPos.x(), Qt::LeftEdge},
{cursorPos.y(), Qt::TopEdge},
{width() - cursorPos.x(), Qt::RightEdge},
{height() - cursorPos.y(), Qt::BottomEdge}
},
[](const EdgeDistance& left, const EdgeDistance& right) -> bool {
return left.first < right.first;
}
);
if (_overlayEdge == closerToEdge.second) {
return;
}
_overlayEdge = closerToEdge.second;
_drawOverlay = true;
update();
}
/* ------------------------------------------------------------------------- */
/* */
/* Display Operations */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::setKeyboardCursorShape(Enum::CursorShapeEnum shape)
{
_cursorShape = shape;
}
void TerminalDisplay::setCursorStyle(Enum::CursorShapeEnum shape, bool isBlinking)
{
setKeyboardCursorShape(shape);
setBlinkingCursorEnabled(isBlinking);
// when the cursor shape and blinking state are changed via the
// Set Cursor Style (DECSCUSR) escape sequences in vim, and if the
// cursor isn't set to blink, the cursor shape doesn't actually
// change until the cursor is moved by the user; calling update()
// makes the cursor shape get updated sooner.
if (!isBlinking) {
update();
}
}
void TerminalDisplay::resetCursorStyle()
{
Q_ASSERT(_sessionController != nullptr);
Q_ASSERT(!_sessionController->session().isNull());
Profile::Ptr currentProfile = SessionManager::instance()->sessionProfile(_sessionController->session());
if (currentProfile != nullptr) {
auto shape = static_cast<Enum::CursorShapeEnum>(currentProfile->property<int>(Profile::CursorShape));
setKeyboardCursorShape(shape);
setBlinkingCursorEnabled(currentProfile->blinkingCursorEnabled());
}
}
void TerminalDisplay::setWallpaper(const ColorSchemeWallpaper::Ptr &p)
{
_wallpaper = p;
}
void TerminalDisplay::scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode, int amount)
{
_screenWindow->scrollBy(mode, amount, _scrollBar->scrollFullPage());
_screenWindow->setTrackOutput(_screenWindow->atEndOfOutput());
updateImage();
viewScrolledByUser();
}
void TerminalDisplay::setRandomSeed(uint randomSeed)
{
_randomSeed = randomSeed;
}
uint TerminalDisplay::randomSeed() const
{
return _randomSeed;
}
void TerminalDisplay::processFilters()
{
if (_screenWindow.isNull()) {
return;
}
if (!_filterUpdateRequired) {
return;
}
const QRegion preUpdateHotSpots = _filterChain->hotSpotRegion();
// use _screenWindow->getImage() here rather than _image because
// other classes may call processFilters() when this display's
// ScreenWindow emits a scrolled() signal - which will happen before
// updateImage() is called on the display and therefore _image is
// out of date at this point
_filterChain->setImage(_screenWindow->getImage(),
_screenWindow->windowLines(),
_screenWindow->windowColumns(),
_screenWindow->getLineProperties());
_filterChain->process();
const QRegion postUpdateHotSpots = _filterChain->hotSpotRegion();
update(preUpdateHotSpots | postUpdateHotSpots);
_filterUpdateRequired = false;
}
void TerminalDisplay::updateImage()
{
if (_screenWindow.isNull()) {
return;
}
// Better control over screen resizing visual glitches
_screenWindow->updateCurrentLine();
// optimization - scroll the existing image where possible and
// avoid expensive text drawing for parts of the image that
// can simply be moved up or down
// disable this shortcut for transparent konsole with scaled pixels, otherwise we get rendering artifacts, see BUG 350651
if (!(WindowSystemInfo::HAVE_TRANSPARENCY && (qApp->devicePixelRatio() > 1.0)) && _wallpaper->isNull() && !_searchBar->isVisible()) {
// if the flow control warning is enabled this will interfere with the
// scrolling optimizations and cause artifacts. the simple solution here
// is to just disable the optimization whilst it is visible
if (!((_outputSuspendedMessageWidget != nullptr) && _outputSuspendedMessageWidget->isVisible()) &&
!((_readOnlyMessageWidget != nullptr) && _readOnlyMessageWidget->isVisible())) {
// hide terminal size label to prevent it being scrolled and show again after scroll
const bool viewResizeWidget = (_resizeWidget != nullptr) && _resizeWidget->isVisible();
if (viewResizeWidget) {
_resizeWidget->hide();
}
_scrollBar->scrollImage(_screenWindow->scrollCount(), _screenWindow->scrollRegion(), _image, _imageSize);
if (viewResizeWidget) {
_resizeWidget->show();
}
}
}
if (_image == nullptr) {
// Create _image.
// The emitted changedContentSizeSignal also leads to getImage being recreated, so do this first.
updateImageSize();
}
Character* const newimg = _screenWindow->getImage();
const int lines = _screenWindow->windowLines();
const int columns = _screenWindow->windowColumns();
QVector<LineProperty> newLineProperties = _screenWindow->getLineProperties();
_scrollBar->setScroll(_screenWindow->currentLine() , _screenWindow->lineCount());
Q_ASSERT(_usedLines <= _lines);
Q_ASSERT(_usedColumns <= _columns);
int y;
int x;
int len;
const QPoint tL = contentsRect().topLeft();
const int tLx = tL.x();
const int tLy = tL.y();
_hasTextBlinker = false;
CharacterColor cf; // undefined
const int linesToUpdate = qBound(0, lines, _lines);
const int columnsToUpdate = qBound(0, columns, _columns);
auto dirtyMask = new char[columnsToUpdate + 2];
QRegion dirtyRegion;
// debugging variable, this records the number of lines that are found to
// be 'dirty' ( ie. have changed from the old _image to the new _image ) and
// which therefore need to be repainted
int dirtyLineCount = 0;
for (y = 0; y < linesToUpdate; ++y) {
const Character* currentLine = &_image[y * _columns];
const Character* const newLine = &newimg[y * columns];
bool updateLine = false;
// The dirty mask indicates which characters need repainting. We also
// mark surrounding neighbors dirty, in case the character exceeds
// its cell boundaries
memset(dirtyMask, 0, columnsToUpdate + 2);
for (x = 0 ; x < columnsToUpdate ; ++x) {
if (newLine[x] != currentLine[x]) {
dirtyMask[x] = 1;
}
}
if (!_resizing) { // not while _resizing, we're expecting a paintEvent
for (x = 0; x < columnsToUpdate; ++x) {
_hasTextBlinker |= (newLine[x].rendition & RE_BLINK);
// Start drawing if this character or the next one differs.
// We also take the next one into account to handle the situation
// where characters exceed their cell width.
if (dirtyMask[x] != 0) {
if (newLine[x + 0].character == 0u) {
continue;
}
const bool lineDraw = LineBlockCharacters::canDraw(newLine[x + 0].character);
const bool doubleWidth = (x + 1 == columnsToUpdate) ? false : (newLine[x + 1].character == 0);
const RenditionFlags cr = newLine[x].rendition;
const CharacterColor clipboard = newLine[x].backgroundColor;
if (newLine[x].foregroundColor != cf) {
cf = newLine[x].foregroundColor;
}
const int lln = columnsToUpdate - x;
for (len = 1; len < lln; ++len) {
const Character& ch = newLine[x + len];
if (ch.character == 0u) {
continue; // Skip trailing part of multi-col chars.
}
const bool nextIsDoubleWidth = (x + len + 1 == columnsToUpdate) ? false : (newLine[x + len + 1].character == 0);
if (ch.foregroundColor != cf ||
ch.backgroundColor != clipboard ||
(ch.rendition & ~RE_EXTENDED_CHAR) != (cr & ~RE_EXTENDED_CHAR) ||
(dirtyMask[x + len] == 0) ||
LineBlockCharacters::canDraw(ch.character) != lineDraw ||
nextIsDoubleWidth != doubleWidth) {
break;
}
}
updateLine = true;
x += len - 1;
}
}
}
if (y >= _lineProperties.count() || y >= newLineProperties.count() || _lineProperties[y] != newLineProperties[y]) {
updateLine = true;
}
// if the characters on the line are different in the old and the new _image
// then this line must be repainted.
if (updateLine) {
dirtyLineCount++;
// add the area occupied by this line to the region which needs to be
// repainted
QRect dirtyRect = QRect(_contentRect.left() + tLx ,
_contentRect.top() + tLy + _terminalFont->fontHeight() * y ,
_terminalFont->fontWidth() * columnsToUpdate ,
_terminalFont->fontHeight());
dirtyRegion |= dirtyRect;
}
// replace the line of characters in the old _image with the
// current line of the new _image
memcpy((void*)currentLine, (const void*)newLine, columnsToUpdate * sizeof(Character));
}
_lineProperties = newLineProperties;
// if the new _image is smaller than the previous _image, then ensure that the area
// outside the new _image is cleared
if (linesToUpdate < _usedLines) {
dirtyRegion |= QRect(_contentRect.left() + tLx ,
_contentRect.top() + tLy + _terminalFont->fontHeight() * linesToUpdate ,
_terminalFont->fontWidth() * _columns ,
_terminalFont->fontHeight() * (_usedLines - linesToUpdate));
}
_usedLines = linesToUpdate;
if (columnsToUpdate < _usedColumns) {
dirtyRegion |= QRect(_contentRect.left() + tLx + columnsToUpdate * _terminalFont->fontWidth(),
_contentRect.top() + tLy ,
_terminalFont->fontWidth() * (_usedColumns - columnsToUpdate) ,
_terminalFont->fontHeight() * _lines);
}
_usedColumns = columnsToUpdate;
dirtyRegion |= _inputMethodData.previousPreeditRect;
if ((_screenWindow->currentResultLine() != -1) && (_screenWindow->scrollCount() != 0)) {
// De-highlight previous result region
dirtyRegion |= _searchResultRect;
// Highlight new result region
dirtyRegion |= QRect(0, _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _terminalFont->fontHeight(),
_columns * _terminalFont->fontWidth(), _terminalFont->fontHeight());
}
if (_scrollBar->highlightScrolledLines().isEnabled()) {
dirtyRegion |= Q_EMIT highlightScrolledLinesRegion(dirtyRegion.isEmpty(), _scrollBar);
}
_screenWindow->resetScrollCount();
// update the parts of the display which have changed
update(dirtyRegion);
if (_allowBlinkingText && _hasTextBlinker && !_blinkTextTimer->isActive()) {
_blinkTextTimer->start();
}
if (!_hasTextBlinker && _blinkTextTimer->isActive()) {
_blinkTextTimer->stop();
_textBlinking = false;
}
delete[] dirtyMask;
#ifndef QT_NO_ACCESSIBILITY
QAccessibleEvent dataChangeEvent(this, QAccessible::VisibleDataChanged);
QAccessible::updateAccessibility(&dataChangeEvent);
QAccessibleTextCursorEvent cursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX());
QAccessible::updateAccessibility(&cursorEvent);
#endif
}
void TerminalDisplay::showResizeNotification()
{
if (_showTerminalSizeHint && isVisible()) {
if (_resizeWidget == nullptr) {
_resizeWidget = new QLabel(i18n("Size: XXX x XXX"), this);
_resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().boundingRect(i18n("Size: XXX x XXX")).width());
_resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height());
_resizeWidget->setAlignment(Qt::AlignCenter);
_resizeWidget->setStyleSheet(QStringLiteral("background-color:palette(window);border-style:solid;border-width:1px;border-color:palette(dark)"));
_resizeTimer = new QTimer(this);
_resizeTimer->setInterval(SIZE_HINT_DURATION);
_resizeTimer->setSingleShot(true);
connect(_resizeTimer, &QTimer::timeout, _resizeWidget, &QLabel::hide);
}
QString sizeStr = i18n("Size: %1 x %2", _columns, _lines);
_resizeWidget->setText(sizeStr);
_resizeWidget->move((width() - _resizeWidget->width()) / 2,
(height() - _resizeWidget->height()) / 2 + 20);
_resizeWidget->show();
_resizeTimer->start();
}
}
void TerminalDisplay::paintEvent(QPaintEvent* pe)
{
QPainter paint(this);
// Determine which characters should be repainted (1 region unit = 1 character)
QRegion dirtyImageRegion;
const QRegion region = pe->region() & contentsRect();
for (const QRect &rect : region) {
dirtyImageRegion += widgetToImage(rect);
Q_EMIT drawBackground(paint, rect, _terminalColor->backgroundColor(), true /* use opacity setting */);
}
if (_displayVerticalLine) {
const int fontWidth = _terminalFont->fontWidth();
const int x = (fontWidth/2) + (fontWidth * _displayVerticalLineAtChar);
const QColor lineColor = _terminalColor->foregroundColor();
paint.setPen(lineColor);
paint.drawLine(QPoint(x, 0), QPoint(x, height()));
}
// only turn on text anti-aliasing, never turn on normal antialiasing
// set https://bugreports.qt.io/browse/QTBUG-66036
paint.setRenderHint(QPainter::TextAntialiasing, _terminalFont->antialiasText());
for (const QRect &rect : qAsConst(dirtyImageRegion)) {
Q_EMIT drawContents(_image, paint, rect, false, _imageSize, _bidiEnabled, _lineProperties);
}
Q_EMIT drawCurrentResultRect(paint, _searchResultRect);
if (_scrollBar->highlightScrolledLines().isEnabled()) {
Q_EMIT highlightScrolledLines(paint, _scrollBar->highlightScrolledLines().isTimerActive(), _scrollBar->highlightScrolledLines().rect());
}
Q_EMIT drawInputMethodPreeditString(paint, preeditRect(), _inputMethodData, _image);
paintFilters(paint);
const bool drawDimmed = _dimWhenInactive && !hasFocus();
if (drawDimmed) {
const QColor dimColor(0, 0, 0, _dimValue);
for (const QRect &rect : region) {
paint.fillRect(rect, dimColor);
}
}
if (_drawOverlay) {
const auto y = _headerBar->isVisible() ? _headerBar->height() : 0;
const auto rect = _overlayEdge == Qt::LeftEdge ? QRect(0, y, width() / 2, height())
: _overlayEdge == Qt::TopEdge ? QRect(0, y, width(), height() / 2)
: _overlayEdge == Qt::RightEdge ? QRect(width() - width() / 2, y, width() / 2, height())
: QRect(0, height() - height() / 2, width(), height() / 2);
paint.setRenderHint(QPainter::Antialiasing);
paint.setPen(Qt::NoPen);
paint.setBrush(QColor(100,100,100, 127));
paint.drawRect(rect);
}
}
QPoint TerminalDisplay::cursorPosition() const
{
if (!_screenWindow.isNull()) {
return _screenWindow->cursorPosition();
} else {
return {0, 0};
}
}
bool TerminalDisplay::isCursorOnDisplay() const
{
return cursorPosition().x() < _columns &&
cursorPosition().y() < _lines;
}
FilterChain* TerminalDisplay::filterChain() const
{
return _filterChain;
}
void TerminalDisplay::paintFilters(QPainter& painter)
{
if (_filterUpdateRequired) {
return;
}
_filterChain->paint(this, painter);
}
QRect TerminalDisplay::imageToWidget(const QRect& imageArea) const
{
QRect result;
const int fontWidth = _terminalFont->fontWidth();
const int fontHeight = _terminalFont->fontHeight();
result.setLeft(_contentRect.left() + fontWidth * imageArea.left());
result.setTop(_contentRect.top() + fontHeight * imageArea.top());
result.setWidth(fontWidth * imageArea.width());
result.setHeight(fontHeight * imageArea.height());
return result;
}
QRect TerminalDisplay::widgetToImage(const QRect &widgetArea) const
{
QRect result;
const int fontWidth = _terminalFont->fontWidth();
const int fontHeight = _terminalFont->fontHeight();
result.setLeft( qBound(0, (widgetArea.left() - contentsRect().left() - _contentRect.left()) / fontWidth, _usedColumns - 1));
result.setTop( qBound(0, (widgetArea.top() - contentsRect().top() - _contentRect.top() ) / fontHeight, _usedLines - 1));
result.setRight( qBound(0, (widgetArea.right() - contentsRect().left() - _contentRect.left()) / fontWidth, _usedColumns - 1));
result.setBottom(qBound(0, (widgetArea.bottom() - contentsRect().top() - _contentRect.top() ) / fontHeight, _usedLines - 1));
return result;
}
/* ------------------------------------------------------------------------- */
/* */
/* Blinking Text & Cursor */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::setBlinkingCursorEnabled(bool blink)
{
_allowBlinkingCursor = blink;
if (blink && !_blinkCursorTimer->isActive()) {
_blinkCursorTimer->start();
}
if (!blink && _blinkCursorTimer->isActive()) {
_blinkCursorTimer->stop();
if (_cursorBlinking) {
// if cursor is blinking(hidden), blink it again to make it show
_cursorBlinking = false;
updateCursor();
}
Q_ASSERT(!_cursorBlinking);
}
}
void TerminalDisplay::setBlinkingTextEnabled(bool blink)
{
_allowBlinkingText = blink;
if (blink && !_blinkTextTimer->isActive()) {
_blinkTextTimer->start();
}
if (!blink && _blinkTextTimer->isActive()) {
_blinkTextTimer->stop();
_textBlinking = false;
}
}
void TerminalDisplay::focusOutEvent(QFocusEvent*)
{
// trigger a repaint of the cursor so that it is both:
//
// * visible (in case it was hidden during blinking)
// * drawn in a focused out state
_cursorBlinking = false;
updateCursor();
// suppress further cursor blinking
_blinkCursorTimer->stop();
Q_ASSERT(!_cursorBlinking);
// if text is blinking (hidden), blink it again to make it shown
if (_textBlinking) {
blinkTextEvent();
}
// suppress further text blinking
_blinkTextTimer->stop();
Q_ASSERT(!_textBlinking);
}
void TerminalDisplay::focusInEvent(QFocusEvent*)
{
if (_allowBlinkingCursor) {
_blinkCursorTimer->start();
}
updateCursor();
if (_allowBlinkingText && _hasTextBlinker) {
_blinkTextTimer->start();
}
}
void TerminalDisplay::blinkTextEvent()
{
Q_ASSERT(_allowBlinkingText);
_textBlinking = !_textBlinking;
// TODO: Optimize to only repaint the areas of the widget where there is
// blinking text rather than repainting the whole widget.
update();
}
void TerminalDisplay::blinkCursorEvent()
{
Q_ASSERT(_allowBlinkingCursor);
_cursorBlinking = !_cursorBlinking;
updateCursor();
}
void TerminalDisplay::updateCursor()
{
if (!isCursorOnDisplay()){
return;
}
const int cursorLocation = loc(cursorPosition().x(), cursorPosition().y());
Q_ASSERT(cursorLocation < _imageSize);
int charWidth = _image[cursorLocation].width();
QRect cursorRect = imageToWidget(QRect(cursorPosition(), QSize(charWidth, 1)));
update(cursorRect);
}
/* ------------------------------------------------------------------------- */
/* */
/* Geometry & Resizing */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::resizeEvent(QResizeEvent *event)
{
Q_UNUSED(event)
if (contentsRect().isValid()) {
// NOTE: This calls setTabText() in TabbedViewContainer::updateTitle(),
// which might update the widget size again. New resizeEvent
// won't be called, do not rely on new sizes before this call.
updateImageSize();
updateImage();
}
const auto scrollBarWidth = _scrollBar->scrollBarPosition() != Enum::ScrollBarHidden
? _scrollBar->width()
: 0;
const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0;
const auto x = width() - scrollBarWidth - _searchBar->width();
const auto y = headerHeight;
_searchBar->move(x, y);
}
void TerminalDisplay::propagateSize()
{
if (_image != nullptr) {
updateImageSize();
}
}
void TerminalDisplay::updateImageSize()
{
Character* oldImage = _image;
const int oldLines = _lines;
const int oldColumns = _columns;
makeImage();
if (oldImage != nullptr) {
// copy the old image to reduce flicker
int lines = qMin(oldLines, _lines);
int columns = qMin(oldColumns, _columns);
for (int line = 0; line < lines; line++) {
memcpy((void*)&_image[_columns * line],
(void*)&oldImage[oldColumns * line],
columns * sizeof(Character));
}
delete[] oldImage;
}
if (!_screenWindow.isNull()) {
_screenWindow->setWindowLines(_lines);
}
_resizing = (oldLines != _lines) || (oldColumns != _columns);
if (_resizing) {
showResizeNotification();
Q_EMIT changedContentSizeSignal(_contentRect.height(), _contentRect.width()); // expose resizeEvent
}
_resizing = false;
}
void TerminalDisplay::makeImage()
{
_wallpaper->load();
calcGeometry();
// confirm that array will be of non-zero size, since the painting code
// assumes a non-zero array length
Q_ASSERT(_lines > 0 && _columns > 0);
Q_ASSERT(_usedLines <= _lines && _usedColumns <= _columns);
_imageSize = _lines * _columns;
_image = new Character[_imageSize];
clearImage();
}
void TerminalDisplay::clearImage()
{
std::fill(_image, _image + _imageSize, Screen::DefaultChar);
}
void TerminalDisplay::calcGeometry()
{
const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0;
_scrollBar->resize(
_scrollBar->sizeHint().width(), // width
contentsRect().height() - headerHeight // height
);
_contentRect = contentsRect().adjusted(
_margin + (_scrollBar->highlightScrolledLines().isEnabled() ? _scrollBar->highlightScrolledLines().HIGHLIGHT_SCROLLED_LINES_WIDTH : 0),
_margin, -_margin - (_scrollBar->highlightScrolledLines().isEnabled() ? _scrollBar->highlightScrolledLines().HIGHLIGHT_SCROLLED_LINES_WIDTH : 0),
-_margin);
switch (_scrollBar->scrollBarPosition()) {
case Enum::ScrollBarHidden :
break;
case Enum::ScrollBarLeft :
_contentRect.setLeft(_contentRect.left() + _scrollBar->width());
_scrollBar->move(contentsRect().left(),
contentsRect().top() + headerHeight);
break;
case Enum::ScrollBarRight:
_contentRect.setRight(_contentRect.right() - _scrollBar->width());
_scrollBar->move(contentsRect().left() + contentsRect().width() - _scrollBar->width(),
contentsRect().top() + headerHeight);
break;
}
_contentRect.setTop(_contentRect.top() + headerHeight);
int fontWidth = _terminalFont->fontWidth();
// ensure that display is always at least one column wide
_columns = qMax(1, _contentRect.width() / fontWidth);
_usedColumns = qMin(_usedColumns, _columns);
// ensure that display is always at least one line high
_lines = qMax(1, _contentRect.height() / _terminalFont->fontHeight());
_usedLines = qMin(_usedLines, _lines);
if(_centerContents) {
QSize unusedPixels = _contentRect.size() - QSize(_columns * fontWidth, _lines * _terminalFont->fontHeight());
_contentRect.adjust(unusedPixels.width() / 2, unusedPixels.height() / 2, 0, 0);
}
}
// calculate the needed size, this must be synced with calcGeometry()
void TerminalDisplay::setSize(int columns, int lines)
{
const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->sizeHint().width();
const int horizontalMargin = _margin * 2;
const int verticalMargin = _margin * 2;
QSize newSize = QSize(horizontalMargin + scrollBarWidth + (columns * _terminalFont->fontWidth()),
verticalMargin + (lines * _terminalFont->fontHeight()));
if (newSize != size()) {
_size = newSize;
updateGeometry();
}
}
QSize TerminalDisplay::sizeHint() const
{
return _size;
}
//showEvent and hideEvent are reimplemented here so that it appears to other classes that the
//display has been resized when the display is hidden or shown.
//
//TODO: Perhaps it would be better to have separate signals for show and hide instead of using
//the same signal as the one for a content size change
void TerminalDisplay::showEvent(QShowEvent*)
{
propagateSize();
Q_EMIT changedContentSizeSignal(_contentRect.height(), _contentRect.width());
}
void TerminalDisplay::hideEvent(QHideEvent*)
{
Q_EMIT changedContentSizeSignal(_contentRect.height(), _contentRect.width());
}
void TerminalDisplay::setMargin(int margin)
{
if (margin < 0) {
margin = 0;
}
_margin = margin;
updateImageSize();
}
void TerminalDisplay::setCenterContents(bool enable)
{
_centerContents = enable;
calcGeometry();
update();
}
/* ------------------------------------------------------------------------- */
/* */
/* Mouse */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::mousePressEvent(QMouseEvent* ev)
{
if (!contentsRect().contains(ev->pos())) {
return;
}
if (!_screenWindow) {
return;
}
_screenWindow->screen()->setCurrentTerminalDisplay(this);
if (_possibleTripleClick && (ev->button() == Qt::LeftButton)) {
mouseTripleClickEvent(ev);
return;
}
// Ignore clicks on the message widget
if (_readOnlyMessageWidget != nullptr &&
_readOnlyMessageWidget->isVisible() && _readOnlyMessageWidget->frameGeometry().contains(ev->pos())) {
return;
}
if (_outputSuspendedMessageWidget != nullptr &&
_outputSuspendedMessageWidget->isVisible() && _outputSuspendedMessageWidget->frameGeometry().contains(ev->pos())) {
return;
}
auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !_usesMouseTracking);
QPoint pos = QPoint(charColumn, charLine);
processFilters();
_filterChain->mouseMoveEvent(this, ev, charLine, charColumn);
auto hotSpotClick = _filterChain->hotSpotAt(charLine, charColumn);
if (hotSpotClick && hotSpotClick->hasDragOperation() && ev->modifiers() & Qt::Modifier::ALT) {
hotSpotClick->startDrag();
return;
}
if (ev->button() == Qt::LeftButton) {
// request the software keyboard, if any
if (qApp->autoSipEnabled()) {
auto behavior = QStyle::RequestSoftwareInputPanel(
style()->styleHint(QStyle::SH_RequestSoftwareInputPanel));
if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) {
QEvent event(QEvent::RequestSoftwareInputPanel);
QApplication::sendEvent(this, &event);
}
}
if (!ev->modifiers()) {
_lineSelectionMode = false;
_wordSelectionMode = false;
}
// The user clicked inside selected text
bool selected = _screenWindow->isSelected(pos.x(), pos.y());
// Drag only when the Control key is held
if ((!_ctrlRequiredForDrag || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && selected) {
_dragInfo.state = diPending;
_dragInfo.start = ev->pos();
} else {
// No reason to ever start a drag event
_dragInfo.state = diNone;
_preserveLineBreaks = !(((ev->modifiers() & Qt::ControlModifier) != 0u) && !(ev->modifiers() & Qt::AltModifier));
_columnSelectionMode = ((ev->modifiers() & Qt::AltModifier) != 0u) && ((ev->modifiers() & Qt::ControlModifier) != 0u);
// There are a couple of use cases when selecting text :
// Normal buffer or Alternate buffer when not using Mouse Tracking:
// select text or extendSelection or columnSelection or columnSelection + extendSelection
//
// Alternate buffer when using Mouse Tracking and with Shift pressed:
// select text or columnSelection
if (!_usesMouseTracking &&
((ev->modifiers() == Qt::ShiftModifier) ||
(((ev->modifiers() & Qt::ShiftModifier) != 0u) && _columnSelectionMode))) {
extendSelection(ev->pos());
} else if ((!_usesMouseTracking && !((ev->modifiers() & Qt::ShiftModifier))) ||
(_usesMouseTracking && ((ev->modifiers() & Qt::ShiftModifier) != 0u))) {
_screenWindow->clearSelection();
pos.ry() += _scrollBar->value();
_iPntSel = _pntSel = pos;
_actSel = 1; // left mouse button pressed but nothing selected yet.
} else if (_usesMouseTracking && !_readOnly) {
Q_EMIT mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0);
}
}
} else if (ev->button() == Qt::MiddleButton) {
processMidButtonClick(ev);
} else if (ev->button() == Qt::RightButton) {
if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) {
Q_EMIT configureRequest(ev->pos());
} else {
if(!_readOnly) {
Q_EMIT mouseSignal(2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0);
}
}
}
}
QSharedPointer<HotSpot> TerminalDisplay::filterActions(const QPoint& position)
{
auto [charLine, charColumn] = getCharacterPosition(position, false);
return _filterChain->hotSpotAt(charLine, charColumn);
}
void TerminalDisplay::mouseMoveEvent(QMouseEvent* ev)
{
if (!hasFocus() && KonsoleSettings::focusFollowsMouse()) {
setFocus();
}
auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !_usesMouseTracking);
// Ignore mouse movements that don't change the character position,
// but don't ignore the ones generated by AutoScrollHandler (which
// allow to extend the selection by dragging the mouse outside the
// display).
if (charLine == _prevCharacterLine && charColumn == _prevCharacterColumn &&
contentsRect().contains(ev->pos())) {
return;
}
_prevCharacterLine = charLine;
_prevCharacterColumn = charColumn;
processFilters();
_filterChain->mouseMoveEvent(this, ev, charLine, charColumn);
// if the program running in the terminal is interested in Mouse Tracking
// events then emit a mouse movement signal, unless the shift key is
// being held down, which overrides this.
if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) {
if (!_readOnly) {
int button = 3;
if ((ev->buttons() & Qt::LeftButton) != 0u) {
button = 0;
}
if ((ev->buttons() & Qt::MiddleButton) != 0u) {
button = 1;
}
if ((ev->buttons() & Qt::RightButton) != 0u) {
button = 2;
}
Q_EMIT mouseSignal(button,
charColumn + 1,
charLine + 1 + _scrollBar->value() - _scrollBar->maximum(),
1);
}
return;
}
// for auto-hiding the cursor, we need mouseTracking
if (ev->buttons() == Qt::NoButton) {
return;
}
if (_dragInfo.state == diPending) {
// we had a mouse down, but haven't confirmed a drag yet
// if the mouse has moved sufficiently, we will confirm
const int distance = QApplication::startDragDistance();
if (ev->x() > _dragInfo.start.x() + distance || ev->x() < _dragInfo.start.x() - distance ||
ev->y() > _dragInfo.start.y() + distance || ev->y() < _dragInfo.start.y() - distance) {
// we've left the drag square, we can start a real drag operation now
_screenWindow->clearSelection();
doDrag();
}
return;
} else if (_dragInfo.state == diDragging) {
// this isn't technically needed because mouseMoveEvent is suppressed during
// Qt drag operations, replaced by dragMoveEvent
return;
}
if (_actSel == 0) {
return;
}
// don't extend selection while pasting
if ((ev->buttons() & Qt::MiddleButton) != 0u) {
return;
}
extendSelection(ev->pos());
}
void TerminalDisplay::leaveEvent(QEvent *ev)
{
// remove underline from an active link when cursor leaves the widget area,
// also restore regular mouse cursor shape
_filterChain->leaveEvent(this, ev);
}
void TerminalDisplay::extendSelection(const QPoint& position)
{
if (_screenWindow.isNull()) {
return;
}
if (_iPntSel.x() < 0 || _iPntSel.y() < 0 || _pntSel.x() < 0 || _pntSel.y() < 0) {
_iPntSel = _pntSel = position;
return;
}
//if ( !contentsRect().contains(ev->pos()) ) return;
const QPoint tL = contentsRect().topLeft();
const int tLx = tL.x();
const int tLy = tL.y();
const int scroll = _scrollBar->value();
// we're in the process of moving the mouse with the left button pressed
// the mouse cursor will kept caught within the bounds of the text in
// this widget.
int linesBeyondWidget = 0;
QRect textBounds(tLx + _contentRect.left(),
tLy + _contentRect.top(),
_usedColumns * _terminalFont->fontWidth() - 1,
_usedLines * _terminalFont->fontHeight() - 1);
QPoint pos = position;
// Adjust position within text area bounds.
const QPoint oldpos = pos;
pos.setX(qBound(textBounds.left(), pos.x(), textBounds.right()));
pos.setY(qBound(textBounds.top(), pos.y(), textBounds.bottom()));
if (oldpos.y() > textBounds.bottom()) {
linesBeyondWidget = (oldpos.y() - textBounds.bottom()) / _terminalFont->fontHeight();
_scrollBar->setValue(_scrollBar->value() + linesBeyondWidget + 1); // scrollforward
}
if (oldpos.y() < textBounds.top()) {
linesBeyondWidget = (textBounds.top() - oldpos.y()) / _terminalFont->fontHeight();
_scrollBar->setValue(_scrollBar->value() - linesBeyondWidget - 1); // history
}
auto [charLine, charColumn] = getCharacterPosition(pos, true);
QPoint here = QPoint(charColumn, charLine);
QPoint ohere;
QPoint iPntSelCorr = _iPntSel;
iPntSelCorr.ry() -= _scrollBar->value();
QPoint pntSelCorr = _pntSel;
pntSelCorr.ry() -= _scrollBar->value();
bool swapping = false;
if (_wordSelectionMode) {
// Extend to word boundaries
const bool left_not_right = (here.y() < iPntSelCorr.y() ||
(here.y() == iPntSelCorr.y() && here.x() < iPntSelCorr.x()));
const bool old_left_not_right = (pntSelCorr.y() < iPntSelCorr.y() ||
(pntSelCorr.y() == iPntSelCorr.y() && pntSelCorr.x() < iPntSelCorr.x()));
swapping = left_not_right != old_left_not_right;
// Find left (left_not_right ? from here : from start of word)
QPoint left = left_not_right ? here : iPntSelCorr;
// Find left (left_not_right ? from end of word : from here)
QPoint right = left_not_right ? iPntSelCorr : here;
if (left.y() < 0 || left.y() >= _lines || left.x() < 0 || left.x() >= _columns) {
left = pntSelCorr;
} else {
left = findWordStart(left);
}
if (right.y() < 0 || right.y() >= _lines || right.x() < 0 || right.x() >= _columns) {
right = pntSelCorr;
} else {
right = findWordEnd(right);
}
// Pick which is start (ohere) and which is extension (here)
if (left_not_right) {
here = left;
ohere = right;
} else {
here = right;
ohere = left;
}
ohere.rx()++;
}
if (_lineSelectionMode) {
// Extend to complete line
const bool above_not_below = (here.y() < iPntSelCorr.y());
if (above_not_below) {
ohere = findLineEnd(iPntSelCorr);
here = findLineStart(here);
} else {
ohere = findLineStart(iPntSelCorr);
here = findLineEnd(here);
}
swapping = !(_tripleSelBegin == ohere);
_tripleSelBegin = ohere;
ohere.rx()++;
}
int offset = 0;
if (!_wordSelectionMode && !_lineSelectionMode) {
const bool left_not_right = (here.y() < iPntSelCorr.y() ||
(here.y() == iPntSelCorr.y() && here.x() < iPntSelCorr.x()));
const bool old_left_not_right = (pntSelCorr.y() < iPntSelCorr.y() ||
(pntSelCorr.y() == iPntSelCorr.y() && pntSelCorr.x() < iPntSelCorr.x()));
swapping = left_not_right != old_left_not_right;
// Find left (left_not_right ? from here : from start)
const QPoint left = left_not_right ? here : iPntSelCorr;
// Find right (left_not_right ? from start : from here)
QPoint right = left_not_right ? iPntSelCorr : here;
// Pick which is start (ohere) and which is extension (here)
if (left_not_right) {
here = left;
ohere = right;
offset = 0;
} else {
here = right;
ohere = left;
offset = -1;
}
}
if ((here == pntSelCorr) && (scroll == _scrollBar->value())) {
return; // not moved
}
if (here == ohere) {
return; // It's not left, it's not right.
}
if (_actSel < 2 || swapping) {
if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) {
_screenWindow->setSelectionStart(ohere.x() , ohere.y() , true);
} else {
_screenWindow->setSelectionStart(ohere.x() - 1 - offset , ohere.y() , false);
}
}
_actSel = 2; // within selection
_pntSel = here;
_pntSel.ry() += _scrollBar->value();
if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) {
_screenWindow->setSelectionEnd(here.x() , here.y());
} else {
_screenWindow->setSelectionEnd(here.x() + offset , here.y());
}
}
void TerminalDisplay::mouseReleaseEvent(QMouseEvent* ev)
{
if (_screenWindow.isNull()) {
return;
}
auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !_usesMouseTracking);
if (ev->button() == Qt::LeftButton) {
if (_dragInfo.state == diPending) {
// We had a drag event pending but never confirmed. Kill selection
_screenWindow->clearSelection();
} else {
if (_actSel > 1) {
copyToX11Selection();
}
_actSel = 0;
//FIXME: emits a release event even if the mouse is
// outside the range. The procedure used in `mouseMoveEvent'
// applies here, too.
if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier) && !_readOnly) {
Q_EMIT mouseSignal(0,
charColumn + 1,
charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2);
}
}
_dragInfo.state = diNone;
}
if (_usesMouseTracking && !_readOnly &&
(ev->button() == Qt::RightButton || ev->button() == Qt::MiddleButton) &&
!(ev->modifiers() & Qt::ShiftModifier)) {
Q_EMIT mouseSignal(ev->button() == Qt::MiddleButton ? 1 : 2,
charColumn + 1,
charLine + 1 + _scrollBar->value() - _scrollBar->maximum() ,
2);
}
if (!_screenWindow->screen()->hasSelection()) {
_filterChain->mouseReleaseEvent(this, ev, charLine, charColumn);
}
}
QPair<int, int> TerminalDisplay::getCharacterPosition(const QPoint& widgetPoint, bool edge) const
{
// the column value returned can be equal to _usedColumns (when edge == true),
// which is the position just after the last character displayed in a line.
//
// this is required so that the user can select characters in the right-most
// column (or left-most for right-to-left input)
const int columnMax = edge ? _usedColumns : _usedColumns - 1;
const int xOffset = edge ? _terminalFont->fontWidth() / 2 : 0;
int line = qBound(0, (widgetPoint.y() - contentsRect().top() - _contentRect.top()) / _terminalFont->fontHeight(), _usedLines - 1);
bool doubleWidth = line < _lineProperties.count() && _lineProperties[line] & LINE_DOUBLEWIDTH;
int column = qBound(0, (widgetPoint.x() + xOffset - contentsRect().left() - _contentRect.left()) / _terminalFont->fontWidth() / (doubleWidth ? 2 : 1), columnMax);
return qMakePair(line, column);
}
void TerminalDisplay::setExpandedMode(bool expand)
{
_headerBar->setExpandedMode(expand);
}
void TerminalDisplay::processMidButtonClick(QMouseEvent* ev)
{
if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) {
const bool appendEnter = (ev->modifiers() & Qt::ControlModifier) != 0u;
if (_middleClickPasteMode == Enum::PasteFromX11Selection) {
pasteFromX11Selection(appendEnter);
} else if (_middleClickPasteMode == Enum::PasteFromClipboard) {
doPaste(terminalClipboard::pasteFromClipboard(), appendEnter);
} else {
Q_ASSERT(false);
}
} else {
if(!_readOnly) {
auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !_usesMouseTracking);
Q_EMIT mouseSignal(1, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0);
}
}
}
void TerminalDisplay::mouseDoubleClickEvent(QMouseEvent* ev)
{
// Yes, successive middle click can trigger this event
if (ev->button() == Qt::MiddleButton) {
processMidButtonClick(ev);
return;
}
if (_screenWindow.isNull()) {
return;
}
auto [charLine, charColumn] = getCharacterPosition(ev->pos(), !_usesMouseTracking);
QPoint pos(qMin(charColumn, _columns - 1), qMin(charLine, _lines - 1));
// pass on double click as two clicks.
if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) {
if(!_readOnly) {
// Send just _ONE_ click event, since the first click of the double click
// was already sent by the click handler
Q_EMIT mouseSignal(ev->button() == Qt::LeftButton ? 0 : 2, charColumn + 1,
charLine + 1 + _scrollBar->value() - _scrollBar->maximum(),
0);
}
return;
}
if (ev->button() != Qt::LeftButton) {
return;
}
_screenWindow->clearSelection();
_iPntSel = pos;
_iPntSel.ry() += _scrollBar->value();
_wordSelectionMode = true;
_actSel = 2; // within selection
// find word boundaries...
{
// find the start of the word
const QPoint bgnSel = findWordStart(pos);
const QPoint endSel = findWordEnd(pos);
_actSel = 2; // within selection
_screenWindow->setSelectionStart(bgnSel.x() , bgnSel.y() , false);
_screenWindow->setSelectionEnd(endSel.x() , endSel.y());
copyToX11Selection();
}
_possibleTripleClick = true;
QTimer::singleShot(QApplication::doubleClickInterval(), this, [this]() {
_possibleTripleClick = false;
});
}
void TerminalDisplay::wheelEvent(QWheelEvent* ev)
{
static QElapsedTimer enable_zoom_timer;
static bool enable_zoom = true;
// Only vertical scrolling is supported
if (qAbs(ev->angleDelta().y()) < qAbs(ev->angleDelta().x())) {
return;
}
if (enable_zoom_timer.isValid() && enable_zoom_timer.elapsed() > 1000) {
enable_zoom = true;
}
const int modifiers = ev->modifiers();
// ctrl+<wheel> for zooming, like in konqueror and firefox
if (((modifiers & Qt::ControlModifier) != 0u) && _mouseWheelZoom && enable_zoom) {
_scrollWheelState.addWheelEvent(ev);
int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE);
for (;steps > 0; --steps) {
// wheel-up for increasing font size
_terminalFont->increaseFontSize();
}
for (;steps < 0; ++steps) {
// wheel-down for decreasing font size
_terminalFont->decreaseFontSize();
}
return;
} else if (!_usesMouseTracking && (_scrollBar->maximum() > 0)) {
// If the program running in the terminal is not interested in Mouse
// Tracking events, send the event to the scrollbar if the slider
// has room to move
_scrollWheelState.addWheelEvent(ev);
_scrollBar->event(ev);
// Reapply scrollbar position since the scrollbar event handler
// sometimes makes the scrollbar visible when set to hidden.
// Don't call propagateSize and update, since nothing changed.
_scrollBar->applyScrollBarPosition(false);
Q_ASSERT(_sessionController != nullptr);
_sessionController->setSearchStartToWindowCurrentLine();
_scrollWheelState.clearAll();
} else if (!_readOnly) {
_scrollWheelState.addWheelEvent(ev);
Q_ASSERT(!_sessionController->session().isNull());
if(!_usesMouseTracking && !_sessionController->session()->isPrimaryScreen() && _scrollBar->alternateScrolling()) {
// Send simulated up / down key presses to the terminal program
// for the benefit of programs such as 'less' (which use the alternate screen)
// assume that each Up / Down key event will cause the terminal application
// to scroll by one line.
//
// to get a reasonable scrolling speed, scroll by one line for every 5 degrees
// of mouse wheel rotation. Mouse wheels typically move in steps of 15 degrees,
// giving a scroll of 3 lines
const int lines = _scrollWheelState.consumeSteps(static_cast<int>(_terminalFont->fontHeight() * qApp->devicePixelRatio()), ScrollState::degreesToAngle(5));
const int keyCode = lines > 0 ? Qt::Key_Up : Qt::Key_Down;
QKeyEvent keyEvent(QEvent::KeyPress, keyCode, Qt::NoModifier);
for (int i = 0; i < abs(lines); i++) {
_screenWindow->screen()->setCurrentTerminalDisplay(this);
Q_EMIT keyPressedSignal(&keyEvent);
}
} else if (_usesMouseTracking) {
// terminal program wants notification of mouse activity
auto [charLine, charColumn] = getCharacterPosition(ev->position().toPoint(), !_usesMouseTracking);
const int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE);
const int button = (steps > 0) ? 4 : 5;
for (int i = 0; i < abs(steps); ++i) {
Q_EMIT mouseSignal(button,
charColumn + 1,
charLine + 1 + _scrollBar->value() - _scrollBar->maximum() ,
0);
}
}
}
enable_zoom_timer.start();
enable_zoom = false;
}
void TerminalDisplay::viewScrolledByUser()
{
Q_ASSERT(_sessionController != nullptr);
_sessionController->setSearchStartToWindowCurrentLine();
}
/* Moving left/up from the line containing pnt, return the starting
offset point which the given line is continuously wrapped
(top left corner = 0,0; previous line not visible = 0,-1).
*/
QPoint TerminalDisplay::findLineStart(const QPoint &pnt)
{
const int visibleScreenLines = _lineProperties.size();
const int topVisibleLine = _screenWindow->currentLine();
Screen *screen = _screenWindow->screen();
int line = pnt.y();
int lineInHistory= line + topVisibleLine;
QVector<LineProperty> lineProperties = _lineProperties;
while (lineInHistory > 0) {
for (; line > 0; line--, lineInHistory--) {
// Does previous line wrap around?
if ((lineProperties[line - 1] & LINE_WRAPPED) == 0) {
return {0, lineInHistory - topVisibleLine};
}
}
if (lineInHistory < 1) {
break;
}
// _lineProperties is only for the visible screen, so grab new data
int newRegionStart = qMax(0, lineInHistory - visibleScreenLines);
lineProperties = screen->getLineProperties(newRegionStart, lineInHistory - 1);
line = lineInHistory - newRegionStart;
}
return {0, lineInHistory - topVisibleLine};
}
/* Moving right/down from the line containing pnt, return the ending
offset point which the given line is continuously wrapped.
*/
QPoint TerminalDisplay::findLineEnd(const QPoint &pnt)
{
const int visibleScreenLines = _lineProperties.size();
const int topVisibleLine = _screenWindow->currentLine();
const int maxY = _screenWindow->lineCount() - 1;
Screen *screen = _screenWindow->screen();
int line = pnt.y();
int lineInHistory= line + topVisibleLine;
QVector<LineProperty> lineProperties = _lineProperties;
while (lineInHistory < maxY) {
for (; line < lineProperties.count() && lineInHistory < maxY; line++, lineInHistory++) {
// Does current line wrap around?
if ((lineProperties[line] & LINE_WRAPPED) == 0) {
return {_columns - 1, lineInHistory - topVisibleLine};
}
}
line = 0;
lineProperties = screen->getLineProperties(lineInHistory, qMin(lineInHistory + visibleScreenLines, maxY));
}
return {_columns - 1, lineInHistory - topVisibleLine};
}
QPoint TerminalDisplay::findWordStart(const QPoint &pnt)
{
const int regSize = qMax(_screenWindow->windowLines(), 10);
const int firstVisibleLine = _screenWindow->currentLine();
Screen *screen = _screenWindow->screen();
Character *image = _image;
Character *tmp_image = nullptr;
int imgLine = pnt.y();
int x = pnt.x();
int y = imgLine + firstVisibleLine;
int imgLoc = loc(x, imgLine);
QVector<LineProperty> lineProperties = _lineProperties;
const QChar selClass = charClass(image[imgLoc]);
const int imageSize = regSize * _columns;
while (true) {
for (;;imgLoc--, x--) {
if (imgLoc < 1) {
// no more chars in this region
break;
}
if (x > 0) {
// has previous char on this line
if (charClass(image[imgLoc - 1]) == selClass) {
continue;
}
goto out;
} else if (imgLine > 0) {
// not the first line in the session
if ((lineProperties[imgLine - 1] & LINE_WRAPPED) != 0) {
// have continuation on prev line
if (charClass(image[imgLoc - 1]) == selClass) {
x = _columns;
imgLine--;
y--;
continue;
}
}
goto out;
} else if (y > 0) {
// want more data, but need to fetch new region
break;
} else {
goto out;
}
}
if (y <= 0) {
// No more data
goto out;
}
int newRegStart = qMax(0, y - regSize + 1);
lineProperties = screen->getLineProperties(newRegStart, y - 1);
imgLine = y - newRegStart;
delete[] tmp_image;
tmp_image = new Character[imageSize];
image = tmp_image;
screen->getImage(tmp_image, imageSize, newRegStart, y - 1);
imgLoc = loc(x, imgLine);
if (imgLoc < 1) {
// Reached the start of the session
break;
}
}
out:
delete[] tmp_image;
return {x, y - firstVisibleLine};
}
QPoint TerminalDisplay::findWordEnd(const QPoint &pnt)
{
const int regSize = qMax(_screenWindow->windowLines(), 10);
const int curLine = _screenWindow->currentLine();
int i = pnt.y();
int x = pnt.x();
int y = i + curLine;
int j = loc(x, i);
QVector<LineProperty> lineProperties = _lineProperties;
Screen *screen = _screenWindow->screen();
Character *image = _image;
Character *tmp_image = nullptr;
const QChar selClass = charClass(image[j]);
const int imageSize = regSize * _columns;
const int maxY = _screenWindow->lineCount() - 1;
const int maxX = _columns - 1;
while (true) {
const int lineCount = lineProperties.count();
for (;;j++, x++) {
if (x < maxX) {
if (charClass(image[j + 1]) == selClass &&
// A colon right before whitespace is never part of a word
! (image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) {
continue;
}
goto out;
} else if (i < lineCount - 1) {
if (((lineProperties[i] & LINE_WRAPPED) != 0) &&
charClass(image[j + 1]) == selClass &&
// A colon right before whitespace is never part of a word
! (image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) {
x = -1;
i++;
y++;
continue;
}
goto out;
} else if (y < maxY) {
if (i < lineCount && ((lineProperties[i] & LINE_WRAPPED) == 0)) {
goto out;
}
break;
} else {
goto out;
}
}
int newRegEnd = qMin(y + regSize - 1, maxY);
lineProperties = screen->getLineProperties(y, newRegEnd);
i = 0;
if (tmp_image == nullptr) {
tmp_image = new Character[imageSize];
image = tmp_image;
}
screen->getImage(tmp_image, imageSize, y, newRegEnd);
x--;
j = loc(x, i);
}
out:
y -= curLine;
// In word selection mode don't select @ (64) if at end of word.
if (((image[j].rendition & RE_EXTENDED_CHAR) == 0) &&
(QChar(image[j].character) == QLatin1Char('@')) &&
(y > pnt.y() || x > pnt.x())) {
if (x > 0) {
x--;
} else {
y--;
}
}
delete[] tmp_image;
return {x, y};
}
Screen::DecodingOptions TerminalDisplay::currentDecodingOptions()
{
Screen::DecodingOptions decodingOptions;
if (_preserveLineBreaks) {
decodingOptions |= Screen::PreserveLineBreaks;
}
if (_trimLeadingSpaces) {
decodingOptions |= Screen::TrimLeadingWhitespace;
}
if (_trimTrailingSpaces) {
decodingOptions |= Screen::TrimTrailingWhitespace;
}
return decodingOptions;
}
void TerminalDisplay::mouseTripleClickEvent(QMouseEvent* ev)
{
if (_screenWindow.isNull()) {
return;
}
auto [charLine, charColumn] = getCharacterPosition(ev->pos(), true);
selectLine(QPoint(charColumn, charLine),
_tripleClickMode == Enum::SelectWholeLine);
}
void TerminalDisplay::selectLine(QPoint pos, bool entireLine)
{
_iPntSel = pos;
_screenWindow->clearSelection();
_lineSelectionMode = true;
_wordSelectionMode = false;
_actSel = 2; // within selection
if (!entireLine) { // Select from cursor to end of line
_tripleSelBegin = findWordStart(_iPntSel);
_screenWindow->setSelectionStart(_tripleSelBegin.x(),
_tripleSelBegin.y() , false);
} else {
_tripleSelBegin = findLineStart(_iPntSel);
_screenWindow->setSelectionStart(0 , _tripleSelBegin.y() , false);
}
_iPntSel = findLineEnd(_iPntSel);
_screenWindow->setSelectionEnd(_iPntSel.x() , _iPntSel.y());
copyToX11Selection();
_iPntSel.ry() += _scrollBar->value();
}
void TerminalDisplay::selectCurrentLine()
{
if (_screenWindow.isNull()) {
return;
}
selectLine(cursorPosition(), true);
}
void TerminalDisplay::selectAll()
{
if (_screenWindow.isNull()) {
return;
}
_preserveLineBreaks = true;
_screenWindow->setSelectionByLineRange(0, _screenWindow->lineCount());
copyToX11Selection();
}
bool TerminalDisplay::focusNextPrevChild(bool next)
{
// for 'Tab', always disable focus switching among widgets
// for 'Shift+Tab', leave the decision to higher level
if (next) {
return false;
} else {
return QWidget::focusNextPrevChild(next);
}
}
QChar TerminalDisplay::charClass(const Character& ch) const
{
if ((ch.rendition & RE_EXTENDED_CHAR) != 0) {
ushort extendedCharLength = 0;
const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength);
if ((chars != nullptr) && extendedCharLength > 0) {
const QString s = QString::fromUcs4(chars, extendedCharLength);
if (_wordCharacters.contains(s, Qt::CaseInsensitive)) {
return QLatin1Char('a');
}
bool letterOrNumber = false;
for (int i = 0; !letterOrNumber && i < s.size(); ++i) {
letterOrNumber = s.at(i).isLetterOrNumber();
}
return letterOrNumber ? QLatin1Char('a') : s.at(0);
}
return 0;
} else {
const QChar qch(ch.character);
if (qch.isSpace()) {
return QLatin1Char(' ');
}
if (qch.isLetterOrNumber() || _wordCharacters.contains(qch, Qt::CaseInsensitive)) {
return QLatin1Char('a');
}
return qch;
}
}
void TerminalDisplay::setWordCharacters(const QString& wc)
{
_wordCharacters = wc;
}
void TerminalDisplay::setUsesMouseTracking(bool on)
{
_usesMouseTracking = on;
resetCursor();
}
void TerminalDisplay::resetCursor()
{
setCursor(_usesMouseTracking ? Qt::ArrowCursor : Qt::IBeamCursor);
}
bool TerminalDisplay::usesMouseTracking() const
{
return _usesMouseTracking;
}
void TerminalDisplay::setBracketedPasteMode(bool on)
{
_bracketedPasteMode = on;
}
bool TerminalDisplay::bracketedPasteMode() const
{
return _bracketedPasteMode;
}
/* ------------------------------------------------------------------------- */
/* */
/* Clipboard */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::doPaste(QString text, bool appendReturn)
{
if (_screenWindow.isNull()) {
return;
}
if (_readOnly) {
return;
}
if (text.length() > 8000) {
if (KMessageBox::warningContinueCancel(window(),
i18np("Are you sure you want to paste %1 character?",
"Are you sure you want to paste %1 characters?",
text.length()),
i18n("Confirm Paste"),
KStandardGuiItem::cont(),
KStandardGuiItem::cancel(),
QStringLiteral("ShowPasteHugeTextWarning")) == KMessageBox::Cancel) {
return;
}
}
auto unsafeCharacters = terminalClipboard::checkForUnsafeCharacters(text);
if (!unsafeCharacters.isEmpty()) {
int result = KMessageBox::warningYesNoCancelList(window(),
i18n("The text you're trying to paste contains hidden control characters, "
"do you want to filter them out?"),
unsafeCharacters,
i18nc("@title", "Confirm Paste"),
KGuiItem(i18nc("@action:button",
"Paste &without control characters"),
QStringLiteral("filter-symbolic")),
KGuiItem(i18nc("@action:button",
"&Paste everything"),
QStringLiteral("edit-paste")),
KGuiItem(i18nc("@action:button",
"&Cancel"),
QStringLiteral("dialog-cancel")),
QStringLiteral("ShowPasteUnprintableWarning")
);
switch(result){
case KMessageBox::Cancel:
return;
case KMessageBox::Yes: {
text = terminalClipboard::sanitizeString(text);
}
case KMessageBox::No:
break;
default:
break;
}
}
auto pasteString = terminalClipboard::prepareStringForPasting(text, appendReturn, bracketedPasteMode());
if (pasteString.has_value()) {
// perform paste by simulating keypress events
QKeyEvent e(QEvent::KeyPress, 0, Qt::NoModifier, text);
Q_EMIT keyPressedSignal(&e);
}
}
void TerminalDisplay::setAutoCopySelectedText(bool enabled)
{
_autoCopySelectedText = enabled;
}
void TerminalDisplay::setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode)
{
_middleClickPasteMode = mode;
}
void TerminalDisplay::setCopyTextAsHTML(bool enabled)
{
_copyTextAsHTML = enabled;
}
void TerminalDisplay::copyToX11Selection()
{
if (_screenWindow.isNull()) {
return;
}
const auto text = _screenWindow->selectedText(currentDecodingOptions());
const auto html = _screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml);
terminalClipboard::copyToX11Selection(text, html, _autoCopySelectedText);
}
void TerminalDisplay::copyToClipboard()
{
if (_screenWindow.isNull()) {
return;
}
const QString &text = _screenWindow->selectedText(currentDecodingOptions());
if (text.isEmpty()) {
return;
}
auto mimeData = new QMimeData;
mimeData->setText(text);
if (_copyTextAsHTML) {
mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml));
}
QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
}
void TerminalDisplay::pasteFromX11Selection(bool appendEnter)
{
if (QApplication::clipboard()->supportsSelection()) {
QString text = QApplication::clipboard()->text(QClipboard::Selection);
doPaste(text, appendEnter);
}
}
/* ------------------------------------------------------------------------- */
/* */
/* Input Method */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::inputMethodEvent(QInputMethodEvent* event)
{
if (!event->commitString().isEmpty()) {
QKeyEvent keyEvent(QEvent::KeyPress, 0, Qt::NoModifier, event->commitString());
Q_EMIT keyPressedSignal(&keyEvent);
}
if (!_readOnly && isCursorOnDisplay()) {
_inputMethodData.preeditString = event->preeditString();
update(preeditRect() | _inputMethodData.previousPreeditRect);
}
event->accept();
}
QVariant TerminalDisplay::inputMethodQuery(Qt::InputMethodQuery query) const
{
const QPoint cursorPos = cursorPosition();
switch (query) {
case Qt::ImCursorRectangle:
return imageToWidget(QRect(cursorPos.x(), cursorPos.y(), 1, 1));
case Qt::ImFont:
return font();
case Qt::ImCursorPosition:
// return the cursor position within the current line
return cursorPos.x();
case Qt::ImSurroundingText: {
// return the text from the current line
QString lineText;
QTextStream stream(&lineText);
PlainTextDecoder decoder;
decoder.begin(&stream);
if (isCursorOnDisplay()) {
decoder.decodeLine(&_image[loc(0, cursorPos.y())], _usedColumns, LINE_DEFAULT);
}
decoder.end();
return lineText;
}
case Qt::ImCurrentSelection:
return QString();
default:
break;
}
return QVariant();
}
QRect TerminalDisplay::preeditRect() const
{
const int preeditLength = Character::stringWidth(_inputMethodData.preeditString);
if (preeditLength == 0) {
return {};
}
const QRect stringRect(_contentRect.left() + _terminalFont->fontWidth() * cursorPosition().x(),
_contentRect.top() + _terminalFont->fontHeight() * cursorPosition().y(),
_terminalFont->fontWidth() * preeditLength,
_terminalFont->fontHeight());
return stringRect.intersected(_contentRect);
}
/* ------------------------------------------------------------------------- */
/* */
/* Keyboard */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::setFlowControlWarningEnabled(bool enable)
{
_flowControlWarningEnabled = enable;
// if the dialog is currently visible and the flow control warning has
// been disabled then hide the dialog
if (!enable) {
outputSuspended(false);
}
}
void TerminalDisplay::outputSuspended(bool suspended)
{
//create the label when this function is first called
if (_outputSuspendedMessageWidget == nullptr) {
//This label includes a link to an English language website
//describing the 'flow control' (Xon/Xoff) feature found in almost
//all terminal emulators.
//If there isn't a suitable article available in the target language the link
//can simply be removed.
_outputSuspendedMessageWidget = createMessageWidget(i18n("<qt>Output has been "
"<a href=\"https://en.wikipedia.org/wiki/Software_flow_control\">suspended</a>"
" by pressing Ctrl+S."
" Press <b>Ctrl+Q</b> to resume.</qt>"));
connect(_outputSuspendedMessageWidget, &KMessageWidget::linkActivated, this, [](const QString &url) {
QDesktopServices::openUrl(QUrl(url));
});
_outputSuspendedMessageWidget->setMessageType(KMessageWidget::Warning);
}
suspended ? _outputSuspendedMessageWidget->animatedShow() : _outputSuspendedMessageWidget->animatedHide();
}
void TerminalDisplay::dismissOutputSuspendedMessage()
{
outputSuspended(false);
}
KMessageWidget* TerminalDisplay::createMessageWidget(const QString &text) {
auto *widget = new KMessageWidget(text, this);
widget->setWordWrap(true);
widget->setFocusProxy(this);
widget->setCursor(Qt::ArrowCursor);
_verticalLayout->insertWidget(1, widget);
_searchBar->raise();
return widget;
}
void TerminalDisplay::updateReadOnlyState(bool readonly) {
if (_readOnly == readonly) {
return;
}
if (readonly) {
// Lazy create the readonly messagewidget
if (_readOnlyMessageWidget == nullptr) {
_readOnlyMessageWidget = createMessageWidget(i18n("This terminal is read-only."));
_readOnlyMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("object-locked")));
}
}
if (_readOnlyMessageWidget != nullptr) {
readonly ? _readOnlyMessageWidget->animatedShow() : _readOnlyMessageWidget->animatedHide();
}
_readOnly = readonly;
}
void TerminalDisplay::keyPressEvent(QKeyEvent* event)
{
{
auto [charLine, charColumn] = getCharacterPosition(mapFromGlobal(QCursor::pos()), !_usesMouseTracking);
// Don't process it if the filterchain handled it for us
if (_filterChain->keyPressEvent(this, event, charLine, charColumn)) {
return;
}
}
if (!_peekPrimaryShortcut.isEmpty() && _peekPrimaryShortcut.matches(QKeySequence(event->key() | event->modifiers()))) {
peekPrimaryRequested(true);
}
_screenWindow->screen()->setCurrentTerminalDisplay(this);
if (!_readOnly) {
_actSel = 0; // Key stroke implies a screen update, so TerminalDisplay won't
// know where the current selection is.
if (_allowBlinkingCursor) {
_blinkCursorTimer->start();
if (_cursorBlinking) {
// if cursor is blinking(hidden), blink it again to show it
blinkCursorEvent();
}
Q_ASSERT(!_cursorBlinking);
}
}
Q_EMIT keyPressedSignal(event);
#ifndef QT_NO_ACCESSIBILITY
if (!_readOnly) {
QAccessibleTextCursorEvent textCursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX());
QAccessible::updateAccessibility(&textCursorEvent);
}
#endif
event->accept();
}
void TerminalDisplay::keyReleaseEvent(QKeyEvent *event)
{
if (_readOnly) {
event->accept();
return;
}
{
auto [charLine, charColumn] = getCharacterPosition(mapFromGlobal(QCursor::pos()), !_usesMouseTracking);
_filterChain->keyReleaseEvent(this, event, charLine, charColumn);
}
peekPrimaryRequested(false);
QWidget::keyReleaseEvent(event);
}
bool TerminalDisplay::handleShortcutOverrideEvent(QKeyEvent* keyEvent)
{
const int modifiers = keyEvent->modifiers();
// When a possible shortcut combination is pressed,
// emit the overrideShortcutCheck() signal to allow the host
// to decide whether the terminal should override it or not.
if (modifiers != Qt::NoModifier) {
int modifierCount = 0;
unsigned int currentModifier = Qt::ShiftModifier;
while (currentModifier <= Qt::KeypadModifier) {
if ((modifiers & currentModifier) != 0u) {
modifierCount++;
}
currentModifier <<= 1;
}
if (modifierCount < 2) {
bool override = false;
Q_EMIT overrideShortcutCheck(keyEvent, override);
if (override) {
keyEvent->accept();
return true;
}
}
}
// Override any of the following shortcuts because
// they are needed by the terminal
int keyCode = keyEvent->key() | modifiers;
switch (keyCode) {
// list is taken from the QLineEdit::event() code
case Qt::Key_Tab:
case Qt::Key_Delete:
case Qt::Key_Home:
case Qt::Key_End:
case Qt::Key_Backspace:
case Qt::Key_Left:
case Qt::Key_Right:
case Qt::Key_Slash:
case Qt::Key_Period:
case Qt::Key_Space:
keyEvent->accept();
return true;
}
return false;
}
bool TerminalDisplay::event(QEvent* event)
{
bool eventHandled = false;
switch (event->type()) {
case QEvent::ShortcutOverride:
eventHandled = handleShortcutOverrideEvent(static_cast<QKeyEvent*>(event));
break;
case QEvent::FocusOut:
case QEvent::FocusIn:
if(_screenWindow != nullptr) {
// force a redraw on focusIn, fixes the
// black screen bug when the view is focused
// but doesn't redraws.
_screenWindow->notifyOutputChanged();
}
update();
break;
default:
break;
}
return eventHandled ? true : QWidget::event(event);
}
void TerminalDisplay::contextMenuEvent(QContextMenuEvent* event)
{
// the logic for the mouse case is within MousePressEvent()
if (event->reason() != QContextMenuEvent::Mouse) {
Q_EMIT configureRequest(mapFromGlobal(QCursor::pos()));
}
}
/* --------------------------------------------------------------------- */
/* */
/* Bell */
/* */
/* --------------------------------------------------------------------- */
void TerminalDisplay::bell(const QString& message)
{
_bell.bell(message, hasFocus());
}
/* --------------------------------------------------------------------- */
/* */
/* Drag & Drop */
/* */
/* --------------------------------------------------------------------- */
void TerminalDisplay::dragEnterEvent(QDragEnterEvent* event)
{
// text/plain alone is enough for KDE-apps
// text/uri-list is for supporting some non-KDE apps, such as thunar
// and pcmanfm
// That also applies in dropEvent()
const auto mimeData = event->mimeData();
if ((!_readOnly) && (mimeData != nullptr)
&& (mimeData->hasFormat(QStringLiteral("text/plain"))
|| mimeData->hasFormat(QStringLiteral("text/uri-list")))) {
event->acceptProposedAction();
}
}
namespace {
QString extractDroppedText(const QList<QUrl>& urls) {
QString dropText;
for (int i = 0 ; i < urls.count() ; i++) {
KIO::StatJob* job = KIO::mostLocalUrl(urls[i], KIO::HideProgressInfo);
if (!job->exec()) {
continue;
}
const QUrl url = job->mostLocalUrl();
// in future it may be useful to be able to insert file names with drag-and-drop
// without quoting them (this only affects paths with spaces in)
dropText += KShell::quoteArg(url.isLocalFile() ? url.path() : url.url());
// Each filename(including the last) should be followed by one space.
dropText += QLatin1Char(' ');
}
return dropText;
}
void setupCdToUrlAction(const QString& dropText, const QUrl& url, QList<QAction*>& additionalActions, TerminalDisplay *display) {
KIO::StatJob* job = KIO::mostLocalUrl(url, KIO::HideProgressInfo);
if (!job->exec()) {
return;
}
const QUrl localUrl = job->mostLocalUrl();
if (!localUrl.isLocalFile()) {
return;
}
const QFileInfo fileInfo(localUrl.path());
if (!fileInfo.isDir()) {
return;
}
QAction* cdAction = new QAction(i18n("Change &Directory To"), display);
const QByteArray triggerText = QString(QLatin1String(" cd ") + dropText + QLatin1Char('\n')).toLocal8Bit();
display->connect(cdAction, &QAction::triggered, display, [display, triggerText]{
Q_EMIT display->sendStringToEmu(triggerText);} );
additionalActions.append(cdAction);
}
}
void TerminalDisplay::dropEvent(QDropEvent* event)
{
if (_readOnly) {
event->accept();
return;
}
const auto mimeData = event->mimeData();
if (mimeData == nullptr) {
return;
}
auto urls = mimeData->urls();
QString dropText;
if (!urls.isEmpty()) {
dropText = extractDroppedText(urls);
// If our target is local we will open a popup - otherwise the fallback kicks
// in and the URLs will simply be pasted as text.
if (!_dropUrlsAsText && (_sessionController != nullptr) && _sessionController->url().isLocalFile()) {
// A standard popup with Copy, Move and Link as options -
// plus an additional Paste option.
QAction* pasteAction = new QAction(i18n("&Paste Location"), this);
connect(pasteAction, &QAction::triggered, this, [this, dropText]{ Q_EMIT sendStringToEmu(dropText.toLocal8Bit());} );
QList<QAction*> additionalActions;
additionalActions.append(pasteAction);
if (urls.count() == 1) {
setupCdToUrlAction(dropText, urls.at(0), additionalActions, this);
}
QUrl target = QUrl::fromLocalFile(_sessionController->currentDir());
KIO::DropJob* job = KIO::drop(event, target);
KJobWidgets::setWindow(job, this);
job->setApplicationActions(additionalActions);
return;
}
} else {
dropText = mimeData->text();
}
if (mimeData->hasFormat(QStringLiteral("text/plain")) ||
mimeData->hasFormat(QStringLiteral("text/uri-list"))) {
Q_EMIT sendStringToEmu(dropText.toLocal8Bit());
}
setFocus(Qt::MouseFocusReason);
}
void TerminalDisplay::doDrag()
{
const QMimeData *clipboardMimeData = QApplication::clipboard()->mimeData(QClipboard::Selection);
if (clipboardMimeData == nullptr) {
return;
}
auto mimeData = new QMimeData();
_dragInfo.state = diDragging;
_dragInfo.dragObject = new QDrag(this);
mimeData->setText(clipboardMimeData->text());
mimeData->setHtml(clipboardMimeData->html());
_dragInfo.dragObject->setMimeData(mimeData);
_dragInfo.dragObject->exec(Qt::CopyAction);
}
void TerminalDisplay::setSessionController(SessionController* controller)
{
_sessionController = controller;
connect(_sessionController, &Konsole::SessionController::pasteFromClipboardRequested, [this] {
doPaste(terminalClipboard::pasteFromClipboard(), false);
});
_headerBar->finishHeaderSetup(controller);
}
SessionController* TerminalDisplay::sessionController()
{
return _sessionController;
}
Session::Ptr TerminalDisplay::session() const
{
return _sessionController->session();
}
IncrementalSearchBar *TerminalDisplay::searchBar() const
{
return _searchBar;
}
void TerminalDisplay::applyProfile(const Profile::Ptr &profile)
{
// load color scheme
_colorScheme = ViewManager::colorSchemeForProfile(profile);
_terminalColor->applyProfile(profile, _colorScheme, randomSeed());
setWallpaper(_colorScheme->wallpaper());
// load font
_terminalFont->applyProfile(profile);
// set scroll-bar position
_scrollBar->setScrollBarPosition(Enum::ScrollBarPositionEnum(profile->property<int>(Profile::ScrollBarPosition)));
_scrollBar->setScrollFullPage(profile->property<bool>(Profile::ScrollFullPage));
// show hint about terminal size after resizing
_showTerminalSizeHint = profile->showTerminalSizeHint();
_dimWhenInactive = profile->dimWhenInactive();
// terminal features
setBlinkingCursorEnabled(profile->blinkingCursorEnabled());
setBlinkingTextEnabled(profile->blinkingTextEnabled());
_tripleClickMode = Enum::TripleClickModeEnum(profile->property<int>(Profile::TripleClickMode));
setAutoCopySelectedText(profile->autoCopySelectedText());
_ctrlRequiredForDrag = profile->property<bool>(Profile::CtrlRequiredForDrag);
_dropUrlsAsText = profile->property<bool>(Profile::DropUrlsAsText);
_bidiEnabled = profile->bidiRenderingEnabled();
_trimLeadingSpaces = profile->property<bool>(Profile::TrimLeadingSpacesInSelectedText);
_trimTrailingSpaces = profile->property<bool>(Profile::TrimTrailingSpacesInSelectedText);
_openLinksByDirectClick = profile->property<bool>(Profile::OpenLinksByDirectClickEnabled);
setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum(profile->property<int>(Profile::MiddleClickPasteMode)));
setCopyTextAsHTML(profile->property<bool>(Profile::CopyTextAsHTML));
// highlight lines scrolled into view (must be applied before margin/center)
_scrollBar->setHighlightScrolledLines(profile->property<bool>(Profile::HighlightScrolledLines));
// reflow lines when terminal resizes
//_screenWindow->screen()->setReflow(profile->property<bool>(Profile::ReflowLines));
// margin/center
setMargin(profile->property<int>(Profile::TerminalMargin));
setCenterContents(profile->property<bool>(Profile::TerminalCenter));
// cursor shape
setKeyboardCursorShape(Enum::CursorShapeEnum(profile->property<int>(Profile::CursorShape)));
// word characters
setWordCharacters(profile->wordCharacters());
// bell mode
_bell.setBellMode(Enum::BellModeEnum(profile->property<int>(Profile::BellMode)));
// mouse wheel zoom
_mouseWheelZoom = profile->mouseWheelZoomEnabled();
_displayVerticalLine = profile->verticalLine();
_displayVerticalLineAtChar = profile->verticalLineAtChar();
_scrollBar->setAlternateScrolling(profile->property<bool>(Profile::AlternateScrolling));
_dimValue = profile->dimValue();
_filterChain->setUrlHintsModifiers(Qt::KeyboardModifiers(profile->property<int>(Profile::UrlHintsModifiers)));
_filterChain->setReverseUrlHints(profile->property<bool>(Profile::ReverseUrlHints));
_peekPrimaryShortcut = profile->peekPrimaryKeySequence();
}
void TerminalDisplay::printScreen()
{
auto lprintContent = [this](QPainter &painter, bool friendly) {
QPoint columnLines(_usedLines, _usedColumns);
auto lfontget = [this]() { return _terminalFont->getVTFont(); };
auto lfontset = [this](const QFont &f) { _terminalFont->setVTFont(f); };
_printManager->printContent(painter, friendly, columnLines, lfontget, lfontset);
};
_printManager->printRequest(lprintContent, this);
}
Character TerminalDisplay::getCursorCharacter(int column, int line)
{
return _image[loc(column, line)];
}
int TerminalDisplay::selectionState() const
{
return _actSel;
}