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.
 
 
 
 
 

3827 lines
129 KiB

/*
This file is part of Konsole, a terminal emulator for KDE.
Copyright 2006-2008 by Robert Knight <robertknight@gmail.com>
Copyright 1997,1998 by Lars Doelle <lars.doelle@on-line.de>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA.
*/
// Own
#include "TerminalDisplay.h"
// Config
#include <config-konsole.h>
// Qt
#include <QApplication>
#include <QClipboard>
#include <QKeyEvent>
#include <QEvent>
#include <QFileInfo>
#include <QVBoxLayout>
#include <QAction>
#include <QFontDatabase>
#include <QLabel>
#include <QMimeData>
#include <QPainter>
#include <QPixmap>
#include <QScrollBar>
#include <QStyle>
#include <QTimer>
#include <QDrag>
#include <QDesktopServices>
#include <QAccessible>
// 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 "Filter.h"
#include "konsoledebug.h"
#include "konsole_wcwidth.h"
#include "TerminalCharacterDecoder.h"
#include "Screen.h"
#include "LineFont.h"
#include "SessionController.h"
#include "ExtendedCharTable.h"
#include "TerminalDisplayAccessible.h"
#include "SessionManager.h"
#include "Session.h"
#include "WindowSystemInfo.h"
using namespace Konsole;
#ifndef loc
#define loc(X,Y) ((Y)*_columns+(X))
#endif
#define REPCHAR "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
"abcdefgjijklmnopqrstuvwxyz" \
"0123456789./+@"
// we use this to force QPainter to display text in LTR mode
// more information can be found in: http://unicode.org/reports/tr9/
const QChar LTR_OVERRIDE_CHAR(0x202D);
/* ------------------------------------------------------------------------- */
/* */
/* 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
*/
ScreenWindow* TerminalDisplay::screenWindow() const
{
return _screenWindow;
}
void TerminalDisplay::setScreenWindow(ScreenWindow* window)
{
// disconnect existing screen window if any
if (_screenWindow != nullptr) {
disconnect(_screenWindow , nullptr , this , nullptr);
}
_screenWindow = window;
if (_screenWindow != nullptr) {
connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateLineProperties);
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::scrolled, this, [this]() {
_filterUpdateRequired = true;
});
_screenWindow->setWindowLines(_lines);
}
}
const ColorEntry* TerminalDisplay::colorTable() const
{
return _colorTable;
}
void TerminalDisplay::updateScrollBarPalette()
{
QColor backgroundColor = _colorTable[DEFAULT_BACK_COLOR];
backgroundColor.setAlphaF(_opacity);
QPalette p = palette();
p.setColor(QPalette::Window, backgroundColor);
//this is a workaround to add some readability to old themes like Fusion
//changing the light value for button a bit makes themes like fusion, windows and oxygen way more readable and pleasing
QColor buttonColor;
buttonColor.setHsvF(backgroundColor.hueF(), backgroundColor.saturationF(), backgroundColor.valueF() + (backgroundColor.valueF() < 0.5 ? 0.2 : -0.2));
p.setColor(QPalette::Button, buttonColor);
p.setColor(QPalette::WindowText, _colorTable[DEFAULT_FORE_COLOR]);
p.setColor(QPalette::ButtonText, _colorTable[DEFAULT_FORE_COLOR]);
_scrollBar->setPalette(p);
}
void TerminalDisplay::setBackgroundColor(const QColor& color)
{
_colorTable[DEFAULT_BACK_COLOR] = color;
QPalette p = palette();
p.setColor(backgroundRole(), color);
setPalette(p);
updateScrollBarPalette();
update();
}
QColor TerminalDisplay::getBackgroundColor() const
{
QPalette p = palette();
return p.color(backgroundRole());
}
void TerminalDisplay::setForegroundColor(const QColor& color)
{
_colorTable[DEFAULT_FORE_COLOR] = color;
updateScrollBarPalette();
update();
}
void TerminalDisplay::setColorTable(const ColorEntry table[])
{
for (int i = 0; i < TABLE_COLORS; i++) {
_colorTable[i] = table[i];
}
setBackgroundColor(_colorTable[DEFAULT_BACK_COLOR]);
}
/* ------------------------------------------------------------------------- */
/* */
/* Font */
/* */
/* ------------------------------------------------------------------------- */
static inline bool isLineCharString(const QString& string)
{
if (string.length() == 0) {
return false;
}
return isSupportedLineChar(string.at(0).unicode());
}
void TerminalDisplay::fontChange(const QFont&)
{
QFontMetrics fm(font());
_fontHeight = fm.height() + _lineSpacing;
// waba TerminalDisplay 1.123:
// "Base character width on widest ASCII character. This prevents too wide
// characters in the presence of double wide (e.g. Japanese) characters."
// Get the width from representative normal width characters
_fontWidth = qRound((static_cast<double>(fm.width(QStringLiteral(REPCHAR))) / static_cast<double>(qstrlen(REPCHAR))));
_fixedFont = true;
const int fw = fm.width(QLatin1Char(REPCHAR[0]));
for (unsigned int i = 1; i < qstrlen(REPCHAR); i++) {
if (fw != fm.width(QLatin1Char(REPCHAR[i]))) {
_fixedFont = false;
break;
}
}
if (_fontWidth < 1) {
_fontWidth = 1;
}
_fontAscent = fm.ascent();
emit changedFontMetricSignal(_fontHeight, _fontWidth);
propagateSize();
update();
}
void TerminalDisplay::setVTFont(const QFont& f)
{
QFont newFont(f);
QFontMetrics fontMetrics(newFont);
// This check seems extreme and semi-random
if ((fontMetrics.height() > height()) || (fontMetrics.maxWidth() > width())) {
return;
}
// hint that text should be drawn without anti-aliasing.
// depending on the user's font configuration, this may not be respected
if (!_antialiasText) {
newFont.setStyleStrategy(QFont::StyleStrategy(newFont.styleStrategy() | QFont::NoAntialias));
}
// experimental optimization. Konsole assumes that the terminal is using a
// mono-spaced font, in which case kerning information should have an effect.
// Disabling kerning saves some computation when rendering text.
newFont.setKerning(false);
// Konsole cannot handle non-integer font metrics
newFont.setStyleStrategy(QFont::StyleStrategy(newFont.styleStrategy() | QFont::ForceIntegerMetrics));
QFontInfo fontInfo(newFont);
// if (!fontInfo.fixedPitch()) {
// qWarning() << "Using a variable-width font - this might cause display problems";
// }
// QFontInfo::fixedPitch() appears to not match QFont::fixedPitch()
// related? https://bugreports.qt.io/browse/QTBUG-34082
if (!fontInfo.exactMatch()) {
const QChar comma(QLatin1Char(','));
QString nonMatching = fontInfo.family() % comma %
QString::number(fontInfo.pointSizeF()) % comma %
QString::number(fontInfo.pixelSize()) % comma %
QString::number(static_cast<int>(fontInfo.styleHint())) % comma %
QString::number(fontInfo.weight()) % comma %
QString::number(static_cast<int>(fontInfo.style())) % comma %
QString::number(static_cast<int>(fontInfo.underline())) % comma %
QString::number(static_cast<int>(fontInfo.strikeOut())) % comma %
QString::number(static_cast<int>(fontInfo.fixedPitch())) % comma %
QString::number(static_cast<int>(fontInfo.rawMode()));
qCDebug(KonsoleDebug) << "The font to use in the terminal can not be matched exactly on your system.";
qCDebug(KonsoleDebug)<<" Selected: "<<newFont.toString();
qCDebug(KonsoleDebug)<<" System : "<<nonMatching;
}
QWidget::setFont(newFont);
fontChange(newFont);
}
void TerminalDisplay::setFont(const QFont &)
{
// ignore font change request if not coming from konsole itself
}
void TerminalDisplay::increaseFontSize()
{
QFont font = getVTFont();
font.setPointSizeF(font.pointSizeF() + 1);
setVTFont(font);
}
void TerminalDisplay::decreaseFontSize()
{
const qreal MinimumFontSize = 6;
QFont font = getVTFont();
font.setPointSizeF(qMax(font.pointSizeF() - 1, MinimumFontSize));
setVTFont(font);
}
uint TerminalDisplay::lineSpacing() const
{
return _lineSpacing;
}
void TerminalDisplay::setLineSpacing(uint i)
{
_lineSpacing = i;
setVTFont(font()); // Trigger an update.
}
/* ------------------------------------------------------------------------- */
/* */
/* 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 (TerminalDisplay *display = qobject_cast<TerminalDisplay*>(object)) {
return new TerminalDisplayAccessible(display);
}
return nullptr;
}
#endif
}
/* ------------------------------------------------------------------------- */
/* */
/* Constructor / Destructor */
/* */
/* ------------------------------------------------------------------------- */
TerminalDisplay::TerminalDisplay(QWidget* parent)
: QWidget(parent)
, _screenWindow(nullptr)
, _bellMasked(false)
, _verticalLayout(new QVBoxLayout(this))
, _fontHeight(1)
, _fontWidth(1)
, _fontAscent(1)
, _boldIntense(true)
, _lines(1)
, _columns(1)
, _usedLines(1)
, _usedColumns(1)
, _image(nullptr)
, _randomSeed(0)
, _resizing(false)
, _showTerminalSizeHint(true)
, _bidiEnabled(false)
, _actSel(0)
, _wordSelectionMode(false)
, _lineSelectionMode(false)
, _preserveLineBreaks(false)
, _columnSelectionMode(false)
, _autoCopySelectedText(false)
, _copyTextAsHTML(true)
, _middleClickPasteMode(Enum::PasteFromX11Selection)
, _scrollbarLocation(Enum::ScrollBarRight)
, _scrollFullPage(false)
, _wordCharacters(QStringLiteral(":@-./_~"))
, _bellMode(Enum::NotifyBell)
, _allowBlinkingText(true)
, _allowBlinkingCursor(false)
, _textBlinking(false)
, _cursorBlinking(false)
, _hasTextBlinker(false)
, _urlHintsModifiers(Qt::NoModifier)
, _showUrlHint(false)
, _openLinksByDirectClick(false)
, _ctrlRequiredForDrag(true)
, _tripleClickMode(Enum::SelectWholeLine)
, _possibleTripleClick(false)
, _resizeWidget(nullptr)
, _resizeTimer(nullptr)
, _flowControlWarningEnabled(false)
, _outputSuspendedMessageWidget(nullptr)
, _lineSpacing(0)
, _blendColor(qRgba(0, 0, 0, 0xff))
, _filterChain(new TerminalImageFilterChain())
, _filterUpdateRequired(true)
, _cursorShape(Enum::BlockCursor)
, _antialiasText(true)
, _useFontLineCharacters(false)
, _printerFriendly(false)
, _sessionController(nullptr)
, _trimLeadingSpaces(false)
, _trimTrailingSpaces(false)
, _margin(1)
, _centerContents(false)
, _readOnlyMessageWidget(nullptr)
, _opacity(1.0)
{
// 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 QScrollBar(this);
_scrollBar->setAutoFillBackground(false);
// set the scroll bar's slider to occupy the whole area of the scroll bar initially
setScroll(0, 0);
_scrollBar->setCursor(Qt::ArrowCursor);
connect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged);
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);
setUsesMouse(true);
setBracketedPasteMode(false);
setColorTable(ColorScheme::defaultTable);
// 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->addStretch();
_verticalLayout->setSpacing(0);
setLayout(_verticalLayout);
// Take the scrollbar into account and add a margin to the layout. Without the timer the scrollbar width
// is garbage.
QTimer::singleShot(0, this, [this]() {
const int scrollBarWidth = _scrollBar->isVisible() ? geometry().intersected(_scrollBar->geometry()).width() : 0;
_verticalLayout->setContentsMargins(0, 0, scrollBarWidth, 0);
});
new AutoScrollHandler(this);
#ifndef QT_NO_ACCESSIBILITY
QAccessible::installFactory(Konsole::accessibleInterfaceFactory);
#endif
}
TerminalDisplay::~TerminalDisplay()
{
disconnect(_blinkTextTimer);
disconnect(_blinkCursorTimer);
delete[] _image;
delete _filterChain;
delete _readOnlyMessageWidget;
delete _outputSuspendedMessageWidget;
_readOnlyMessageWidget = nullptr;
_outputSuspendedMessageWidget = nullptr;
}
/* ------------------------------------------------------------------------- */
/* */
/* Display Operations */
/* */
/* ------------------------------------------------------------------------- */
/**
A table for emulating the simple (single width) unicode drawing chars.
It represents the 250x - 257x glyphs. If it's zero, we can't use it.
if it's not, it's encoded as follows: imagine a 5x5 grid where the points are numbered
0 to 24 left to top, top to bottom. Each point is represented by the corresponding bit.
Then, the pixels basically have the following interpretation:
_|||_
-...-
-...-
-...-
_|||_
where _ = none
| = vertical line.
- = horizontal line.
*/
enum LineEncode {
TopL = (1 << 1),
TopC = (1 << 2),
TopR = (1 << 3),
LeftT = (1 << 5),
Int11 = (1 << 6),
Int12 = (1 << 7),
Int13 = (1 << 8),
RightT = (1 << 9),
LeftC = (1 << 10),
Int21 = (1 << 11),
Int22 = (1 << 12),
Int23 = (1 << 13),
RightC = (1 << 14),
LeftB = (1 << 15),
Int31 = (1 << 16),
Int32 = (1 << 17),
Int33 = (1 << 18),
RightB = (1 << 19),
BotL = (1 << 21),
BotC = (1 << 22),
BotR = (1 << 23)
};
static void drawLineChar(QPainter& paint, int x, int y, int w, int h, uchar code)
{
//Calculate cell midpoints, end points.
const int cx = x + w / 2;
const int cy = y + h / 2;
const int ex = x + w - 1;
const int ey = y + h - 1;
const quint32 toDraw = LineChars[code];
//Top _lines:
if ((toDraw & TopL) != 0u) {
paint.drawLine(cx - 1, y, cx - 1, cy - 2);
}
if ((toDraw & TopC) != 0u) {
paint.drawLine(cx, y, cx, cy - 2);
}
if ((toDraw & TopR) != 0u) {
paint.drawLine(cx + 1, y, cx + 1, cy - 2);
}
//Bot _lines:
if ((toDraw & BotL) != 0u) {
paint.drawLine(cx - 1, cy + 2, cx - 1, ey);
}
if ((toDraw & BotC) != 0u) {
paint.drawLine(cx, cy + 2, cx, ey);
}
if ((toDraw & BotR) != 0u) {
paint.drawLine(cx + 1, cy + 2, cx + 1, ey);
}
//Left _lines:
if ((toDraw & LeftT) != 0u) {
paint.drawLine(x, cy - 1, cx - 2, cy - 1);
}
if ((toDraw & LeftC) != 0u) {
paint.drawLine(x, cy, cx - 2, cy);
}
if ((toDraw & LeftB) != 0u) {
paint.drawLine(x, cy + 1, cx - 2, cy + 1);
}
//Right _lines:
if ((toDraw & RightT) != 0u) {
paint.drawLine(cx + 2, cy - 1, ex, cy - 1);
}
if ((toDraw & RightC) != 0u) {
paint.drawLine(cx + 2, cy, ex, cy);
}
if ((toDraw & RightB) != 0u) {
paint.drawLine(cx + 2, cy + 1, ex, cy + 1);
}
//Intersection points.
if ((toDraw & Int11) != 0u) {
paint.drawPoint(cx - 1, cy - 1);
}
if ((toDraw & Int12) != 0u) {
paint.drawPoint(cx, cy - 1);
}
if ((toDraw & Int13) != 0u) {
paint.drawPoint(cx + 1, cy - 1);
}
if ((toDraw & Int21) != 0u) {
paint.drawPoint(cx - 1, cy);
}
if ((toDraw & Int22) != 0u) {
paint.drawPoint(cx, cy);
}
if ((toDraw & Int23) != 0u) {
paint.drawPoint(cx + 1, cy);
}
if ((toDraw & Int31) != 0u) {
paint.drawPoint(cx - 1, cy + 1);
}
if ((toDraw & Int32) != 0u) {
paint.drawPoint(cx, cy + 1);
}
if ((toDraw & Int33) != 0u) {
paint.drawPoint(cx + 1, cy + 1);
}
}
static void drawOtherChar(QPainter& paint, int x, int y, int w, int h, uchar code)
{
//Calculate cell midpoints, end points.
const int cx = x + w / 2;
const int cy = y + h / 2;
const int ex = x + w - 1;
const int ey = y + h - 1;
// Double dashes
if (0x4C <= code && code <= 0x4F) {
const int xHalfGap = qMax(w / 15, 1);
const int yHalfGap = qMax(h / 15, 1);
switch (code) {
case 0x4D: // BOX DRAWINGS HEAVY DOUBLE DASH HORIZONTAL
paint.drawLine(x, cy - 1, cx - xHalfGap - 1, cy - 1);
paint.drawLine(x, cy + 1, cx - xHalfGap - 1, cy + 1);
paint.drawLine(cx + xHalfGap, cy - 1, ex, cy - 1);
paint.drawLine(cx + xHalfGap, cy + 1, ex, cy + 1);
// No break!
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
Q_FALLTHROUGH();
#endif
case 0x4C: // BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL
paint.drawLine(x, cy, cx - xHalfGap - 1, cy);
paint.drawLine(cx + xHalfGap, cy, ex, cy);
break;
case 0x4F: // BOX DRAWINGS HEAVY DOUBLE DASH VERTICAL
paint.drawLine(cx - 1, y, cx - 1, cy - yHalfGap - 1);
paint.drawLine(cx + 1, y, cx + 1, cy - yHalfGap - 1);
paint.drawLine(cx - 1, cy + yHalfGap, cx - 1, ey);
paint.drawLine(cx + 1, cy + yHalfGap, cx + 1, ey);
// No break!
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
Q_FALLTHROUGH();
#endif
case 0x4E: // BOX DRAWINGS LIGHT DOUBLE DASH VERTICAL
paint.drawLine(cx, y, cx, cy - yHalfGap - 1);
paint.drawLine(cx, cy + yHalfGap, cx, ey);
break;
}
}
// Rounded corner characters
else if (0x6D <= code && code <= 0x70) {
const int r = w * 3 / 8;
const int d = 2 * r;
switch (code) {
case 0x6D: // BOX DRAWINGS LIGHT ARC DOWN AND RIGHT
paint.drawLine(cx, cy + r, cx, ey);
paint.drawLine(cx + r, cy, ex, cy);
paint.drawArc(cx, cy, d, d, 90 * 16, 90 * 16);
break;
case 0x6E: // BOX DRAWINGS LIGHT ARC DOWN AND LEFT
paint.drawLine(cx, cy + r, cx, ey);
paint.drawLine(x, cy, cx - r, cy);
paint.drawArc(cx - d, cy, d, d, 0 * 16, 90 * 16);
break;
case 0x6F: // BOX DRAWINGS LIGHT ARC UP AND LEFT
paint.drawLine(cx, y, cx, cy - r);
paint.drawLine(x, cy, cx - r, cy);
paint.drawArc(cx - d, cy - d, d, d, 270 * 16, 90 * 16);
break;
case 0x70: // BOX DRAWINGS LIGHT ARC UP AND RIGHT
paint.drawLine(cx, y, cx, cy - r);
paint.drawLine(cx + r, cy, ex, cy);
paint.drawArc(cx, cy - d, d, d, 180 * 16, 90 * 16);
break;
}
}
// Diagonals
else if (0x71 <= code && code <= 0x73) {
switch (code) {
case 0x71: // BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT
paint.drawLine(ex, y, x, ey);
break;
case 0x72: // BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT
paint.drawLine(x, y, ex, ey);
break;
case 0x73: // BOX DRAWINGS LIGHT DIAGONAL CROSS
paint.drawLine(ex, y, x, ey);
paint.drawLine(x, y, ex, ey);
break;
}
}
}
void TerminalDisplay::drawLineCharString(QPainter& painter, int x, int y, const QString& str,
const Character* attributes)
{
const QPen& originalPen = painter.pen();
if (((attributes->rendition & RE_BOLD) != 0) && _boldIntense) {
QPen boldPen(originalPen);
boldPen.setWidth(3);
painter.setPen(boldPen);
}
for (int i = 0 ; i < str.length(); i++) {
const uchar code = str[i].cell();
if (LineChars[code] != 0u) {
drawLineChar(painter, x + (_fontWidth * i), y, _fontWidth, _fontHeight, code);
} else {
drawOtherChar(painter, x + (_fontWidth * i), y, _fontWidth, _fontHeight, code);
}
}
painter.setPen(originalPen);
}
void TerminalDisplay::setKeyboardCursorShape(Enum::CursorShapeEnum shape)
{
_cursorShape = shape;
}
Enum::CursorShapeEnum TerminalDisplay::keyboardCursorShape() const
{
return _cursorShape;
}
void TerminalDisplay::setKeyboardCursorColor(const QColor& color)
{
_cursorColor = color;
}
QColor TerminalDisplay::keyboardCursorColor() const
{
return _cursorColor;
}
void TerminalDisplay::setOpacity(qreal opacity)
{
QColor color(_blendColor);
color.setAlphaF(opacity);
_opacity = opacity;
// enable automatic background filling to prevent the display
// flickering if there is no transparency
/*if ( color.alpha() == 255 )
{
setAutoFillBackground(true);
}
else
{
setAutoFillBackground(false);
}*/
_blendColor = color.rgba();
updateScrollBarPalette();
}
void TerminalDisplay::setWallpaper(ColorSchemeWallpaper::Ptr p)
{
_wallpaper = p;
}
void TerminalDisplay::drawBackground(QPainter& painter, const QRect& rect, const QColor& backgroundColor, bool useOpacitySetting)
{
// the area of the widget showing the contents of the terminal display is drawn
// using the background color from the color scheme set with setColorTable()
//
// the area of the widget behind the scroll-bar is drawn using the background
// brush from the scroll-bar's palette, to give the effect of the scroll-bar
// being outside of the terminal display and visual consistency with other KDE
// applications.
//
QRect scrollBarArea = _scrollBar->isVisible() ?
rect.intersected(_scrollBar->geometry()) :
QRect();
QRegion contentsRegion = QRegion(rect).subtracted(scrollBarArea);
QRect contentsRect = contentsRegion.boundingRect();
if (useOpacitySetting && !_wallpaper->isNull() &&
_wallpaper->draw(painter, contentsRect, _opacity)) {
} else if (qAlpha(_blendColor) < 0xff && useOpacitySetting) {
#if defined(Q_OS_MACOS)
// TODO - On MacOS, using CompositionMode doesn't work. Altering the
// transparency in the color scheme alters the brightness.
painter.fillRect(contentsRect, backgroundColor);
#else
QColor color(backgroundColor);
color.setAlpha(qAlpha(_blendColor));
painter.save();
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.fillRect(contentsRect, color);
painter.restore();
#endif
} else {
painter.fillRect(contentsRect, backgroundColor);
}
painter.fillRect(scrollBarArea, _scrollBar->palette().background());
}
void TerminalDisplay::drawCursor(QPainter& painter,
const QRect& rect,
const QColor& foregroundColor,
const QColor& /*backgroundColor*/,
bool& invertCharacterColor)
{
// don't draw cursor which is currently blinking
if (_cursorBlinking) {
return;
}
// shift rectangle top down one pixel to leave some space
// between top and bottom
QRect cursorRect = rect.adjusted(0, 1, 0, 0);
QColor cursorColor = _cursorColor.isValid() ? _cursorColor : foregroundColor;
painter.setPen(cursorColor);
if (_cursorShape == Enum::BlockCursor) {
// draw the cursor outline, adjusting the area so that
// it is draw entirely inside 'rect'
int penWidth = qMax(1, painter.pen().width());
painter.drawRect(cursorRect.adjusted(penWidth / 2,
penWidth / 2,
- penWidth / 2 - penWidth % 2,
- penWidth / 2 - penWidth % 2));
// draw the cursor body only when the widget has focus
if (hasFocus()) {
painter.fillRect(cursorRect, cursorColor);
if (!_cursorColor.isValid()) {
// invert the color used to draw the text to ensure that the character at
// the cursor position is readable
invertCharacterColor = true;
}
}
} else if (_cursorShape == Enum::UnderlineCursor) {
painter.drawLine(cursorRect.left(),
cursorRect.bottom(),
cursorRect.right(),
cursorRect.bottom());
} else if (_cursorShape == Enum::IBeamCursor) {
painter.drawLine(cursorRect.left(),
cursorRect.top(),
cursorRect.left(),
cursorRect.bottom());
}
}
void TerminalDisplay::drawCharacters(QPainter& painter,
const QRect& rect,
const QString& text,
const Character* style,
bool invertCharacterColor)
{
// don't draw text which is currently blinking
if (_textBlinking && ((style->rendition & RE_BLINK) != 0)) {
return;
}
// don't draw concealed characters
if ((style->rendition & RE_CONCEAL) != 0) {
return;
}
// setup bold and underline
bool useBold = (((style->rendition & RE_BOLD) != 0) && _boldIntense) || font().bold();
const bool useUnderline = ((style->rendition & RE_UNDERLINE) != 0) || font().underline();
const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || font().italic();
const bool useStrikeOut = ((style->rendition & RE_STRIKEOUT) != 0) || font().strikeOut();
const bool useOverline = ((style->rendition & RE_OVERLINE) != 0) || font().overline();
QFont font = painter.font();
if (font.bold() != useBold
|| font.underline() != useUnderline
|| font.italic() != useItalic
|| font.strikeOut() != useStrikeOut
|| font.overline() != useOverline) {
font.setBold(useBold);
font.setUnderline(useUnderline);
font.setItalic(useItalic);
font.setStrikeOut(useStrikeOut);
font.setOverline(useOverline);
painter.setFont(font);
}
// setup pen
const CharacterColor& textColor = (invertCharacterColor ? style->backgroundColor : style->foregroundColor);
const QColor color = textColor.color(_colorTable);
QPen pen = painter.pen();
if (pen.color() != color) {
pen.setColor(color);
painter.setPen(color);
}
// draw text
if (isLineCharString(text) && !_useFontLineCharacters) {
drawLineCharString(painter, rect.x(), rect.y(), text, style);
} else {
// Force using LTR as the document layout for the terminal area, because
// there is no use cases for RTL emulator and RTL terminal application.
//
// This still allows RTL characters to be rendered in the RTL way.
painter.setLayoutDirection(Qt::LeftToRight);
if (_bidiEnabled) {
painter.drawText(rect.x(), rect.y() + _fontAscent + _lineSpacing, text);
} else {
painter.drawText(rect.x(), rect.y() + _fontAscent + _lineSpacing, LTR_OVERRIDE_CHAR + text);
}
}
}
void TerminalDisplay::drawTextFragment(QPainter& painter ,
const QRect& rect,
const QString& text,
const Character* style)
{
painter.save();
// setup painter
const QColor foregroundColor = style->foregroundColor.color(_colorTable);
const QColor backgroundColor = style->backgroundColor.color(_colorTable);
// draw background if different from the display's background color
if (backgroundColor != palette().background().color()) {
drawBackground(painter, rect, backgroundColor,
false /* do not use transparency */);
}
// draw cursor shape if the current character is the cursor
// this may alter the foreground and background colors
bool invertCharacterColor = false;
if ((style->rendition & RE_CURSOR) != 0) {
drawCursor(painter, rect, foregroundColor, backgroundColor, invertCharacterColor);
}
// draw text
drawCharacters(painter, rect, text, style, invertCharacterColor);
painter.restore();
}
void TerminalDisplay::drawPrinterFriendlyTextFragment(QPainter& painter,
const QRect& rect,
const QString& text,
const Character* style)
{
painter.save();
// Set the colors used to draw to black foreground and white
// background for printer friendly output when printing
Character print_style = *style;
print_style.foregroundColor = CharacterColor(COLOR_SPACE_RGB, 0x00000000);
print_style.backgroundColor = CharacterColor(COLOR_SPACE_RGB, 0xFFFFFFFF);
// draw text
drawCharacters(painter, rect, text, &print_style, false);
painter.restore();
}
void TerminalDisplay::setRandomSeed(uint randomSeed)
{
_randomSeed = randomSeed;
}
uint TerminalDisplay::randomSeed() const
{
return _randomSeed;
}
// scrolls the image by 'lines', down if lines > 0 or up otherwise.
//
// the terminal emulation keeps track of the scrolling of the character
// image as it receives input, and when the view is updated, it calls scrollImage()
// with the final scroll amount. this improves performance because scrolling the
// display is much cheaper than re-rendering all the text for the
// part of the image which has moved up or down.
// Instead only new lines have to be drawn
void TerminalDisplay::scrollImage(int lines , const QRect& screenWindowRegion)
{
// return if there is nothing to do
if ((lines == 0) || (_image == nullptr)) {
return;
}
// 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()) {
return;
}
if ((_readOnlyMessageWidget != nullptr) && _readOnlyMessageWidget->isVisible()) {
return;
}
// constrain the region to the display
// the bottom of the region is capped to the number of lines in the display's
// internal image - 2, so that the height of 'region' is strictly less
// than the height of the internal image.
QRect region = screenWindowRegion;
region.setBottom(qMin(region.bottom(), this->_lines - 2));
// return if there is nothing to do
if (!region.isValid()
|| (region.top() + abs(lines)) >= region.bottom()
|| this->_lines <= region.height()) {
return;
}
// hide terminal size label to prevent it being scrolled
if ((_resizeWidget != nullptr) && _resizeWidget->isVisible()) {
_resizeWidget->hide();
}
// Note: With Qt 4.4 the left edge of the scrolled area must be at 0
// to get the correct (newly exposed) part of the widget repainted.
//
// The right edge must be before the left edge of the scroll bar to
// avoid triggering a repaint of the entire widget, the distance is
// given by SCROLLBAR_CONTENT_GAP
//
// Set the QT_FLUSH_PAINT environment variable to '1' before starting the
// application to monitor repainting.
//
const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->width();
const int SCROLLBAR_CONTENT_GAP = 1;
QRect scrollRect;
if (_scrollbarLocation == Enum::ScrollBarLeft) {
scrollRect.setLeft(scrollBarWidth + SCROLLBAR_CONTENT_GAP);
scrollRect.setRight(width());
} else {
scrollRect.setLeft(0);
scrollRect.setRight(width() - scrollBarWidth - SCROLLBAR_CONTENT_GAP);
}
void* firstCharPos = &_image[ region.top() * this->_columns ];
void* lastCharPos = &_image[(region.top() + abs(lines)) * this->_columns ];
const int top = _contentRect.top() + (region.top() * _fontHeight);
const int linesToMove = region.height() - abs(lines);
const int bytesToMove = linesToMove * this->_columns * sizeof(Character);
Q_ASSERT(linesToMove > 0);
Q_ASSERT(bytesToMove > 0);
//scroll internal image
if (lines > 0) {
// check that the memory areas that we are going to move are valid
Q_ASSERT((char*)lastCharPos + bytesToMove <
(char*)(_image + (this->_lines * this->_columns)));
Q_ASSERT((lines * this->_columns) < _imageSize);
//scroll internal image down
memmove(firstCharPos , lastCharPos , bytesToMove);
//set region of display to scroll
scrollRect.setTop(top);
} else {
// check that the memory areas that we are going to move are valid
Q_ASSERT((char*)firstCharPos + bytesToMove <
(char*)(_image + (this->_lines * this->_columns)));
//scroll internal image up
memmove(lastCharPos , firstCharPos , bytesToMove);
//set region of the display to scroll
scrollRect.setTop(top + abs(lines) * _fontHeight);
}
scrollRect.setHeight(linesToMove * _fontHeight);
Q_ASSERT(scrollRect.isValid() && !scrollRect.isEmpty());
//scroll the display vertically to match internal _image
scroll(0 , _fontHeight * (-lines) , scrollRect);
}
QRegion TerminalDisplay::hotSpotRegion() const
{
QRegion region;
foreach(Filter::HotSpot * hotSpot , _filterChain->hotSpots()) {
QRect r;
if (hotSpot->startLine() == hotSpot->endLine()) {
r.setLeft(hotSpot->startColumn());
r.setTop(hotSpot->startLine());
r.setRight(hotSpot->endColumn());
r.setBottom(hotSpot->endLine());
region |= imageToWidget(r);
} else {
r.setLeft(hotSpot->startColumn());
r.setTop(hotSpot->startLine());
r.setRight(_columns);
r.setBottom(hotSpot->startLine());
region |= imageToWidget(r);
for (int line = hotSpot->startLine() + 1 ; line < hotSpot->endLine() ; line++) {
r.setLeft(0);
r.setTop(line);
r.setRight(_columns);
r.setBottom(line);
region |= imageToWidget(r);
}
r.setLeft(0);
r.setTop(hotSpot->endLine());
r.setRight(hotSpot->endColumn());
r.setBottom(hotSpot->endLine());
region |= imageToWidget(r);
}
}
return region;
}
void TerminalDisplay::processFilters()
{
if (_screenWindow == nullptr) {
return;
}
if (!_filterUpdateRequired) {
return;
}
QRegion preUpdateHotSpots = 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();
QRegion postUpdateHotSpots = hotSpotRegion();
update(preUpdateHotSpots | postUpdateHotSpots);
_filterUpdateRequired = false;
}
void TerminalDisplay::updateImage()
{
if (_screenWindow == nullptr) {
return;
}
// 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 artefacts, see BUG 350651
if (!(WindowSystemInfo::HAVE_TRANSPARENCY && (qApp->devicePixelRatio() > 1.0)) && _wallpaper->isNull()) {
scrollImage(_screenWindow->scrollCount() ,
_screenWindow->scrollRegion());
_screenWindow->resetScrollCount();
}
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();
setScroll(_screenWindow->currentLine() , _screenWindow->lineCount());
Q_ASSERT(this->_usedLines <= this->_lines);
Q_ASSERT(this->_usedColumns <= this->_columns);
int y, x, len;
const QPoint tL = contentsRect().topLeft();
const int tLx = tL.x();
const int tLy = tL.y();
_hasTextBlinker = false;
CharacterColor cf; // undefined
const int linesToUpdate = qMin(this->_lines, qMax(0, lines));
const int columnsToUpdate = qMin(this->_columns, qMax(0, 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 * this->_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 = newLine[x + 0].isLineChar();
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) ||
ch.isLineChar() != lineDraw ||
nextIsDoubleWidth != doubleWidth) {
break;
}
}
const bool saveFixedFont = _fixedFont;
if (lineDraw) {
_fixedFont = false;
}
if (doubleWidth) {
_fixedFont = false;
}
updateLine = true;
_fixedFont = saveFixedFont;
x += len - 1;
}
}
}
//both the top and bottom halves of double height _lines must always be redrawn
//although both top and bottom halves contain the same characters, only
//the top one is actually
//drawn.
if (_lineProperties.count() > y) {
updateLine |= (_lineProperties[y] & LINE_DOUBLEHEIGHT);
}
// 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 + _fontHeight * y ,
_fontWidth * columnsToUpdate ,
_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));
}
// 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 + _fontHeight * linesToUpdate ,
_fontWidth * this->_columns ,
_fontHeight * (_usedLines - linesToUpdate));
}
_usedLines = linesToUpdate;
if (columnsToUpdate < _usedColumns) {
dirtyRegion |= QRect(_contentRect.left() + tLx + columnsToUpdate * _fontWidth ,
_contentRect.top() + tLy ,
_fontWidth * (_usedColumns - columnsToUpdate) ,
_fontHeight * this->_lines);
}
_usedColumns = columnsToUpdate;
dirtyRegion |= _inputMethodData.previousPreeditRect;
// 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().width(i18n("Size: XXX x XXX")));
_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);
foreach(const QRect & rect, (pe->region() & contentsRect()).rects()) {
drawBackground(paint, rect, palette().background().color(),
true /* use opacity setting */);
drawContents(paint, rect);
}
drawCurrentResultRect(paint);
drawInputMethodPreeditString(paint, preeditRect());
paintFilters(paint);
}
void TerminalDisplay::printContent(QPainter& painter, bool friendly)
{
// Reinitialize the font with the printers paint device so the font
// measurement calculations will be done correctly
QFont savedFont = getVTFont();
QFont font(savedFont, painter.device());
painter.setFont(font);
setVTFont(font);
QRect rect(0, 0, size().width(), size().height());
_printerFriendly = friendly;
if (!friendly) {
drawBackground(painter, rect, getBackgroundColor(),
true /* use opacity setting */);
}
drawContents(painter, rect);
_printerFriendly = false;
setVTFont(savedFont);
}
QPoint TerminalDisplay::cursorPosition() const
{
if (_screenWindow != nullptr) {
return _screenWindow->cursorPosition();
} else {
return QPoint(0, 0);
}
}
FilterChain* TerminalDisplay::filterChain() const
{
return _filterChain;
}
void TerminalDisplay::paintFilters(QPainter& painter)
{
if (_filterUpdateRequired) {
return;
}
// get color of character under mouse and use it to draw
// lines for filters
QPoint cursorPos = mapFromGlobal(QCursor::pos());
int cursorLine;
int cursorColumn;
getCharacterPosition(cursorPos , cursorLine , cursorColumn);
Character cursorCharacter = _image[loc(cursorColumn, cursorLine)];
painter.setPen(QPen(cursorCharacter.foregroundColor.color(colorTable())));
// iterate over hotspots identified by the display's currently active filters
// and draw appropriate visuals to indicate the presence of the hotspot
int urlNumber = 0;
QList<Filter::HotSpot*> spots = _filterChain->hotSpots();
foreach(Filter::HotSpot* spot, spots) {
urlNumber++;
QRegion region;
if (spot->type() == Filter::HotSpot::Link) {
QRect r;
if (spot->startLine() == spot->endLine()) {
r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(),
spot->startLine()*_fontHeight + _contentRect.top(),
(spot->endColumn())*_fontWidth + _contentRect.left() - 1,
(spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1);
region |= r;
} else {
r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(),
spot->startLine()*_fontHeight + _contentRect.top(),
(_columns)*_fontWidth + _contentRect.left() - 1,
(spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1);
region |= r;
for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) {
r.setCoords(0 * _fontWidth + _contentRect.left(),
line * _fontHeight + _contentRect.top(),
(_columns)*_fontWidth + _contentRect.left() - 1,
(line + 1)*_fontHeight + _contentRect.top() - 1);
region |= r;
}
r.setCoords(0 * _fontWidth + _contentRect.left(),
spot->endLine()*_fontHeight + _contentRect.top(),
(spot->endColumn())*_fontWidth + _contentRect.left() - 1,
(spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1);
region |= r;
}
if (_showUrlHint && urlNumber < 10) {
// Position at the beginning of the URL
const QVector<QRect> regionRects = region.rects();
QRect hintRect(regionRects.first());
hintRect.setWidth(r.height());
painter.fillRect(hintRect, QColor(0, 0, 0, 128));
painter.setPen(Qt::white);
painter.drawRect(hintRect.adjusted(0, 0, -1, -1));
painter.drawText(hintRect, Qt::AlignCenter, QString::number(urlNumber));
}
}
for (int line = spot->startLine() ; line <= spot->endLine() ; line++) {
int startColumn = 0;
int endColumn = _columns - 1; // TODO use number of _columns which are actually
// occupied on this line rather than the width of the
// display in _columns
// Check image size so _image[] is valid (see makeImage)
if (loc(endColumn, line) > _imageSize) {
break;
}
// ignore whitespace at the end of the lines
while (_image[loc(endColumn, line)].isSpace() && endColumn > 0) {
endColumn--;
}
// increment here because the column which we want to set 'endColumn' to
// is the first whitespace character at the end of the line
endColumn++;
if (line == spot->startLine()) {
startColumn = spot->startColumn();
}
if (line == spot->endLine()) {
endColumn = spot->endColumn();
}
// TODO: resolve this comment with the new margin/center code
// subtract one pixel from
// the right and bottom so that
// we do not overdraw adjacent
// hotspots
//
// subtracting one pixel from all sides also prevents an edge case where
// moving the mouse outside a link could still leave it underlined
// because the check below for the position of the cursor
// finds it on the border of the target area
QRect r;
r.setCoords(startColumn * _fontWidth + _contentRect.left(),
line * _fontHeight + _contentRect.top(),
endColumn * _fontWidth + _contentRect.left() - 1,
(line + 1)*_fontHeight + _contentRect.top() - 1);
// Underline link hotspots
if (spot->type() == Filter::HotSpot::Link) {
QFontMetrics metrics(font());
// find the baseline (which is the invisible line that the characters in the font sit on,
// with some having tails dangling below)
const int baseline = r.bottom() - metrics.descent();
// find the position of the underline below that
const int underlinePos = baseline + metrics.underlinePos();
if (_showUrlHint || region.contains(mapFromGlobal(QCursor::pos()))) {
painter.drawLine(r.left() , underlinePos ,
r.right() , underlinePos);
}
// Marker hotspots simply have a transparent rectangular shape
// drawn on top of them
} else if (spot->type() == Filter::HotSpot::Marker) {
//TODO - Do not use a hardcoded color for this
const bool isCurrentResultLine = (_screenWindow->currentResultLine() == (spot->startLine() + _screenWindow->currentLine()));
QColor color = isCurrentResultLine ? QColor(255, 255, 0, 120) : QColor(255, 0, 0, 120);
painter.fillRect(r, color);
}
}
}
}
void TerminalDisplay::drawContents(QPainter& paint, const QRect& rect)
{
const QPoint tL = contentsRect().topLeft();
const int tLx = tL.x();
const int tLy = tL.y();
const int lux = qMin(_usedColumns - 1, qMax(0, (rect.left() - tLx - _contentRect.left()) / _fontWidth));
const int luy = qMin(_usedLines - 1, qMax(0, (rect.top() - tLy - _contentRect.top()) / _fontHeight));
const int rlx = qMin(_usedColumns - 1, qMax(0, (rect.right() - tLx - _contentRect.left()) / _fontWidth));
const int rly = qMin(_usedLines - 1, qMax(0, (rect.bottom() - tLy - _contentRect.top()) / _fontHeight));
const int numberOfColumns = _usedColumns;
QString unistr;
unistr.reserve(numberOfColumns);
for (int y = luy; y <= rly; y++) {
int x = lux;
if ((_image[loc(lux, y)].character == 0u) && (x != 0)) {
x--; // Search for start of multi-column character
}
for (; x <= rlx; x++) {
int len = 1;
int p = 0;
// reset our buffer to the number of columns
int bufferSize = numberOfColumns;
unistr.resize(bufferSize);
QChar *disstrU = unistr.data();
// is this a single character or a sequence of characters ?
if ((_image[loc(x, y)].rendition & RE_EXTENDED_CHAR) != 0) {
// sequence of characters
ushort extendedCharLength = 0;
const ushort* chars = ExtendedCharTable::instance.lookupExtendedChar(_image[loc(x, y)].character, extendedCharLength);
if (chars != nullptr) {
Q_ASSERT(extendedCharLength > 1);
bufferSize += extendedCharLength - 1;
unistr.resize(bufferSize);
disstrU = unistr.data();
for (int index = 0 ; index < extendedCharLength ; index++) {
Q_ASSERT(p < bufferSize);
disstrU[p++] = chars[index];
}
}
} else {
// single character
const quint16 c = _image[loc(x, y)].character;
if (c != 0u) {
Q_ASSERT(p < bufferSize);
disstrU[p++] = c; //fontMap(c);
}
}
const bool lineDraw = _image[loc(x, y)].isLineChar();
const bool doubleWidth = (_image[ qMin(loc(x, y) + 1, _imageSize) ].character == 0);
const CharacterColor currentForeground = _image[loc(x, y)].foregroundColor;
const CharacterColor currentBackground = _image[loc(x, y)].backgroundColor;
const RenditionFlags currentRendition = _image[loc(x, y)].rendition;
while (x + len <= rlx &&
_image[loc(x + len, y)].foregroundColor == currentForeground &&
_image[loc(x + len, y)].backgroundColor == currentBackground &&
(_image[loc(x + len, y)].rendition & ~RE_EXTENDED_CHAR) == (currentRendition & ~RE_EXTENDED_CHAR) &&
(_image[ qMin(loc(x + len, y) + 1, _imageSize) ].character == 0) == doubleWidth &&
_image[loc(x + len, y)].isLineChar() == lineDraw) {
const quint16 c = _image[loc(x + len, y)].character;
if ((_image[loc(x + len, y)].rendition & RE_EXTENDED_CHAR) != 0) {
// sequence of characters
ushort extendedCharLength = 0;
const ushort* chars = ExtendedCharTable::instance.lookupExtendedChar(c, extendedCharLength);
if (chars != nullptr) {
Q_ASSERT(extendedCharLength > 1);
bufferSize += extendedCharLength - 1;
unistr.resize(bufferSize);
disstrU = unistr.data();
for (int index = 0 ; index < extendedCharLength ; index++) {
Q_ASSERT(p < bufferSize);
disstrU[p++] = chars[index];
}
}
} else {
// single character
if (c != 0u) {
Q_ASSERT(p < bufferSize);
disstrU[p++] = c; //fontMap(c);
}
}
if (doubleWidth) { // assert((_image[loc(x+len,y)+1].character == 0)), see above if condition
len++; // Skip trailing part of multi-column character
}
len++;
}
if ((x + len < _usedColumns) && (_image[loc(x + len, y)].character == 0u)) {
len++; // Adjust for trailing part of multi-column character
}
const bool save__fixedFont = _fixedFont;
if (lineDraw) {
_fixedFont = false;
}
if (doubleWidth) {
_fixedFont = false;
}
unistr.resize(p);
// Create a text scaling matrix for double width and double height lines.
QMatrix textScale;
if (y < _lineProperties.size()) {
if ((_lineProperties[y] & LINE_DOUBLEWIDTH) != 0) {
textScale.scale(2, 1);
}
if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) {
textScale.scale(1, 2);
}
}
//Apply text scaling matrix.
paint.setWorldMatrix(textScale, true);
//calculate the area in which the text will be drawn
QRect textArea = QRect(_contentRect.left() + tLx + _fontWidth * x , _contentRect.top() + tLy + _fontHeight * y , _fontWidth * len , _fontHeight);
//move the calculated area to take account of scaling applied to the painter.
//the position of the area from the origin (0,0) is scaled
//by the opposite of whatever
//transformation has been applied to the painter. this ensures that
//painting does actually start from textArea.topLeft()
//(instead of textArea.topLeft() * painter-scale)
textArea.moveTopLeft(textScale.inverted().map(textArea.topLeft()));
//paint text fragment
if (_printerFriendly) {
drawPrinterFriendlyTextFragment(paint,
textArea,
unistr,
&_image[loc(x, y)]);
} else {
drawTextFragment(paint,
textArea,
unistr,
&_image[loc(x, y)]);
}
_fixedFont = save__fixedFont;
//reset back to single-width, single-height _lines
paint.setWorldMatrix(textScale.inverted(), true);
if (y < _lineProperties.size() - 1) {
//double-height _lines are represented by two adjacent _lines
//containing the same characters
//both _lines will have the LINE_DOUBLEHEIGHT attribute.
//If the current line has the LINE_DOUBLEHEIGHT attribute,
//we can therefore skip the next line
if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) {
y++;
}
}
x += len - 1;
}
}
}
void TerminalDisplay::drawCurrentResultRect(QPainter& painter)
{
if(_screenWindow->currentResultLine() == -1) {
return;
}
QRect r(0, (_screenWindow->currentResultLine() - _screenWindow->currentLine())*_fontHeight,
contentsRect().width(), _fontHeight);
painter.fillRect(r, QColor(0, 0, 255, 80));
}
QRect TerminalDisplay::imageToWidget(const QRect& imageArea) const
{
QRect result;
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;
}
/* ------------------------------------------------------------------------- */
/* */
/* 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
blinkCursorEvent();
}
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);
_showUrlHint = false;
emit focusLost();
}
void TerminalDisplay::focusInEvent(QFocusEvent*)
{
if (_allowBlinkingCursor) {
_blinkCursorTimer->start();
}
updateCursor();
if (_allowBlinkingText && _hasTextBlinker) {
_blinkTextTimer->start();
}
emit focusGained();
}
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()
{
int cursorLocation = loc(cursorPosition().x(), cursorPosition().y());
int charWidth = konsole_wcwidth(_image[cursorLocation].character);
QRect cursorRect = imageToWidget(QRect(cursorPosition(), QSize(charWidth, 1)));
update(cursorRect);
}
/* ------------------------------------------------------------------------- */
/* */
/* Geometry & Resizing */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::resizeEvent(QResizeEvent*)
{
if (contentsRect().isValid()) {
updateImageSize();
}
}
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 != nullptr) {
_screenWindow->setWindowLines(_lines);
}
_resizing = (oldLines != _lines) || (oldColumns != _columns);
if (_resizing) {
showResizeNotification();
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;
// We over-commit one character so that we can be more relaxed in dealing with
// certain boundary conditions: _image[_imageSize] is a valid but unused position
_image = new Character[_imageSize + 1];
clearImage();
}
void TerminalDisplay::clearImage()
{
for (int i = 0; i <= _imageSize; ++i) {
_image[i] = Screen::DefaultChar;
}
}
void TerminalDisplay::calcGeometry()
{
_scrollBar->resize(_scrollBar->sizeHint().width(), contentsRect().height());
_contentRect = contentsRect().adjusted(_margin, _margin, -_margin, -_margin);
switch (_scrollbarLocation) {
case Enum::ScrollBarHidden :
break;
case Enum::ScrollBarLeft :
_contentRect.setLeft(_contentRect.left() + _scrollBar->width());
_scrollBar->move(contentsRect().topLeft());
break;
case Enum::ScrollBarRight:
_contentRect.setRight(_contentRect.right() - _scrollBar->width());
_scrollBar->move(contentsRect().topRight() - QPoint(_scrollBar->width() - 1, 0));
break;
}
// 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() / _fontHeight);
_usedLines = qMin(_usedLines, _lines);
if(_centerContents) {
QSize unusedPixels = _contentRect.size() - QSize(_columns * _fontWidth, _lines * _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 * _fontWidth) ,
verticalMargin + (lines * _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*)
{
emit changedContentSizeSignal(_contentRect.height(), _contentRect.width());
}
void TerminalDisplay::hideEvent(QHideEvent*)
{
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();
}
/* ------------------------------------------------------------------------- */
/* */
/* Scrollbar */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::setScrollBarPosition(Enum::ScrollBarPositionEnum position)
{
if (_scrollbarLocation == position) {
return;
}
if (position == Enum::ScrollBarHidden) {
_scrollBar->hide();
} else {
_scrollBar->show();
}
_scrollbarLocation = position;
propagateSize();
update();
}
void TerminalDisplay::scrollBarPositionChanged(int)
{
if (_screenWindow == nullptr) {
return;
}
_screenWindow->scrollTo(_scrollBar->value());
// if the thumb has been moved to the bottom of the _scrollBar then set
// the display to automatically track new output,
// that is, scroll down automatically
// to how new _lines as they are added
const bool atEndOfOutput = (_scrollBar->value() == _scrollBar->maximum());
_screenWindow->setTrackOutput(atEndOfOutput);
updateImage();
}
void TerminalDisplay::setScroll(int cursor, int slines)
{
// update _scrollBar if the range or value has changed,
// otherwise return
//
// setting the range or value of a _scrollBar will always trigger
// a repaint, so it should be avoided if it is not necessary
if (_scrollBar->minimum() == 0 &&
_scrollBar->maximum() == (slines - _lines) &&
_scrollBar->value() == cursor) {
return;
}
disconnect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged);
_scrollBar->setRange(0, slines - _lines);
_scrollBar->setSingleStep(1);
_scrollBar->setPageStep(_lines);
_scrollBar->setValue(cursor);
connect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged);
}
void TerminalDisplay::setScrollFullPage(bool fullPage)
{
_scrollFullPage = fullPage;
}
bool TerminalDisplay::scrollFullPage() const
{
return _scrollFullPage;
}
/* ------------------------------------------------------------------------- */
/* */
/* Mouse */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::mousePressEvent(QMouseEvent* ev)
{
if (_possibleTripleClick && (ev->button() == Qt::LeftButton)) {
mouseTripleClickEvent(ev);
return;
}
if (!contentsRect().contains(ev->pos())) {
return;
}
if (_screenWindow == nullptr) {
return;
}
// Ignore clicks on the message widget
if (_readOnlyMessageWidget != nullptr) {
if (_readOnlyMessageWidget->isVisible() && _readOnlyMessageWidget->frameGeometry().contains(ev->pos())) {
return;
}
}
if (_outputSuspendedMessageWidget != nullptr) {
if (_outputSuspendedMessageWidget->isVisible() && _outputSuspendedMessageWidget->frameGeometry().contains(ev->pos())) {
return;
}
}
int charLine;
int charColumn;
getCharacterPosition(ev->pos(), charLine, charColumn);
QPoint pos = QPoint(charColumn, charLine);
if (ev->button() == Qt::LeftButton) {
// request the software keyboard, if any
if (qApp->autoSipEnabled()) {
QStyle::RequestSoftwareInputPanel behavior = QStyle::RequestSoftwareInputPanel(
style()->styleHint(QStyle::SH_RequestSoftwareInputPanel));
if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) {
QEvent event(QEvent::RequestSoftwareInputPanel);
QApplication::sendEvent(this, &event);
}
}
_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);
if (_mouseMarks || (ev->modifiers() == Qt::ShiftModifier)) {
// Only extend selection for programs not interested in mouse
if (_mouseMarks && (ev->modifiers() == Qt::ShiftModifier)) {
extendSelection(ev->pos());
} else {
_screenWindow->clearSelection();
pos.ry() += _scrollBar->value();
_iPntSel = _pntSel = pos;
_actSel = 1; // left mouse button pressed but nothing selected yet.
}
} else {
emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0);
}
if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u))) {
Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn);
if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) {
QObject action;
action.setObjectName(QStringLiteral("open-action"));
spot->activate(&action);
}
}
}
} else if (ev->button() == Qt::MidButton) {
processMidButtonClick(ev);
} else if (ev->button() == Qt::RightButton) {
if (_mouseMarks || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) {
emit configureRequest(ev->pos());
} else {
emit mouseSignal(2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0);
}
}
}
QList<QAction*> TerminalDisplay::filterActions(const QPoint& position)
{
int charLine, charColumn;
getCharacterPosition(position, charLine, charColumn);
Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn);
return spot != nullptr ? spot->actions() : QList<QAction*>();
}
void TerminalDisplay::mouseMoveEvent(QMouseEvent* ev)
{
int charLine = 0;
int charColumn = 0;
getCharacterPosition(ev->pos(), charLine, charColumn);
processFilters();
// handle filters
// change link hot-spot appearance on mouse-over
Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn);
if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) {
QRegion previousHotspotArea = _mouseOverHotspotArea;
_mouseOverHotspotArea = QRegion();
QRect r;
if (spot->startLine() == spot->endLine()) {
r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(),
spot->startLine()*_fontHeight + _contentRect.top(),
(spot->endColumn())*_fontWidth + _contentRect.left() - 1,
(spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1);
_mouseOverHotspotArea |= r;
} else {
r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(),
spot->startLine()*_fontHeight + _contentRect.top(),
(_columns)*_fontWidth + _contentRect.left() - 1,
(spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1);
_mouseOverHotspotArea |= r;
for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) {
r.setCoords(0 * _fontWidth + _contentRect.left(),
line * _fontHeight + _contentRect.top(),
(_columns)*_fontWidth + _contentRect.left() - 1,
(line + 1)*_fontHeight + _contentRect.top() - 1);
_mouseOverHotspotArea |= r;
}
r.setCoords(0 * _fontWidth + _contentRect.left(),
spot->endLine()*_fontHeight + _contentRect.top(),
(spot->endColumn())*_fontWidth + _contentRect.left() - 1,
(spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1);
_mouseOverHotspotArea |= r;
}
if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && (cursor().shape() != Qt::PointingHandCursor)) {
setCursor(Qt::PointingHandCursor);
}
update(_mouseOverHotspotArea | previousHotspotArea);
} else if (!_mouseOverHotspotArea.isEmpty()) {
if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) || (cursor().shape() == Qt::PointingHandCursor)) {
setCursor(_mouseMarks ? Qt::IBeamCursor : Qt::ArrowCursor);
}
update(_mouseOverHotspotArea);
// set hotspot area to an invalid rectangle
_mouseOverHotspotArea = QRegion();
}
// for auto-hiding the cursor, we need mouseTracking
if (ev->buttons() == Qt::NoButton) {
return;
}
// if the terminal is interested in mouse movements
// then emit a mouse movement signal, unless the shift
// key is being held down, which overrides this.
if (!_mouseMarks && !(ev->modifiers() & Qt::ShiftModifier)) {
int button = 3;
if ((ev->buttons() & Qt::LeftButton) != 0u) {
button = 0;
}
if ((ev->buttons() & Qt::MidButton) != 0u) {
button = 1;
}
if ((ev->buttons() & Qt::RightButton) != 0u) {
button = 2;
}
emit mouseSignal(button,
charColumn + 1,
charLine + 1 + _scrollBar->value() - _scrollBar->maximum(),
1);
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::MidButton) != 0u) {
return;
}
extendSelection(ev->pos());
}
void TerminalDisplay::leaveEvent(QEvent *)
{
// remove underline from an active link when cursor leaves the widget area
if(!_mouseOverHotspotArea.isEmpty()) {
update(_mouseOverHotspotArea);
_mouseOverHotspotArea = QRegion();
}
}
void TerminalDisplay::extendSelection(const QPoint& position)
{
if (_screenWindow == nullptr) {
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 * _fontWidth - 1,
_usedLines * _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()) / _fontHeight;
_scrollBar->setValue(_scrollBar->value() + linesBeyondWidget + 1); // scrollforward
}
if (oldpos.y() < textBounds.top()) {
linesBeyondWidget = (textBounds.top() - oldpos.y()) / _fontHeight;
_scrollBar->setValue(_scrollBar->value() - linesBeyondWidget - 1); // history
}
int charColumn = 0;
int charLine = 0;
getCharacterPosition(pos, charLine, charColumn);
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
int i;
QChar selClass;
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)
QPoint left = left_not_right ? here : _iPntSelCorr;
i = loc(left.x(), left.y());
if (i >= 0 && i <= _imageSize) {
selClass = charClass(_image[i]);
while (((left.x() > 0) || (left.y() > 0 && (_lineProperties[left.y() - 1] & LINE_WRAPPED)))
&& charClass(_image[i - 1]) == selClass) {
i--;
if (left.x() > 0) {
left.rx()--;
} else {
left.rx() = _usedColumns - 1;
left.ry()--;
}
}
}
// Find left (left_not_right ? from start : from here)
QPoint right = left_not_right ? _iPntSelCorr : here;
i = loc(right.x(), right.y());
if (i >= 0 && i <= _imageSize) {
selClass = charClass(_image[i]);
while (((right.x() < _usedColumns - 1) || (right.y() < _usedLines - 1 && (_lineProperties[right.y()] & LINE_WRAPPED)))
&& charClass(_image[i + 1]) == selClass) {
i++;
if (right.x() < _usedColumns - 1) {
right.rx()++;
} else {
right.rx() = 0;
right.ry()++;
}
}
}
// 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) {
QChar selClass;
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 left (left_not_right ? from start : from here)
QPoint right = left_not_right ? _iPntSelCorr : here;
if (right.x() > 0 && !_columnSelectionMode) {
int i = loc(right.x(), right.y());
if (i >= 0 && i <= _imageSize) {
selClass = charClass(_image[i - 1]);
/* if (selClass == ' ')
{
while ( right.x() < _usedColumns-1 && charClass(_image[i+1].character) == selClass && (right.y()<_usedLines-1) &&
!(_lineProperties[right.y()] & LINE_WRAPPED))
{ i++; right.rx()++; }
if (right.x() < _usedColumns-1)
right = left_not_right ? _iPntSelCorr : here;
else
right.rx()++; // will be balanced later because of offset=-1;
}*/
}
}
// 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 == nullptr) {
return;
}
int charLine;
int charColumn;
getCharacterPosition(ev->pos(), charLine, charColumn);
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 (!_mouseMarks && !(ev->modifiers() & Qt::ShiftModifier)) {
emit mouseSignal(0,
charColumn + 1,
charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2);
}
}
_dragInfo.state = diNone;
}
if (!_mouseMarks &&
(ev->button() == Qt::RightButton || ev->button() == Qt::MidButton) &&
!(ev->modifiers() & Qt::ShiftModifier)) {
emit mouseSignal(ev->button() == Qt::MidButton ? 1 : 2,
charColumn + 1,
charLine + 1 + _scrollBar->value() - _scrollBar->maximum() ,
2);
}
}
void TerminalDisplay::getCharacterPosition(const QPoint& widgetPoint, int& line, int& column) const
{
column = (widgetPoint.x() + _fontWidth / 2 - contentsRect().left() - _contentRect.left()) / _fontWidth;
line = (widgetPoint.y() - contentsRect().top() - _contentRect.top()) / _fontHeight;
if (line < 0) {
line = 0;
}
if (column < 0) {
column = 0;
}
if (line >= _usedLines) {
line = _usedLines - 1;
}
// the column value returned can be equal to _usedColumns, 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)
if (column > _usedColumns) {
column = _usedColumns;
}
}
void TerminalDisplay::updateLineProperties()
{
if (_screenWindow == nullptr) {
return;
}
_lineProperties = _screenWindow->getLineProperties();
}
void TerminalDisplay::processMidButtonClick(QMouseEvent* ev)
{
if (_mouseMarks || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) {
const bool appendEnter = (ev->modifiers() & Qt::ControlModifier) != 0u;
if (_middleClickPasteMode == Enum::PasteFromX11Selection) {
pasteFromX11Selection(appendEnter);
} else if (_middleClickPasteMode == Enum::PasteFromClipboard) {
pasteFromClipboard(appendEnter);
} else {
Q_ASSERT(false);
}
} else {
int charLine = 0;
int charColumn = 0;
getCharacterPosition(ev->pos(), charLine, charColumn);
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::MidButton) {
processMidButtonClick(ev);
return;
}
if (ev->button() != Qt::LeftButton) {
return;
}
if (_screenWindow == nullptr) {
return;
}
int charLine = 0;
int charColumn = 0;
getCharacterPosition(ev->pos(), charLine, charColumn);
QPoint pos(charColumn, charLine);
// pass on double click as two clicks.
if (!_mouseMarks && !(ev->modifiers() & Qt::ShiftModifier)) {
// Send just _ONE_ click event, since the first click of the double click
// was already sent by the click handler
emit mouseSignal(0, charColumn + 1,
charLine + 1 + _scrollBar->value() - _scrollBar->maximum(),
0); // left button
return;
}
_screenWindow->clearSelection();
QPoint bgnSel = pos;
QPoint endSel = pos;
int i = loc(bgnSel.x(), bgnSel.y());
_iPntSel = bgnSel;
_iPntSel.ry() += _scrollBar->value();
_wordSelectionMode = true;
_actSel = 2; // within selection
// find word boundaries...
const QChar selClass = charClass(_image[i]);
{
// find the start of the word
int x = bgnSel.x();
while (((x > 0) || (bgnSel.y() > 0 && (_lineProperties[bgnSel.y() - 1] & LINE_WRAPPED)))
&& charClass(_image[i - 1]) == selClass) {
i--;
if (x > 0) {
x--;
} else {
x = _usedColumns - 1;
bgnSel.ry()--;
}
}
bgnSel.setX(x);
_screenWindow->setSelectionStart(bgnSel.x() , bgnSel.y() , false);
// find the end of the word
i = loc(endSel.x(), endSel.y());
x = endSel.x();
while (((x < _usedColumns - 1) || (endSel.y() < _usedLines - 1 && (_lineProperties[endSel.y()] & LINE_WRAPPED)))
&& charClass(_image[i + 1]) == selClass) {
i++;
if (x < _usedColumns - 1) {
x++;
} else {
x = 0;
endSel.ry()++;
}
}
endSel.setX(x);
// In word selection mode don't select @ (64) if at end of word.
if (((_image[i].rendition & RE_EXTENDED_CHAR) == 0) &&
(QChar(_image[i].character) == QLatin1Char('@')) &&
((endSel.x() - bgnSel.x()) > 0)) {
endSel.setX(x - 1);
}
_actSel = 2; // within selection
_screenWindow->setSelectionEnd(endSel.x() , endSel.y());
copyToX11Selection();
}
_possibleTripleClick = true;
QTimer::singleShot(QApplication::doubleClickInterval(), [this]() {
_possibleTripleClick = false;
});
}
void TerminalDisplay::wheelEvent(QWheelEvent* ev)
{
// Only vertical scrolling is supported
if (ev->orientation() != Qt::Vertical) {
return;
}
const int modifiers = ev->modifiers();
_scrollWheelState.addWheelEvent(ev);
// ctrl+<wheel> for zooming, like in konqueror and firefox
if (((modifiers & Qt::ControlModifier) != 0u) && mouseWheelZoom()) {
int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE);
for (;steps > 0; --steps) {
// wheel-up for increasing font size
increaseFontSize();
}
for (;steps < 0; ++steps) {
// wheel-down for decreasing font size
decreaseFontSize();
}
return;
}
// If the program running in the terminal is not interested in mouse events:
// - Send the event to the scrollbar if the slider has room to move
// - Otherwise, send simulated up / down key presses to the terminal program
// for the benefit of programs such as 'less' (which use the alternate screen)
if (_mouseMarks) {
const bool canScroll = _scrollBar->maximum() > 0;
if (canScroll) {
_scrollBar->event(ev);
_sessionController->setSearchStartToWindowCurrentLine();
_scrollWheelState.clearAll();
} else if (!_isPrimaryScreen) {
// 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>(_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++) {
emit keyPressedSignal(&keyEvent);
}
}
} else {
// terminal program wants notification of mouse activity
int charLine;
int charColumn;
getCharacterPosition(ev->pos() , charLine , charColumn);
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) {
emit mouseSignal(button,
charColumn + 1,
charLine + 1 + _scrollBar->value() - _scrollBar->maximum() ,
0);
}
}
}
void TerminalDisplay::viewScrolledByUser()
{
_sessionController->setSearchStartToWindowCurrentLine();
}
/* Moving left/up from the line containing pnt, return the starting
offset point which the given line is continiously 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 QPoint(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 QPoint(0, lineInHistory - topVisibleLine);
}
/* Moving right/down from the line containing pnt, return the ending
offset point which the given line is continiously 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 QPoint(_columns - 1, lineInHistory - topVisibleLine);
}
}
line = 0;
lineProperties = screen->getLineProperties(lineInHistory, qMin(lineInHistory + visibleScreenLines, maxY));
}
return QPoint(_columns - 1, lineInHistory - topVisibleLine);
}
QPoint TerminalDisplay::findWordStart(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;
while (true) {
for (;;j--, x--) {
if (x > 0) {
if (charClass(image[j - 1]) == selClass) {
continue;
}
goto out;
} else if (i > 0) {
if (((lineProperties[i - 1] & LINE_WRAPPED) != 0) &&
charClass(image[j - 1]) == selClass) {
x = _columns;
i--;
y--;
continue;
}
goto out;
} else if (y > 0) {
break;
} else {
goto out;
}
}
int newRegStart = qMax(0, y - regSize);
lineProperties = screen->getLineProperties(newRegStart, y - 1);
i = y - newRegStart;
if (tmp_image == nullptr) {
tmp_image = new Character[imageSize];
image = tmp_image;
}
screen->getImage(tmp_image, imageSize, newRegStart, y - 1);
j = loc(x, i);
}
out:
if (tmp_image != nullptr) {
delete[] tmp_image;
}
return QPoint(x, y - curLine);
}
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) {
continue;
}
goto out;
} else if (i < lineCount - 1) {
if (((lineProperties[i] & LINE_WRAPPED) != 0) &&
charClass(image[j + 1]) == selClass) {
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--;
}
}
if (tmp_image != nullptr) {
delete[] tmp_image;
}
return QPoint(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 == nullptr) {
return;
}
int charLine;
int charColumn;
getCharacterPosition(ev->pos(), charLine, charColumn);
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 == nullptr) {
return;
}
selectLine(cursorPosition(), true);
}
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 ushort* chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength);
if ((chars != nullptr) && extendedCharLength > 0) {
const QString s = QString::fromUtf16(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;
}
// FIXME: the actual value of _mouseMarks is the opposite of its semantic.
// When using programs not interested with mouse(shell, less), it is true.
// When using programs interested with mouse(vim,mc), it is false.
void TerminalDisplay::setUsesMouse(bool on)
{
_mouseMarks = on;
setCursor(_mouseMarks ? Qt::IBeamCursor : Qt::ArrowCursor);
}
bool TerminalDisplay::usesMouse() const
{
return _mouseMarks;
}
void TerminalDisplay::usingPrimaryScreen(bool use)
{
_isPrimaryScreen = use;
}
void TerminalDisplay::setBracketedPasteMode(bool on)
{
_bracketedPasteMode = on;
}
bool TerminalDisplay::bracketedPasteMode() const
{
return _bracketedPasteMode;
}
/* ------------------------------------------------------------------------- */
/* */
/* Clipboard */
/* */
/* ------------------------------------------------------------------------- */
void TerminalDisplay::doPaste(QString text, bool appendReturn)
{
if (_screenWindow == nullptr) {
return;
}
if (_sessionController->isReadOnly()) {
return;
}
if (appendReturn) {
text.append(QLatin1String("\r"));
}
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;
}
}
if (!text.isEmpty()) {
text.replace(QLatin1Char('\n'), QLatin1Char('\r'));
if (bracketedPasteMode()) {
text.prepend(QLatin1String("\033[200~"));
text.append(QLatin1String("\033[201~"));
}
// perform paste by simulating keypress events
QKeyEvent e(QEvent::KeyPress, 0, Qt::NoModifier, text);
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 == nullptr) {
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));
}
if (QApplication::clipboard()->supportsSelection()) {
QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection);
}
if (_autoCopySelectedText) {
QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
}
}
void TerminalDisplay::copyToClipboard()
{
if (_screenWindow == nullptr) {
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::pasteFromClipboard(bool appendEnter)
{
QString text = QApplication::clipboard()->text(QClipboard::Clipboard);
doPaste(text, appendEnter);
}
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());
emit keyPressedSignal(&keyEvent);
}
if (!_sessionController->isReadOnly()) {
_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::ImMicroFocus:
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);
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 = string_width(_inputMethodData.preeditString);
if (preeditLength == 0) {
return QRect();
}
return QRect(_contentRect.left() + _fontWidth * cursorPosition().x(),
_contentRect.top() + _fontHeight * cursorPosition().y(),
_fontWidth * preeditLength,
_fontHeight);
}
void TerminalDisplay::drawInputMethodPreeditString(QPainter& painter , const QRect& rect)
{
if (_inputMethodData.preeditString.isEmpty()) {
return;
}
const QPoint cursorPos = cursorPosition();
bool invertColors = false;
const QColor background = _colorTable[DEFAULT_BACK_COLOR];
const QColor foreground = _colorTable[DEFAULT_FORE_COLOR];
const Character* style = &_image[loc(cursorPos.x(), cursorPos.y())];
drawBackground(painter, rect, background, true);
drawCursor(painter, rect, foreground, background, invertColors);
drawCharacters(painter, rect, _inputMethodData.preeditString, style, invertColors);
_inputMethodData.previousPreeditRect = rect;
}
/* ------------------------------------------------------------------------- */
/* */
/* 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=\"http://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, [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);
widget->setWordWrap(true);
widget->setFocusProxy(this);
widget->setCursor(Qt::ArrowCursor);
_verticalLayout->insertWidget(0, widget);
return widget;
}
void TerminalDisplay::updateReadOnlyState(bool readonly) {
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();
}
}
void TerminalDisplay::scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode, int amount)
{
_screenWindow->scrollBy(mode, amount, _scrollFullPage);
_screenWindow->setTrackOutput(_screenWindow->atEndOfOutput());
updateLineProperties();
updateImage();
viewScrolledByUser();
}
void TerminalDisplay::keyPressEvent(QKeyEvent* event)
{
if (_sessionController->isReadOnly()) {
event->accept();
return;
}
if ((_urlHintsModifiers != 0u) && event->modifiers() == _urlHintsModifiers) {
int hintSelected = event->key() - 0x31;
if (hintSelected >= 0 && hintSelected < 10 && hintSelected < _filterChain->hotSpots().count()) {
_filterChain->hotSpots().at(hintSelected)->activate();
_showUrlHint = false;
update();
return;
}
if (!_showUrlHint) {
processFilters();
_showUrlHint = true;
update();
}
}
_screenWindow->screen()->setCurrentTerminalDisplay(this);
_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);
}
emit keyPressedSignal(event);
#ifndef QT_NO_ACCESSIBILITY
QAccessibleTextCursorEvent textCursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX());
QAccessible::updateAccessibility(&textCursorEvent);
#endif
event->accept();
}
void TerminalDisplay::keyReleaseEvent(QKeyEvent *event)
{
if (_showUrlHint) {
_showUrlHint = false;
update();
}
if (_sessionController->isReadOnly()) {
event->accept();
return;
}
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;
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::PaletteChange:
case QEvent::ApplicationPaletteChange:
_scrollBar->setPalette(QApplication::palette());
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) {
emit configureRequest(mapFromGlobal(QCursor::pos()));
}
}
/* --------------------------------------------------------------------- */
/* */
/* Bell */
/* */
/* --------------------------------------------------------------------- */
void TerminalDisplay::setBellMode(int mode)
{
_bellMode = mode;
}
int TerminalDisplay::bellMode() const
{
return _bellMode;
}
void TerminalDisplay::bell(const QString& message)
{
if (_bellMasked) {
return;
}
switch (_bellMode) {
case Enum::SystemBeepBell:
KNotification::beep();
break;
case Enum::NotifyBell:
// STABLE API:
// Please note that these event names, "BellVisible" and "BellInvisible",
// should not change and should be kept stable, because other applications
// that use this code via KPart rely on these names for notifications.
KNotification::event(hasFocus() ? QStringLiteral("BellVisible") : QStringLiteral("BellInvisible"),
message, QPixmap(), this);
break;
case Enum::VisualBell:
visualBell();
break;
default:
break;
}
// limit the rate at which bells can occur.
// ...mainly for sound effects where rapid bells in sequence
// produce a horrible noise.
_bellMasked = true;
QTimer::singleShot(500, [this]() {
_bellMasked = false;
});
}
void TerminalDisplay::visualBell()
{
swapFGBGColors();
QTimer::singleShot(200, this, &Konsole::TerminalDisplay::swapFGBGColors);
}
void TerminalDisplay::swapFGBGColors()
{
// swap the default foreground & background color
ColorEntry color = _colorTable[DEFAULT_BACK_COLOR];
_colorTable[DEFAULT_BACK_COLOR] = _colorTable[DEFAULT_FORE_COLOR];
_colorTable[DEFAULT_FORE_COLOR] = color;
update();
}
/* --------------------------------------------------------------------- */
/* */
/* 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 ((!_sessionController->isReadOnly()) && (mimeData != nullptr)
&& (mimeData->hasFormat(QStringLiteral("text/plain"))
|| mimeData->hasFormat(QStringLiteral("text/uri-list")))) {
event->acceptProposedAction();
}
}
void TerminalDisplay::dropEvent(QDropEvent* event)
{
if (_sessionController->isReadOnly()) {
event->accept();
return;
}
const auto mimeData = event->mimeData();
if (mimeData == nullptr) {
return;
}
auto urls = mimeData->urls();
QString dropText;
if (!urls.isEmpty()) {
for (int i = 0 ; i < urls.count() ; i++) {
KIO::StatJob* job = KIO::mostLocalUrl(urls[i], KIO::HideProgressInfo);
bool ok = job->exec();
if (!ok) {
continue;
}
QUrl url = job->mostLocalUrl();
QString urlText;
if (url.isLocalFile()) {
urlText = url.path();
} else {
urlText = url.url();
}
// 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)
urlText = KShell::quoteArg(urlText);
dropText += urlText;
// Each filename(including the last) should be followed by one space.
dropText += QLatin1Char(' ');
}
// 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);
pasteAction->setData(dropText);
connect(pasteAction, &QAction::triggered, this, &TerminalDisplay::dropMenuPasteActionTriggered);
QList<QAction*> additionalActions;
additionalActions.append(pasteAction);
if (urls.count() == 1) {
KIO::StatJob* job = KIO::mostLocalUrl(urls[0], KIO::HideProgressInfo);
bool ok = job->exec();
if (ok) {
const QUrl url = job->mostLocalUrl();
if (url.isLocalFile()) {
const QFileInfo fileInfo(url.path());
if (fileInfo.isDir()) {
QAction* cdAction = new QAction(i18n("Change &Directory To"), this);
dropText = QLatin1String(" cd ") + dropText + QLatin1Char('\n');
cdAction->setData(dropText);
connect(cdAction, &QAction::triggered, this, &TerminalDisplay::dropMenuCdActionTriggered);
additionalActions.append(cdAction);
}
}
}
}
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"))) {
emit sendStringToEmu(dropText.toLocal8Bit());
}
}
void TerminalDisplay::dropMenuPasteActionTriggered()
{
if (sender() != nullptr) {
const QAction* action = qobject_cast<const QAction*>(sender());
if (action != nullptr) {
emit sendStringToEmu(action->data().toString().toLocal8Bit());
}
}
}
void TerminalDisplay::dropMenuCdActionTriggered()
{
if (sender() != nullptr) {
const QAction* action = qobject_cast<const QAction*>(sender());
if (action != nullptr) {
emit sendStringToEmu(action->data().toString().toLocal8Bit());
}
}
}
void TerminalDisplay::doDrag()
{
_dragInfo.state = diDragging;
_dragInfo.dragObject = new QDrag(this);
auto mimeData = new QMimeData();
mimeData->setText(QApplication::clipboard()->mimeData(QClipboard::Selection)->text());
mimeData->setHtml(QApplication::clipboard()->mimeData(QClipboard::Selection)->html());
_dragInfo.dragObject->setMimeData(mimeData);
_dragInfo.dragObject->exec(Qt::CopyAction);
}
void TerminalDisplay::setSessionController(SessionController* controller)
{
_sessionController = controller;
}
SessionController* TerminalDisplay::sessionController()
{
return _sessionController;
}
AutoScrollHandler::AutoScrollHandler(QWidget* parent)
: QObject(parent)
, _timerId(0)
{
parent->installEventFilter(this);
}
void AutoScrollHandler::timerEvent(QTimerEvent* event)
{
if (event->timerId() != _timerId) {
return;
}
QMouseEvent mouseEvent(QEvent::MouseMove,
widget()->mapFromGlobal(QCursor::pos()),
Qt::NoButton,
Qt::LeftButton,
Qt::NoModifier);
QApplication::sendEvent(widget(), &mouseEvent);
}
bool AutoScrollHandler::eventFilter(QObject* watched, QEvent* event)
{
Q_ASSERT(watched == parent());
Q_UNUSED(watched);
switch (event->type()) {
case QEvent::MouseMove: {
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
bool mouseInWidget = widget()->rect().contains(mouseEvent->pos());
if (mouseInWidget) {
if (_timerId != 0) {
killTimer(_timerId);
}
_timerId = 0;
} else {
if ((_timerId == 0) && ((mouseEvent->buttons() & Qt::LeftButton) != 0u)) {
_timerId = startTimer(100);
}
}
break;
}
case QEvent::MouseButtonRelease: {
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
if ((_timerId != 0) && ((mouseEvent->buttons() & ~Qt::LeftButton) != 0u)) {
killTimer(_timerId);
_timerId = 0;
}
break;
}
default:
break;
};
return false;
}