mirror of https://github.com/cutefishos/terminal
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2482 lines
84 KiB
C++
2482 lines
84 KiB
C++
/*
|
|
Copyright 2007-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 "TerminalView.h"
|
|
|
|
// System
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
|
|
#include <unicode/uchar.h>
|
|
#include <xkbcommon/xkbcommon.h>
|
|
|
|
#include "base/strings.h"
|
|
#include "ui/clipboard.h"
|
|
#include "ui/drag.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/window.h"
|
|
|
|
// Konsole
|
|
#include "ColorScheme.h"
|
|
#include "Filter.h"
|
|
#include "HistorySearch.h"
|
|
#include "LineChars.h"
|
|
#include "Screen.h"
|
|
#include "ScreenWindow.h"
|
|
#include "TerminalCharacterDecoder.h"
|
|
#include "TerminalFont.h"
|
|
#include "ui/bitmap.h"
|
|
#include "ui/bitmap_painter.h"
|
|
#include "ui/glyph_run.h"
|
|
#include "TerminalSession.h"
|
|
#include "Vt102Emulation.h"
|
|
#include "konsole_wcwidth.h"
|
|
#include "tools.h"
|
|
|
|
using namespace Konsole;
|
|
namespace ui = cutefish::ui;
|
|
namespace strings = cutefish::base::strings;
|
|
using cutefish::base::Point;
|
|
|
|
#ifndef loc
|
|
#define loc(X,Y) ((Y)*_columns+(X))
|
|
#endif
|
|
|
|
static const int TEXT_BLINK_DELAY = 500;
|
|
// std::max(QApplication::cursorFlashTime(), 1000) / 2.
|
|
static const int CURSOR_BLINK_INTERVAL = 500;
|
|
// QApplication::doubleClickInterval().
|
|
static const int DOUBLE_CLICK_INTERVAL = 400;
|
|
// QApplication::startDragDistance().
|
|
static const int START_DRAG_DISTANCE = 10;
|
|
// QApplication::wheelScrollLines().
|
|
static const int WHEEL_SCROLL_LINES = 3;
|
|
|
|
TerminalView::TerminalView()
|
|
: _font(std::make_unique<TerminalFont>())
|
|
{
|
|
setName("terminal");
|
|
setClipsChildren(true);
|
|
setTracksHover(true);
|
|
updatePointerCursor();
|
|
|
|
_blinkTimer.onTimeout = [this] { blinkEvent(); };
|
|
_blinkCursorTimer.onTimeout = [this] { blinkCursorTimeout(); };
|
|
_tripleClickTimer.setSingleShot(true);
|
|
_tripleClickTimer.onTimeout = [this] { _possibleTripleClick = false; };
|
|
_bellTimer.setSingleShot(true);
|
|
_bellTimer.onTimeout = [this] { _allowBell = true; };
|
|
_swapTimer.setSingleShot(true);
|
|
_swapTimer.onTimeout = [this] { swapColorTable(); };
|
|
|
|
setColorTable(base_color_table);
|
|
}
|
|
|
|
TerminalView::~TerminalView()
|
|
{
|
|
_windowConnections.clear();
|
|
_sessionConnections.clear();
|
|
if (_session)
|
|
_session->removeView(this);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Session */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::setSession(TerminalSession* session)
|
|
{
|
|
if (_session == session)
|
|
return;
|
|
|
|
_sessionConnections.clear();
|
|
_session = session;
|
|
if (!session)
|
|
return;
|
|
|
|
// Session::addView().
|
|
Emulation* emulation = session->emulation();
|
|
_sessionConnections.push_back(emulation->programUsesMouseChanged.connect([this](bool usesMouse) {
|
|
setUsesMouse(usesMouse);
|
|
}));
|
|
setUsesMouse(emulation->programUsesMouse());
|
|
_sessionConnections.push_back(emulation->programBracketedPasteModeChanged.connect([this](bool on) {
|
|
_bracketedPasteMode = on;
|
|
}));
|
|
_bracketedPasteMode = emulation->programBracketedPasteMode();
|
|
setScreenWindow(session->createWindow());
|
|
_sessionConnections.push_back(session->bellRequest.connect([this] { bell(); }));
|
|
|
|
session->setViewSize(this, _lines, _columns);
|
|
requestPaint();
|
|
}
|
|
|
|
void TerminalView::setScreenWindow(ScreenWindow* window)
|
|
{
|
|
_windowConnections.clear();
|
|
_screenWindow = window;
|
|
|
|
if (window) {
|
|
_windowConnections.push_back(window->outputChanged.connect([this] { updateLineProperties(); }));
|
|
_windowConnections.push_back(window->outputChanged.connect([this] { updateImage(); }));
|
|
_windowConnections.push_back(window->scrollToEnd.connect([this] { scrollToEnd(); }));
|
|
_windowConnections.push_back(window->cursorRevealRequested.connect([this] { revealCursor(); }));
|
|
// Drives copyAvailable, and with it hasSelection.
|
|
_windowConnections.push_back(window->selectionChanged.connect([this] { selectionChanged(); }));
|
|
window->setWindowLines(_lines);
|
|
}
|
|
}
|
|
|
|
void TerminalView::setUsesMouse(bool on)
|
|
{
|
|
if (_mouseMarks != on) {
|
|
_mouseMarks = on;
|
|
updatePointerCursor();
|
|
usesMouseChanged();
|
|
}
|
|
}
|
|
|
|
void TerminalView::selectionChanged()
|
|
{
|
|
requestPaint();
|
|
copyAvailable(_screenWindow && !_screenWindow->selectedText(false).empty());
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Colors */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::setColorTable(const ColorEntry table[])
|
|
{
|
|
for (int i = 0; i < TABLE_COLORS; i++)
|
|
_colorTable[i] = table[i];
|
|
requestPaint();
|
|
}
|
|
|
|
void TerminalView::setColorScheme(const std::string& name)
|
|
{
|
|
if (name == _colorScheme)
|
|
return;
|
|
|
|
// Looked up by name: listing the available schemes first would parse every file.
|
|
const ColorScheme* cs = ColorSchemeManager::instance()->findColorScheme(name);
|
|
if (!cs)
|
|
cs = ColorSchemeManager::instance()->defaultColorScheme();
|
|
|
|
ColorEntry table[TABLE_COLORS];
|
|
cs->getColorTable(table);
|
|
setColorTable(table);
|
|
_colorScheme = name;
|
|
}
|
|
|
|
ColorSchemeInfo TerminalView::colorSchemeInfo(const std::string& name)
|
|
{
|
|
ColorSchemeInfo info;
|
|
const ColorScheme* cs = ColorSchemeManager::instance()->findColorScheme(name);
|
|
if (!cs)
|
|
return info;
|
|
|
|
ColorEntry table[TABLE_COLORS];
|
|
cs->getColorTable(table);
|
|
|
|
info.valid = true;
|
|
info.description = cs->description();
|
|
info.background = table[DEFAULT_BACK_COLOR].color;
|
|
info.foreground = table[DEFAULT_FORE_COLOR].color;
|
|
for (int i = 0; i < 8; ++i)
|
|
info.colors[size_t(i)] = table[i + 2].color;
|
|
for (int i = 0; i < 8; ++i)
|
|
info.colors[size_t(i + 8)] = table[i + 2 + BASE_COLORS].color;
|
|
return info;
|
|
}
|
|
|
|
void TerminalView::setBackgroundOpacity(double opacity)
|
|
{
|
|
opacity = std::clamp(opacity, 0.0, 1.0);
|
|
if (std::abs(_opacity - opacity) < 1e-12)
|
|
return;
|
|
_opacity = opacity;
|
|
requestPaint();
|
|
}
|
|
|
|
void TerminalView::swapColorTable()
|
|
{
|
|
ColorEntry color = _colorTable[1];
|
|
_colorTable[1] = _colorTable[0];
|
|
_colorTable[0] = color;
|
|
_colorsInverted = !_colorsInverted;
|
|
requestPaint();
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Font */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::setTerminalFont(const std::string& family, double pointSize)
|
|
{
|
|
if (family == _fontFamily && pointSize == _fontPointSize && _metricsScale != 0)
|
|
return;
|
|
_fontFamily = family;
|
|
_fontPointSize = pointSize;
|
|
_metricsScale = 0;
|
|
updateFontMetrics();
|
|
}
|
|
|
|
// TerminalDisplay::fontChange(): the cell from the font at the window's scale.
|
|
void TerminalView::updateFontMetrics()
|
|
{
|
|
const double scale = window() ? window()->scale() : 1;
|
|
if (_metricsScale == scale)
|
|
return;
|
|
_metricsScale = scale;
|
|
|
|
_font->setFont(_fontFamily, _fontPointSize);
|
|
_metrics = _font->metrics();
|
|
_metrics.height += _lineSpacing;
|
|
if (_metrics.width < 1)
|
|
_metrics.width = 1;
|
|
|
|
fontMetricsChanged();
|
|
propagateSize();
|
|
requestPaint();
|
|
}
|
|
|
|
void TerminalView::setBlinkingCursor(bool blink)
|
|
{
|
|
_hasBlinkingCursor = blink;
|
|
|
|
if (blink && !_blinkCursorTimer.isActive() && hasKeyboard())
|
|
_blinkCursorTimer.start(std::chrono::milliseconds(CURSOR_BLINK_INTERVAL));
|
|
|
|
if (!blink && _blinkCursorTimer.isActive()) {
|
|
_blinkCursorTimer.stop();
|
|
if (_cursorBlinking)
|
|
blinkCursorEvent();
|
|
else
|
|
_cursorBlinking = false;
|
|
}
|
|
}
|
|
|
|
void TerminalView::setTerminalCursorShape(int shape)
|
|
{
|
|
_cursorShape = std::clamp(shape, 0, 2);
|
|
requestPaint();
|
|
}
|
|
|
|
void TerminalView::setFullCursorHeight(bool full)
|
|
{
|
|
_fullCursorHeight = full;
|
|
requestPaint();
|
|
}
|
|
|
|
void TerminalView::blinkEvent()
|
|
{
|
|
_blinking = !_blinking;
|
|
requestPaint();
|
|
}
|
|
|
|
void TerminalView::blinkCursorEvent()
|
|
{
|
|
_cursorBlinking = !_cursorBlinking;
|
|
requestPaint();
|
|
}
|
|
|
|
void TerminalView::blinkCursorTimeout()
|
|
{
|
|
// Without the keyboard the cursor shows in its unfocused state; the next
|
|
// paint with it starts blinking again.
|
|
if (!hasKeyboard()) {
|
|
_blinkCursorTimer.stop();
|
|
if (_cursorBlinking)
|
|
blinkCursorEvent();
|
|
return;
|
|
}
|
|
blinkCursorEvent();
|
|
}
|
|
|
|
bool TerminalView::hasKeyboard() const
|
|
{
|
|
const ui::Window* window = this->window();
|
|
return hasFocus() && window->hasKeyboardFocus();
|
|
}
|
|
|
|
void TerminalView::focusIn(ui::FocusReason)
|
|
{
|
|
if (_hasBlinkingCursor && hasKeyboard())
|
|
_blinkCursorTimer.start(std::chrono::milliseconds(CURSOR_BLINK_INTERVAL));
|
|
requestPaint();
|
|
|
|
if (_hasBlinker)
|
|
_blinkTimer.start(std::chrono::milliseconds(TEXT_BLINK_DELAY));
|
|
}
|
|
|
|
void TerminalView::focusOut()
|
|
{
|
|
// The cursor shows while out of focus, drawn in its unfocused state.
|
|
_cursorBlinking = false;
|
|
requestPaint();
|
|
_blinkCursorTimer.stop();
|
|
|
|
if (_blinking)
|
|
blinkEvent();
|
|
|
|
_blinkTimer.stop();
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Resizing */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::layout()
|
|
{
|
|
if (!_scroller && window())
|
|
_scroller = std::make_unique<ui::Scroller>(window(), this);
|
|
|
|
updateFontMetrics();
|
|
updateImageSize();
|
|
requestPaint();
|
|
}
|
|
|
|
void TerminalView::propagateSize()
|
|
{
|
|
if (!_image.empty())
|
|
updateImageSize();
|
|
}
|
|
|
|
void TerminalView::calcGeometry()
|
|
{
|
|
const int width = int(geometry().width);
|
|
const int height = int(geometry().height);
|
|
|
|
_leftMargin = _leftBaseMargin;
|
|
_contentWidth = width - 2 * _leftBaseMargin;
|
|
|
|
_topMargin = _topBaseMargin + kRound(_scrollOffset);
|
|
_contentHeight = height - 2 * _topBaseMargin + /* mysterious */ 1;
|
|
|
|
// ensure that display is always at least one column wide
|
|
_columns = std::max(1, _contentWidth / _metrics.width);
|
|
_usedColumns = std::min(_usedColumns, _columns);
|
|
|
|
// ensure that display is always at least one line high
|
|
_lines = std::max(1, _contentHeight / _metrics.height);
|
|
_usedLines = std::min(_usedLines, _lines);
|
|
}
|
|
|
|
void TerminalView::clearImage()
|
|
{
|
|
// We initialize _image[_imageSize] too. See makeImage()
|
|
for (int i = 0; i <= _imageSize; i++) {
|
|
_image[size_t(i)].character = ' ';
|
|
_image[size_t(i)].foregroundColor = CharacterColor(COLOR_SPACE_DEFAULT, DEFAULT_FORE_COLOR);
|
|
_image[size_t(i)].backgroundColor = CharacterColor(COLOR_SPACE_DEFAULT, DEFAULT_BACK_COLOR);
|
|
_image[size_t(i)].rendition = DEFAULT_RENDITION;
|
|
}
|
|
}
|
|
|
|
void TerminalView::makeImage()
|
|
{
|
|
calcGeometry();
|
|
|
|
// One line more for the line below the window, see _hasLineBelow.
|
|
_imageSize = (_lines + 1) * _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.assign(size_t(_imageSize + 1), Character());
|
|
|
|
clearImage();
|
|
}
|
|
|
|
void TerminalView::updateImageSize()
|
|
{
|
|
if (_scroller) {
|
|
_scroller->cancel(false);
|
|
_scrollOffset = 0;
|
|
}
|
|
|
|
std::vector<Character> oldimg = std::move(_image);
|
|
const int oldlin = _lines;
|
|
const int oldcol = _columns;
|
|
|
|
makeImage();
|
|
|
|
// copy the old image to reduce flicker
|
|
const int lines = std::min(oldlin, _lines);
|
|
const int columns = std::min(oldcol, _columns);
|
|
|
|
if (!oldimg.empty()) {
|
|
for (int line = 0; line < lines; line++) {
|
|
std::memcpy(static_cast<void*>(&_image[size_t(_columns * line)]), &oldimg[size_t(oldcol * line)],
|
|
size_t(columns) * sizeof(Character));
|
|
}
|
|
}
|
|
|
|
if (_screenWindow)
|
|
_screenWindow->setWindowLines(_lines);
|
|
|
|
if (oldlin != _lines || oldcol != _columns) {
|
|
if (_session)
|
|
_session->setViewSize(this, _lines, _columns);
|
|
sizeChanged();
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Display Operations */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::updateLineProperties()
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
_lineProperties = _screenWindow->getLineProperties();
|
|
}
|
|
|
|
void TerminalView::updateImage()
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
if (_image.empty()) {
|
|
// Create _image.
|
|
// The emitted size change also leads to getImage being recreated, so do this first.
|
|
updateImageSize();
|
|
}
|
|
|
|
// Scroll just far enough for output that pushed the cursor out of view.
|
|
if (_followCursor && !_screenWindow->trackOutput()) {
|
|
const int below = cursorLineInWindow() - _screenWindow->windowLines() + 1;
|
|
if (below > 0)
|
|
_screenWindow->scrollTo(_screenWindow->currentLine() + below);
|
|
if (_screenWindow->atEndOfOutput()) {
|
|
_screenWindow->setTrackOutput(true);
|
|
_followCursor = false;
|
|
}
|
|
}
|
|
|
|
Character* const newimg = _screenWindow->getImage();
|
|
const int lines = _screenWindow->windowLines();
|
|
const int columns = _screenWindow->windowColumns();
|
|
|
|
setScroll(_screenWindow->currentLine(), _screenWindow->lineCount());
|
|
|
|
_hasBlinker = false;
|
|
|
|
const int linesToUpdate = std::min(_lines, std::max(0, lines));
|
|
const int columnsToUpdate = std::min(_columns, std::max(0, columns));
|
|
|
|
for (int y = 0; y < linesToUpdate; ++y) {
|
|
Character* currentLine = &_image[size_t(y * _columns)];
|
|
const Character* const newLine = &newimg[y * columns];
|
|
|
|
for (int x = 0; x < columnsToUpdate; ++x) {
|
|
if ((newLine[x].rendition & RE_BLINK) != 0)
|
|
_hasBlinker = true;
|
|
}
|
|
|
|
// replace the line of characters in the old _image with the
|
|
// current line of the new _image
|
|
std::memcpy(static_cast<void*>(currentLine), newLine, size_t(columnsToUpdate) * sizeof(Character));
|
|
}
|
|
|
|
_usedLines = linesToUpdate;
|
|
_usedColumns = columnsToUpdate;
|
|
|
|
const int lineBelow = _screenWindow->currentLine() + lines;
|
|
_hasLineBelow = lineBelow < _screenWindow->lineCount();
|
|
if (_hasLineBelow) {
|
|
std::vector<Character> below(static_cast<size_t>(columns));
|
|
_screenWindow->screen()->getImage(below.data(), columns, lineBelow, lineBelow);
|
|
std::memcpy(static_cast<void*>(&_image[size_t(linesToUpdate * _columns)]), below.data(),
|
|
size_t(columnsToUpdate) * sizeof(Character));
|
|
}
|
|
|
|
requestPaint();
|
|
|
|
if (_hasBlinker && !_blinkTimer.isActive())
|
|
_blinkTimer.start(std::chrono::milliseconds(TEXT_BLINK_DELAY));
|
|
if (!_hasBlinker && _blinkTimer.isActive()) {
|
|
_blinkTimer.stop();
|
|
_blinking = false;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Scrollbar */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::setScrollBarValueQuietly(int value)
|
|
{
|
|
_scrollValue = std::clamp(value, 0, _scrollMaximum);
|
|
}
|
|
|
|
// ScrollBar::setValue(): changes are what scrollBarPositionChanged() follows.
|
|
void TerminalView::setScrollbarValue(int value)
|
|
{
|
|
const int newValue = std::clamp(value, 0, _scrollMaximum);
|
|
if (newValue == _scrollValue)
|
|
return;
|
|
_scrollValue = newValue;
|
|
scrollbarParamsChanged();
|
|
refreshLinkHover();
|
|
scrollBarPositionChanged();
|
|
}
|
|
|
|
void TerminalView::scrollBarPositionChanged()
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
if (!_applyingScroll) {
|
|
if (_scroller)
|
|
_scroller->cancel(false);
|
|
setScrollOffset(0);
|
|
}
|
|
_followCursor = false;
|
|
|
|
_screenWindow->scrollTo(_scrollValue);
|
|
|
|
// 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 = (_scrollValue == _scrollMaximum);
|
|
_screenWindow->setTrackOutput(atEndOfOutput);
|
|
|
|
updateImage();
|
|
|
|
scrollbarValueChanged();
|
|
}
|
|
|
|
void TerminalView::setScroll(int cursor, int slines)
|
|
{
|
|
// Before the first layout the display is a one-line fallback, so the whole screen would read as scrollback.
|
|
const int maxScrollValue = geometry().height > 0 ? std::max(0, slines - _lines) : 0;
|
|
const int boundedCursor = std::clamp(cursor, 0, maxScrollValue);
|
|
|
|
if (_scrollMaximum == maxScrollValue && _scrollValue == boundedCursor)
|
|
return;
|
|
|
|
const int previousValue = _scrollValue;
|
|
const bool rangeChanged = _scrollMaximum != maxScrollValue;
|
|
_scrollMaximum = maxScrollValue;
|
|
setScrollBarValueQuietly(_scrollValue);
|
|
const bool clamped = _scrollValue != previousValue;
|
|
setScrollBarValueQuietly(boundedCursor);
|
|
if (rangeChanged || clamped || _scrollValue != previousValue) {
|
|
scrollbarParamsChanged();
|
|
refreshLinkHover();
|
|
}
|
|
|
|
// The view moved under a resting partial scroll, or the scrollback went away.
|
|
if (maxScrollValue == 0 || (_scrollValue != previousValue && !(_scroller && _scroller->isActive()))) {
|
|
if (_scroller)
|
|
_scroller->cancel(false);
|
|
setScrollOffset(0);
|
|
}
|
|
}
|
|
|
|
int TerminalView::cursorLineInWindow() const
|
|
{
|
|
Screen* screen = _screenWindow->screen();
|
|
return screen->getHistLines() + screen->getCursorY() - _screenWindow->currentLine();
|
|
}
|
|
|
|
// A short scrollback over a mostly empty screen: jumping to the end would
|
|
// push the prompt up and show the empty lines instead.
|
|
void TerminalView::revealCursor()
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
const int line = cursorLineInWindow();
|
|
if (line < 0 || line >= _screenWindow->windowLines()) {
|
|
scrollToEnd();
|
|
return;
|
|
}
|
|
|
|
_followCursor = !_screenWindow->atEndOfOutput();
|
|
}
|
|
|
|
void TerminalView::scrollToEnd()
|
|
{
|
|
_followCursor = false;
|
|
if (_scroller)
|
|
_scroller->cancel(false);
|
|
setScrollOffset(0);
|
|
|
|
if (_scrollValue != _scrollMaximum) {
|
|
setScrollBarValueQuietly(_scrollMaximum);
|
|
scrollbarParamsChanged();
|
|
}
|
|
|
|
_screenWindow->scrollTo(_scrollValue + 1);
|
|
_screenWindow->setTrackOutput(_screenWindow->atEndOfOutput());
|
|
}
|
|
|
|
double TerminalView::scrollPixelPosition() const
|
|
{
|
|
return _scrollValue * double(_metrics.height) - _scrollOffset;
|
|
}
|
|
|
|
double TerminalView::scrollPixelMaximum() const
|
|
{
|
|
return _scrollMaximum * double(_metrics.height);
|
|
}
|
|
|
|
// Past either end the whole text moves with the stretch.
|
|
void TerminalView::setScrollPixelPosition(double position)
|
|
{
|
|
const int maximum = _scrollMaximum;
|
|
const double bounded = std::clamp(position, 0.0, maximum * double(_metrics.height));
|
|
const int line = std::min(maximum, int(bounded / _metrics.height));
|
|
|
|
_applyingScroll = true;
|
|
setScrollbarValue(line);
|
|
_applyingScroll = false;
|
|
setScrollOffset(line * double(_metrics.height) - position);
|
|
}
|
|
|
|
void TerminalView::setScrollOffset(double offset)
|
|
{
|
|
_scrollOffset = offset;
|
|
const int topMargin = _topBaseMargin + kRound(offset);
|
|
if (topMargin == _topMargin)
|
|
return;
|
|
|
|
_topMargin = topMargin;
|
|
requestPaint();
|
|
}
|
|
|
|
int TerminalView::drawnLines() const
|
|
{
|
|
return _usedLines + (_topMargin < _topBaseMargin && _hasLineBelow ? 1 : 0);
|
|
}
|
|
|
|
// The scroller's axis: the scrollback in pixels.
|
|
ui::Scroller::Axis TerminalView::horizontalAxis() const
|
|
{
|
|
return {};
|
|
}
|
|
|
|
ui::Scroller::Axis TerminalView::verticalAxis() const
|
|
{
|
|
return {0, scrollPixelMaximum(), geometry().height, _scrollMaximum > 0};
|
|
}
|
|
|
|
ui::PointF TerminalView::position() const
|
|
{
|
|
return {0, scrollPixelPosition()};
|
|
}
|
|
|
|
void TerminalView::applyPosition(ui::PointF position)
|
|
{
|
|
setScrollPixelPosition(position.y);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Mouse */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::sendMouse(int button, int column, int line, int eventType)
|
|
{
|
|
if (_session)
|
|
_session->emulation()->sendMouseEvent(button, column, line, eventType);
|
|
}
|
|
|
|
void TerminalView::getCharacterPosition(ui::PointF point, int& line, int& column) const
|
|
{
|
|
line = int((point.y - _topMargin) / _metrics.height);
|
|
if (line < 0)
|
|
line = 0;
|
|
if (line >= _usedLines)
|
|
line = _usedLines - 1;
|
|
|
|
const int x = int(point.x + _metrics.width / 2 - _leftMargin);
|
|
column = x / _metrics.width;
|
|
|
|
if (column < 0)
|
|
column = 0;
|
|
|
|
// 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 TerminalView::getCharacterCellAt(ui::PointF point, int& line, int& column) const
|
|
{
|
|
getCharacterPosition({point.x - _metrics.width / 2.0, point.y}, line, column);
|
|
column = std::clamp(column, 0, std::max(0, _usedColumns - 1));
|
|
}
|
|
|
|
// QMouseEvent::pos(): the position rounded to whole pixels.
|
|
static ui::PointF eventPosition(double x, double y)
|
|
{
|
|
return {double(kRound(x)), double(kRound(y))};
|
|
}
|
|
|
|
void TerminalView::simulateMousePress(double x, double y, ui::MouseButton button, uint32_t modifiers)
|
|
{
|
|
const ui::PointF pos = eventPosition(x, y);
|
|
if (_possibleTripleClick && button == ui::MouseButton::Left) {
|
|
mouseTripleClick(pos);
|
|
return;
|
|
}
|
|
|
|
// contentsRect().contains(ev->pos()).
|
|
if (pos.x < 0 || pos.y < 0 || pos.x > int(geometry().width) - 1 || pos.y > int(geometry().height) - 1)
|
|
return;
|
|
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
int charLine;
|
|
int charColumn;
|
|
getCharacterPosition(pos, charLine, charColumn);
|
|
Point point(charColumn, charLine);
|
|
|
|
if (button == ui::MouseButton::Left) {
|
|
_lineSelectionMode = false;
|
|
_wordSelectionMode = false;
|
|
|
|
const bool selected = _screenWindow->isSelected(point.x(), point.y());
|
|
|
|
if (selected) {
|
|
// The user clicked inside selected text
|
|
_dragState = DragPending;
|
|
_dragStart = pos;
|
|
} else {
|
|
// No reason to ever start a drag event
|
|
_dragState = DragNone;
|
|
|
|
_preserveLineBreaks = !((modifiers & ui::ControlModifier) && !(modifiers & ui::AltModifier));
|
|
_columnSelectionMode = (modifiers & ui::AltModifier) && (modifiers & ui::ControlModifier);
|
|
|
|
if (_mouseMarks || (modifiers & ui::ShiftModifier)) {
|
|
_screenWindow->clearSelection();
|
|
|
|
point.setY(point.y() + _scrollValue);
|
|
_iPntSel = _pntSel = point;
|
|
_actSel = 1; // left mouse button pressed but nothing selected yet.
|
|
} else {
|
|
sendMouse(0, charColumn + 1, charLine + 1 + _scrollValue - _scrollMaximum, 0);
|
|
}
|
|
}
|
|
} else if (button == ui::MouseButton::Middle) {
|
|
if (_mouseMarks || (modifiers & ui::ShiftModifier))
|
|
emitSelection(true, modifiers & ui::ControlModifier);
|
|
else
|
|
sendMouse(1, charColumn + 1, charLine + 1 + _scrollValue - _scrollMaximum, 0);
|
|
} else if (button == ui::MouseButton::Right) {
|
|
if (!_mouseMarks && !(modifiers & ui::ShiftModifier))
|
|
sendMouse(2, charColumn + 1, charLine + 1 + _scrollValue - _scrollMaximum, 0);
|
|
}
|
|
}
|
|
|
|
void TerminalView::simulateMouseMove(double x, double y, uint32_t buttons, uint32_t modifiers)
|
|
{
|
|
const ui::PointF pos = eventPosition(x, y);
|
|
int charLine = 0;
|
|
int charColumn = 0;
|
|
getCharacterPosition(pos, charLine, charColumn);
|
|
|
|
if (buttons == 0)
|
|
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 && !(modifiers & ui::ShiftModifier)) {
|
|
int button = 3;
|
|
if (buttons & (1u << int(ui::MouseButton::Left)))
|
|
button = 0;
|
|
if (buttons & (1u << int(ui::MouseButton::Middle)))
|
|
button = 1;
|
|
if (buttons & (1u << int(ui::MouseButton::Right)))
|
|
button = 2;
|
|
|
|
sendMouse(button, charColumn + 1, charLine + 1 + _scrollValue - _scrollMaximum, 1);
|
|
return;
|
|
}
|
|
|
|
if (_dragState == DragPending) {
|
|
// we had a mouse down, but haven't confirmed a drag yet
|
|
// if the mouse has moved sufficiently, we will confirm
|
|
const int distance = START_DRAG_DISTANCE;
|
|
if (pos.x > _dragStart.x + distance || pos.x < _dragStart.x - distance
|
|
|| pos.y > _dragStart.y + distance || pos.y < _dragStart.y - distance) {
|
|
// we've left the drag square, we can start a real drag operation now
|
|
doDrag();
|
|
}
|
|
return;
|
|
} else if (_dragState == DragDragging) {
|
|
return;
|
|
}
|
|
|
|
if (_actSel == 0)
|
|
return;
|
|
|
|
// don't extend selection while pasting
|
|
if (buttons & (1u << int(ui::MouseButton::Middle)))
|
|
return;
|
|
|
|
extendSelection(pos);
|
|
}
|
|
|
|
void TerminalView::doDrag()
|
|
{
|
|
_dragState = DragDragging;
|
|
|
|
const std::string text = _screenWindow ? _screenWindow->selectedText(_preserveLineBreaks) : std::string();
|
|
ui::DragSource source;
|
|
source.mimeTypes = {"text/plain;charset=utf-8", "text/plain"};
|
|
source.data = [text](const std::string&) { return text; };
|
|
source.actions = ui::CopyAction;
|
|
source.onFinished = [this](ui::DropAction) { _dragState = DragNone; };
|
|
startDrag(std::move(source));
|
|
}
|
|
|
|
void TerminalView::extendSelection(ui::PointF position)
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
const int scroll = _scrollValue;
|
|
|
|
// 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(left, top, width, height) with right = left + width - 1.
|
|
const int textLeft = _leftMargin;
|
|
const int textTop = _topMargin;
|
|
const int textRight = textLeft + _usedColumns * _metrics.width - 1 - 1;
|
|
const int textBottom = textTop + _usedLines * _metrics.height - 1 - 1;
|
|
|
|
// Adjust position within text area bounds.
|
|
const int oldX = int(position.x);
|
|
const int oldY = int(position.y);
|
|
ui::PointF pos{double(std::clamp(oldX, textLeft, std::max(textLeft, textRight))),
|
|
double(std::clamp(oldY, textTop, std::max(textTop, textBottom)))};
|
|
|
|
if (oldY > textBottom) {
|
|
linesBeyondWidget = (oldY - textBottom) / _metrics.height;
|
|
setScrollbarValue(_scrollValue + linesBeyondWidget + 1); // scrollforward
|
|
}
|
|
if (oldY < textTop) {
|
|
linesBeyondWidget = (textTop - oldY) / _metrics.height;
|
|
setScrollbarValue(_scrollValue - linesBeyondWidget - 1); // history
|
|
}
|
|
|
|
int charColumn = 0;
|
|
int charLine = 0;
|
|
getCharacterPosition(pos, charLine, charColumn);
|
|
|
|
Point here(charColumn, charLine);
|
|
Point ohere;
|
|
Point iPntSelCorr = _iPntSel;
|
|
iPntSelCorr.setY(iPntSelCorr.y() - _scrollValue);
|
|
Point pntSelCorr = _pntSel;
|
|
pntSelCorr.setY(pntSelCorr.y() - _scrollValue);
|
|
bool swapping = false;
|
|
|
|
if (_wordSelectionMode) {
|
|
// Extend to word boundaries
|
|
int i;
|
|
char32_t 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)
|
|
Point left = left_not_right ? here : iPntSelCorr;
|
|
i = loc(left.x(), left.y());
|
|
if (i >= 0 && i <= _imageSize) {
|
|
selClass = charClass(_image[size_t(i)]);
|
|
while (((left.x() > 0) || (left.y() > 0 && (_lineProperties[size_t(left.y() - 1)] & LINE_WRAPPED)))
|
|
&& charClass(_image[size_t(i - 1)]) == selClass) {
|
|
i--;
|
|
if (left.x() > 0) {
|
|
left.setX(left.x() - 1);
|
|
} else {
|
|
left.setX(_usedColumns - 1);
|
|
left.setY(left.y() - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find left (left_not_right ? from start : from here)
|
|
Point right = left_not_right ? iPntSelCorr : here;
|
|
i = loc(right.x(), right.y());
|
|
if (i >= 0 && i <= _imageSize) {
|
|
selClass = charClass(_image[size_t(i)]);
|
|
while (((right.x() < _usedColumns - 1)
|
|
|| (right.y() < _usedLines - 1 && (_lineProperties[size_t(right.y())] & LINE_WRAPPED)))
|
|
&& charClass(_image[size_t(i + 1)]) == selClass) {
|
|
i++;
|
|
if (right.x() < _usedColumns - 1) {
|
|
right.setX(right.x() + 1);
|
|
} else {
|
|
right.setX(0);
|
|
right.setY(right.y() + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pick which is start (ohere) and which is extension (here)
|
|
if (left_not_right) {
|
|
here = left;
|
|
ohere = right;
|
|
} else {
|
|
here = right;
|
|
ohere = left;
|
|
}
|
|
ohere.setX(ohere.x() + 1);
|
|
}
|
|
|
|
if (_lineSelectionMode) {
|
|
// Extend to complete line
|
|
const bool above_not_below = (here.y() < iPntSelCorr.y());
|
|
|
|
Point above = above_not_below ? here : iPntSelCorr;
|
|
Point below = above_not_below ? iPntSelCorr : here;
|
|
|
|
while (above.y() > 0 && (_lineProperties[size_t(above.y() - 1)] & LINE_WRAPPED))
|
|
above.setY(above.y() - 1);
|
|
while (below.y() < _usedLines - 1 && (_lineProperties[size_t(below.y())] & LINE_WRAPPED))
|
|
below.setY(below.y() + 1);
|
|
|
|
above.setX(0);
|
|
below.setX(_usedColumns - 1);
|
|
|
|
// Pick which is start (ohere) and which is extension (here)
|
|
if (above_not_below) {
|
|
here = above;
|
|
ohere = below;
|
|
} else {
|
|
here = below;
|
|
ohere = above;
|
|
}
|
|
|
|
const Point newSelBegin(ohere.x(), ohere.y());
|
|
swapping = !(_tripleSelBegin == newSelBegin);
|
|
_tripleSelBegin = newSelBegin;
|
|
|
|
ohere.setX(ohere.x() + 1);
|
|
}
|
|
|
|
int offset = 0;
|
|
if (!_wordSelectionMode && !_lineSelectionMode) {
|
|
const bool left_not_right = (here.y() < iPntSelCorr.y()
|
|
|| (here.y() == iPntSelCorr.y() && here.x() < iPntSelCorr.x()));
|
|
const bool old_left_not_right = (pntSelCorr.y() < iPntSelCorr.y()
|
|
|| (pntSelCorr.y() == iPntSelCorr.y() && pntSelCorr.x() < iPntSelCorr.x()));
|
|
swapping = left_not_right != old_left_not_right;
|
|
|
|
// Find left (left_not_right ? from here : from start)
|
|
const Point left = left_not_right ? here : iPntSelCorr;
|
|
|
|
// Find left (left_not_right ? from start : from here)
|
|
const Point right = left_not_right ? iPntSelCorr : here;
|
|
|
|
// Pick which is start (ohere) and which is extension (here)
|
|
if (left_not_right) {
|
|
here = left;
|
|
ohere = right;
|
|
offset = 0;
|
|
} else {
|
|
here = right;
|
|
ohere = left;
|
|
offset = -1;
|
|
}
|
|
}
|
|
|
|
if ((here == pntSelCorr) && (scroll == _scrollValue))
|
|
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.setY(_pntSel.y() + _scrollValue);
|
|
|
|
if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode)
|
|
_screenWindow->setSelectionEnd(here.x(), here.y());
|
|
else
|
|
_screenWindow->setSelectionEnd(here.x() + offset, here.y());
|
|
}
|
|
|
|
void TerminalView::simulateMouseRelease(double x, double y, ui::MouseButton button, uint32_t modifiers)
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
int charLine;
|
|
int charColumn;
|
|
getCharacterPosition(eventPosition(x, y), charLine, charColumn);
|
|
|
|
if (button == ui::MouseButton::Left) {
|
|
if (_dragState == DragPending) {
|
|
// We had a drag event pending but never confirmed. Kill selection
|
|
_screenWindow->clearSelection();
|
|
} else {
|
|
if (_actSel > 1)
|
|
setSelection(_screenWindow->selectedText(_preserveLineBreaks));
|
|
|
|
_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 && !(modifiers & ui::ShiftModifier))
|
|
sendMouse(0, charColumn + 1, charLine + 1 + _scrollValue - _scrollMaximum, 2);
|
|
}
|
|
_dragState = DragNone;
|
|
}
|
|
|
|
if (!_mouseMarks
|
|
&& ((button == ui::MouseButton::Right && !(modifiers & ui::ShiftModifier))
|
|
|| button == ui::MouseButton::Middle)) {
|
|
sendMouse(button == ui::MouseButton::Middle ? 1 : 2, charColumn + 1,
|
|
charLine + 1 + _scrollValue - _scrollMaximum, 2);
|
|
}
|
|
}
|
|
|
|
void TerminalView::simulateMouseDoubleClick(double x, double y, ui::MouseButton button, uint32_t modifiers)
|
|
{
|
|
if (button != ui::MouseButton::Left)
|
|
return;
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
int charLine = 0;
|
|
int charColumn = 0;
|
|
getCharacterPosition(eventPosition(x, y), charLine, charColumn);
|
|
|
|
// pass on double click as two clicks.
|
|
if (!_mouseMarks && !(modifiers & ui::ShiftModifier)) {
|
|
// Send just _ONE_ click event, since the first click of the double click
|
|
// was already sent by the click handler
|
|
sendMouse(0, charColumn + 1, charLine + 1 + _scrollValue - _scrollMaximum, 0); // left button
|
|
return;
|
|
}
|
|
|
|
_screenWindow->clearSelection();
|
|
_iPntSel = Point(charColumn, charLine + _scrollValue);
|
|
|
|
_wordSelectionMode = true;
|
|
|
|
int beginColumn, beginLine, endColumn, endLine;
|
|
findWordBounds(charColumn, charLine, beginColumn, beginLine, endColumn, endLine);
|
|
|
|
_screenWindow->setSelectionStart(beginColumn, beginLine, false);
|
|
|
|
_actSel = 2; // within selection
|
|
|
|
_screenWindow->setSelectionEnd(endColumn, endLine);
|
|
|
|
setSelection(_screenWindow->selectedText(_preserveLineBreaks));
|
|
|
|
_possibleTripleClick = true;
|
|
|
|
_tripleClickTimer.start(std::chrono::milliseconds(DOUBLE_CLICK_INTERVAL));
|
|
}
|
|
|
|
void TerminalView::findWordBounds(int column, int line, int& beginColumn, int& beginLine, int& endColumn,
|
|
int& endLine) const
|
|
{
|
|
beginColumn = endColumn = column;
|
|
beginLine = endLine = line;
|
|
int i = loc(column, line);
|
|
|
|
const char32_t selClass = charClass(_image[size_t(i)]);
|
|
|
|
// find the start of the word
|
|
int x = column;
|
|
while (((x > 0) || (beginLine > 0 && (_lineProperties[size_t(beginLine - 1)] & LINE_WRAPPED)))
|
|
&& charClass(_image[size_t(i - 1)]) == selClass) {
|
|
i--;
|
|
if (x > 0) {
|
|
x--;
|
|
} else {
|
|
x = _usedColumns - 1;
|
|
beginLine--;
|
|
}
|
|
}
|
|
|
|
beginColumn = x;
|
|
|
|
// find the end of the word
|
|
i = loc(column, line);
|
|
x = column;
|
|
while (((x < _usedColumns - 1) || (endLine < _usedLines - 1 && (_lineProperties[size_t(endLine)] & LINE_WRAPPED)))
|
|
&& charClass(_image[size_t(i + 1)]) == selClass) {
|
|
i++;
|
|
if (x < _usedColumns - 1) {
|
|
x++;
|
|
} else {
|
|
x = 0;
|
|
endLine++;
|
|
}
|
|
}
|
|
|
|
endColumn = x;
|
|
|
|
// In word selection mode don't select @ (64) if at end of word.
|
|
if (char16_t(_image[size_t(i)].character) == u'@' && endColumn - beginColumn > 0
|
|
&& (_image[size_t(i)].rendition & RE_EXTENDED_CHAR) == 0) {
|
|
endColumn = x - 1;
|
|
}
|
|
}
|
|
|
|
bool TerminalView::isSelectedAt(double x, double y)
|
|
{
|
|
if (!_screenWindow)
|
|
return false;
|
|
|
|
int charLine = 0;
|
|
int charColumn = 0;
|
|
getCharacterCellAt({x, y}, charLine, charColumn);
|
|
return _screenWindow->isSelected(charColumn, charLine);
|
|
}
|
|
|
|
bool TerminalView::selectWordAt(double x, double y)
|
|
{
|
|
if (!_screenWindow || _image.empty())
|
|
return false;
|
|
|
|
int charLine = 0;
|
|
int charColumn = 0;
|
|
getCharacterCellAt({x, y}, charLine, charColumn);
|
|
|
|
if (konsole_isspace(char16_t(_image[size_t(loc(charColumn, charLine))].character)))
|
|
return false;
|
|
|
|
int beginColumn, beginLine, endColumn, endLine;
|
|
findWordBounds(charColumn, charLine, beginColumn, beginLine, endColumn, endLine);
|
|
|
|
_screenWindow->clearSelection();
|
|
_screenWindow->setSelectionStart(beginColumn, beginLine, false);
|
|
_screenWindow->setSelectionEnd(endColumn, endLine);
|
|
setSelection(_screenWindow->selectedText(_preserveLineBreaks));
|
|
return true;
|
|
}
|
|
|
|
void TerminalView::mouseTripleClick(ui::PointF position)
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
int charLine;
|
|
int charColumn;
|
|
getCharacterPosition(position, charLine, charColumn);
|
|
_iPntSel = Point(charColumn, charLine);
|
|
|
|
_screenWindow->clearSelection();
|
|
|
|
_lineSelectionMode = true;
|
|
_wordSelectionMode = false;
|
|
|
|
_actSel = 2; // within selection
|
|
|
|
while (_iPntSel.y() > 0 && (_lineProperties[size_t(_iPntSel.y() - 1)] & LINE_WRAPPED))
|
|
_iPntSel.setY(_iPntSel.y() - 1);
|
|
|
|
// SelectWholeLine.
|
|
_screenWindow->setSelectionStart(0, _iPntSel.y(), false);
|
|
_tripleSelBegin = Point(0, _iPntSel.y());
|
|
|
|
while (_iPntSel.y() < _lines - 1 && (_lineProperties[size_t(_iPntSel.y())] & LINE_WRAPPED))
|
|
_iPntSel.setY(_iPntSel.y() + 1);
|
|
|
|
_screenWindow->setSelectionEnd(_columns - 1, _iPntSel.y());
|
|
|
|
setSelection(_screenWindow->selectedText(_preserveLineBreaks));
|
|
|
|
_iPntSel.setY(_iPntSel.y() + _scrollValue);
|
|
}
|
|
|
|
// QChar::isLetterOrNumber() of a UTF-16 unit.
|
|
static bool isLetterOrNumber(char16_t ch)
|
|
{
|
|
switch (u_charType(ch)) {
|
|
case U_UPPERCASE_LETTER:
|
|
case U_LOWERCASE_LETTER:
|
|
case U_TITLECASE_LETTER:
|
|
case U_MODIFIER_LETTER:
|
|
case U_OTHER_LETTER:
|
|
case U_DECIMAL_DIGIT_NUMBER:
|
|
case U_LETTER_NUMBER:
|
|
case U_OTHER_NUMBER:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// QString::contains(QChar, Qt::CaseInsensitive): QChar::toCaseFolded on both.
|
|
static bool containsIgnoringCase(const std::u16string& text, char16_t ch)
|
|
{
|
|
const UChar32 folded = u_foldCase(ch, U_FOLD_CASE_DEFAULT);
|
|
return std::any_of(text.begin(), text.end(), [folded](char16_t c) {
|
|
return u_foldCase(c, U_FOLD_CASE_DEFAULT) == folded;
|
|
});
|
|
}
|
|
|
|
char32_t TerminalView::charClass(const Character& ch) const
|
|
{
|
|
if (ch.rendition & RE_EXTENDED_CHAR) {
|
|
ushort extendedCharLength = 0;
|
|
const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength);
|
|
if (chars && extendedCharLength > 0) {
|
|
std::wstring str;
|
|
for (ushort nchar = 0; nchar < extendedCharLength; nchar++)
|
|
str.push_back(wchar_t(chars[nchar]));
|
|
std::u16string s;
|
|
TerminalCharacterDecoder::appendWide(s, str);
|
|
// QString::contains(QString, CaseInsensitive).
|
|
std::u16string foldedWords;
|
|
std::u16string foldedText;
|
|
for (char16_t c : _wordCharacters)
|
|
foldedWords.push_back(char16_t(u_foldCase(c, U_FOLD_CASE_DEFAULT)));
|
|
for (char16_t c : s)
|
|
foldedText.push_back(char16_t(u_foldCase(c, U_FOLD_CASE_DEFAULT)));
|
|
if (foldedWords.find(foldedText) != std::u16string::npos)
|
|
return U'a';
|
|
bool allLetterOrNumber = true;
|
|
for (size_t i = 0; allLetterOrNumber && i < s.size(); ++i)
|
|
allLetterOrNumber = isLetterOrNumber(s[i]);
|
|
return allLetterOrNumber ? U'a' : char32_t(s.empty() ? 0 : s[0]);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
const char16_t qch = char16_t(ch.character);
|
|
if (konsole_isspace(qch))
|
|
return U' ';
|
|
if (isLetterOrNumber(qch) || containsIgnoringCase(_wordCharacters, qch))
|
|
return U'a';
|
|
return qch;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Wheel */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
bool TerminalView::scroll(const ui::ScrollEvent& event)
|
|
{
|
|
// A tab moved to another window brings its view along; the scroller ticks on the window's frames.
|
|
if (_scroller && _scroller->window() != window())
|
|
_scroller = window() ? std::make_unique<ui::Scroller>(window(), this) : nullptr;
|
|
|
|
// A touchpad drags the scrollback by the pixel, coasts and stretches at the ends.
|
|
if (_mouseMarks && event.precise && _scrollMaximum > 0 && _scroller) {
|
|
_scroller->handleScroll(event);
|
|
return true;
|
|
}
|
|
|
|
// Qt's angle delta: positive for a wheel turned away, towards the top.
|
|
const int angleDelta = event.precise ? -kRound(event.deltaY) : -event.value120Y;
|
|
if (angleDelta == 0)
|
|
return true;
|
|
|
|
// if the terminal program is not interested mouse events
|
|
// then send the event to the scrollbar if the slider has room to move
|
|
// or otherwise send simulated up / down key presses to the terminal program
|
|
// for the benefit of programs such as 'less'
|
|
if (_mouseMarks) {
|
|
int lines;
|
|
if (event.precise && _metrics.height > 0) {
|
|
// Touchpads report pixels: one line per line of text scrolled.
|
|
_wheelPixelRemainder += angleDelta;
|
|
lines = _wheelPixelRemainder / _metrics.height;
|
|
_wheelPixelRemainder -= lines * _metrics.height;
|
|
} else {
|
|
// A wheel notch is 120; high-resolution wheels send fractions of one.
|
|
_wheelAngleRemainder += angleDelta * WHEEL_SCROLL_LINES;
|
|
lines = _wheelAngleRemainder / 120;
|
|
_wheelAngleRemainder -= lines * 120;
|
|
}
|
|
if (lines == 0)
|
|
return true;
|
|
|
|
if (_scrollMaximum > 0) {
|
|
setScrollbarValue(_scrollValue - lines);
|
|
} else {
|
|
// A program without mouse reporting, such as less, scrolls on Up / Down.
|
|
KeyEvent keyScrollEvent{lines > 0 ? Key_Up : Key_Down, NoModifier, std::string()};
|
|
for (int i = 0; i < std::abs(lines); i++)
|
|
keyPressed(keyScrollEvent, false);
|
|
}
|
|
} else {
|
|
// terminal program wants notification of mouse activity
|
|
int charLine;
|
|
int charColumn;
|
|
const ui::Window* window = this->window();
|
|
(void)window;
|
|
getCharacterPosition(_lastPointer, charLine, charColumn);
|
|
|
|
sendMouse(angleDelta > 0 ? 4 : 5, charColumn + 1, charLine + 1 + _scrollValue - _scrollMaximum, 0);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Keyboard */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::keyPressed(KeyEvent& event, bool fromPaste)
|
|
{
|
|
if (_session)
|
|
_session->emulation()->sendKeyEvent(&event, fromPaste);
|
|
}
|
|
|
|
void TerminalView::restartCursorBlink()
|
|
{
|
|
if (_hasBlinkingCursor) {
|
|
_blinkCursorTimer.start(std::chrono::milliseconds(CURSOR_BLINK_INTERVAL));
|
|
if (_cursorBlinking)
|
|
blinkCursorEvent();
|
|
else
|
|
_cursorBlinking = false;
|
|
}
|
|
}
|
|
|
|
void TerminalView::sendKeyEvent(KeyEvent& event)
|
|
{
|
|
_actSel = 0; // Key stroke implies a screen update, so TerminalDisplay won't
|
|
// know where the current selection is.
|
|
|
|
restartCursorBlink();
|
|
keyPressed(event, false);
|
|
}
|
|
|
|
// libxkbcommon's control transformation, which Qt's key text carries.
|
|
static char32_t toControl(char32_t c)
|
|
{
|
|
if ((c >= '@' && c < 0x7f) || c == ' ')
|
|
return c & 0x1f;
|
|
if (c == '2')
|
|
return 0;
|
|
if (c >= '3' && c <= '7')
|
|
return c - ('3' - 0x1b);
|
|
if (c == '8')
|
|
return 0x7f;
|
|
if (c == '/')
|
|
return '_' & 0x1f;
|
|
return c;
|
|
}
|
|
|
|
KeyEvent TerminalView::keyEventFor(const ui::KeyEvent& event)
|
|
{
|
|
KeyEvent key;
|
|
if (event.modifiers & ui::ShiftModifier)
|
|
key.modifiers |= ShiftModifier;
|
|
if (event.modifiers & ui::ControlModifier)
|
|
key.modifiers |= ControlModifier;
|
|
if (event.modifiers & ui::AltModifier)
|
|
key.modifiers |= AltModifier;
|
|
if (event.modifiers & ui::SuperModifier)
|
|
key.modifiers |= MetaModifier;
|
|
|
|
// The toolkit's key is the lower-case keysym; the keypad's keep their own.
|
|
uint32_t keysym = event.key;
|
|
if (isKeypadKeysym(keysym))
|
|
key.modifiers |= KeypadModifier;
|
|
|
|
// Qt's text is xkb's, control characters included, which the toolkit leaves out.
|
|
key.text = event.text;
|
|
if (key.text.empty()) {
|
|
char32_t c = xkb_keysym_to_utf32(keysym);
|
|
if (c != 0 && (event.modifiers & ui::ControlModifier) && c < 0x80)
|
|
c = toControl(c);
|
|
if (c != 0 && (c < 0x20 || c == 0x7f))
|
|
strings::appendUtf8(key.text, c);
|
|
}
|
|
|
|
// The text without the control transformation names the key.
|
|
std::string plain;
|
|
if (char32_t c = xkb_keysym_to_utf32(keysym); c >= 0x20 && c != 0x7f)
|
|
strings::appendUtf8(plain, c);
|
|
key.key = keyCodeFromKeysym(keysym, key.modifiers, plain);
|
|
// Shift+Tab reaches the program as Backtab, as Qt names it.
|
|
if (key.key == Key_Tab && (key.modifiers & ShiftModifier))
|
|
key.key = Key_Backtab;
|
|
return key;
|
|
}
|
|
|
|
bool TerminalView::keyPress(const ui::KeyEvent& event)
|
|
{
|
|
if (event.key == XKB_KEY_Control_L || event.key == XKB_KEY_Control_R) {
|
|
_ctrlHeld = true;
|
|
updatePointerCursor();
|
|
}
|
|
if (onShortcut && onShortcut(event))
|
|
return true;
|
|
|
|
KeyEvent key = keyEventFor(event);
|
|
sendKeyEvent(key);
|
|
return true;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Input method */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
ui::TextInputState TerminalView::textInputState() const
|
|
{
|
|
ui::TextInputState state;
|
|
const Point cursor = _screenWindow ? _screenWindow->cursorPosition() : Point(0, 0);
|
|
state.cursorRectangle = {double(_leftMargin + _metrics.width * cursor.x()),
|
|
double(_topMargin + _metrics.height * cursor.y()), double(_metrics.width),
|
|
double(_metrics.height)};
|
|
const ui::PointF origin = mapToWindow({0, 0});
|
|
state.cursorRectangle.x += origin.x;
|
|
state.cursorRectangle.y += origin.y;
|
|
// ImhNoPredictiveText | ImhNoAutoUppercase.
|
|
state.hints = ui::NoTextHint;
|
|
return state;
|
|
}
|
|
|
|
void TerminalView::textInput(const ui::TextInputEvent& event)
|
|
{
|
|
if (!event.commit.empty()) {
|
|
KeyEvent key{0, NoModifier, event.commit};
|
|
keyPressed(key, false);
|
|
}
|
|
|
|
std::wstring preedit;
|
|
for (char32_t c : strings::utf8ToUtf32(event.preedit))
|
|
preedit.push_back(wchar_t(c));
|
|
_preedit = preedit;
|
|
requestPaint();
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Clipboard */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::setSelection(const std::string& text)
|
|
{
|
|
if (ui::Clipboard::supportsSelection())
|
|
ui::Clipboard::setText(text, ui::ClipboardMode::Selection);
|
|
}
|
|
|
|
bool TerminalView::hasSelection() const
|
|
{
|
|
return _screenWindow && !_screenWindow->selectedText(false).empty();
|
|
}
|
|
|
|
void TerminalView::selectAll()
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
_screenWindow->selectAll();
|
|
setSelection(_screenWindow->selectedText(_preserveLineBreaks));
|
|
}
|
|
|
|
std::string TerminalView::selectedText() const
|
|
{
|
|
return _screenWindow ? _screenWindow->selectedText(_preserveLineBreaks) : std::string();
|
|
}
|
|
|
|
void TerminalView::copyClipboard()
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
const std::string text = _screenWindow->selectedText(_preserveLineBreaks);
|
|
if (!text.empty())
|
|
ui::Clipboard::setText(text);
|
|
}
|
|
|
|
void TerminalView::pasteClipboard()
|
|
{
|
|
emitSelection(false, false);
|
|
}
|
|
|
|
void TerminalView::pasteSelection()
|
|
{
|
|
emitSelection(true, false);
|
|
}
|
|
|
|
void TerminalView::emitSelection(bool useXselection, bool appendReturn)
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
// Paste Clipboard by simulating keypress events
|
|
auto paste = [weak = std::weak_ptr<bool>(_alive), this, appendReturn](std::string text) {
|
|
if (!weak.lock())
|
|
return;
|
|
pasteText(text, appendReturn);
|
|
};
|
|
ui::Clipboard::requestText(paste, useXselection ? ui::ClipboardMode::Selection : ui::ClipboardMode::Clipboard);
|
|
}
|
|
|
|
// QString::replace(QRegularExpression("\\r+$"), ""): trailing carriage returns.
|
|
static void trimTrailingReturns(std::u16string& text)
|
|
{
|
|
while (!text.empty() && text.back() == u'\r')
|
|
text.pop_back();
|
|
}
|
|
|
|
static void replaceAll(std::u16string& text, const std::u16string& from, const std::u16string& to)
|
|
{
|
|
size_t position = 0;
|
|
while ((position = text.find(from, position)) != std::u16string::npos) {
|
|
text.replace(position, from.size(), to);
|
|
position += to.size();
|
|
}
|
|
}
|
|
|
|
void TerminalView::pasteText(const std::string& utf8, bool appendReturn)
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
std::u16string text = strings::utf8ToUtf16(utf8);
|
|
if (text.empty())
|
|
return;
|
|
|
|
replaceAll(text, u"\r\n", u"\n");
|
|
std::replace(text.begin(), text.end(), u'\n', u'\r');
|
|
|
|
if (_trimPastedTrailingNewlines)
|
|
trimTrailingReturns(text);
|
|
|
|
if (_confirmMultilinePaste && text.find(u'\r') != std::u16string::npos) {
|
|
_pendingPaste = text;
|
|
_pendingPasteAppendReturn = appendReturn;
|
|
_pendingPasteIsDrop = false;
|
|
std::u16string shown = text;
|
|
std::replace(shown.begin(), shown.end(), u'\r', u'\n');
|
|
multilinePasteRequested(strings::utf16ToUtf8(shown));
|
|
return;
|
|
}
|
|
|
|
sendPaste(text, appendReturn);
|
|
}
|
|
|
|
void TerminalView::sendPaste(std::u16string text, bool appendReturn)
|
|
{
|
|
if (!_screenWindow)
|
|
return;
|
|
|
|
bracketText(text);
|
|
|
|
// appendReturn is intentionally handled _after_ enclosing texts with brackets as
|
|
// that feature is used to allow execution of commands immediately after paste.
|
|
if (appendReturn)
|
|
text.push_back(u'\r');
|
|
|
|
KeyEvent e{0, NoModifier, strings::utf16ToUtf8(text)};
|
|
keyPressed(e, true); // expose as a big fat keypress event
|
|
|
|
_screenWindow->clearSelection();
|
|
}
|
|
|
|
void TerminalView::bracketText(std::u16string& text) const
|
|
{
|
|
if (_bracketedPasteMode) {
|
|
text.insert(0, u"\033[200~");
|
|
text.append(u"\033[201~");
|
|
}
|
|
}
|
|
|
|
void TerminalView::confirmPaste()
|
|
{
|
|
if (_pendingPaste.empty())
|
|
return;
|
|
|
|
const std::u16string text = _pendingPaste;
|
|
_pendingPaste.clear();
|
|
|
|
if (_pendingPasteIsDrop) {
|
|
const std::string bytes = strings::utf16ToUtf8(text);
|
|
if (_session)
|
|
_session->emulation()->sendString(bytes.c_str());
|
|
} else {
|
|
sendPaste(text, _pendingPasteAppendReturn);
|
|
}
|
|
}
|
|
|
|
void TerminalView::dropText(const std::string& utf8)
|
|
{
|
|
std::u16string dropText = strings::utf8ToUtf16(utf8);
|
|
replaceAll(dropText, u"\r\n", u"\n");
|
|
std::replace(dropText.begin(), dropText.end(), u'\n', u'\r');
|
|
if (_trimPastedTrailingNewlines)
|
|
trimTrailingReturns(dropText);
|
|
if (_confirmMultilinePaste && dropText.find(u'\r') != std::u16string::npos) {
|
|
_pendingPaste = dropText;
|
|
_pendingPasteIsDrop = true;
|
|
std::u16string shown = dropText;
|
|
std::replace(shown.begin(), shown.end(), u'\r', u'\n');
|
|
multilinePasteRequested(strings::utf16ToUtf8(shown));
|
|
return;
|
|
}
|
|
|
|
const std::string bytes = strings::utf16ToUtf8(dropText);
|
|
if (_session)
|
|
_session->emulation()->sendString(bytes.c_str());
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Bell */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::bell()
|
|
{
|
|
if (_bellMode == NoBell)
|
|
return;
|
|
|
|
//limit the rate at which bells can occur
|
|
//...mainly for sound effects where rapid bells in sequence
|
|
//produce a horrible noise
|
|
if (_allowBell) {
|
|
_allowBell = false;
|
|
_bellTimer.start(std::chrono::milliseconds(500));
|
|
|
|
if (_bellMode == SystemBeepBell) {
|
|
// QApplication::beep() does nothing on Wayland.
|
|
} else if (_bellMode == NotifyBell) {
|
|
notifyBell();
|
|
} else if (_bellMode == VisualBell) {
|
|
swapColorTable();
|
|
_swapTimer.start(std::chrono::milliseconds(200));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Links */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
UrlFilter::HotSpot* TerminalView::linkHotSpotAt(TerminalImageFilterChain& chain, double x, double y)
|
|
{
|
|
if (_image.empty())
|
|
return nullptr;
|
|
|
|
int charLine = 0;
|
|
int charColumn = 0;
|
|
getCharacterCellAt({x, y}, charLine, charColumn);
|
|
|
|
// Matched on demand against the text on display.
|
|
chain.addFilter(new UrlFilter());
|
|
chain.setImage(_image.data(), _lines, _columns, _lineProperties);
|
|
chain.process();
|
|
|
|
return dynamic_cast<UrlFilter::HotSpot*>(chain.hotSpotAt(charLine, charColumn));
|
|
}
|
|
|
|
std::string TerminalView::linkAt(double x, double y)
|
|
{
|
|
TerminalImageFilterChain chain;
|
|
UrlFilter::HotSpot* spot = linkHotSpotAt(chain, x, y);
|
|
return spot ? spot->url() : std::string();
|
|
}
|
|
|
|
std::vector<TerminalView::Segment> TerminalView::linkUnderlineAt(double x, double y)
|
|
{
|
|
TerminalImageFilterChain chain;
|
|
UrlFilter::HotSpot* spot = linkHotSpotAt(chain, x, y);
|
|
if (!spot)
|
|
return {};
|
|
|
|
// Placed like the underline paintFilters() would draw.
|
|
std::vector<Segment> segments;
|
|
for (int line = spot->startLine(); line <= spot->endLine(); ++line) {
|
|
const int startColumn = line == spot->startLine() ? spot->startColumn() : 0;
|
|
int endColumn = _columns;
|
|
if (line == spot->endLine()) {
|
|
endColumn = spot->endColumn();
|
|
} else {
|
|
while (endColumn > startColumn
|
|
&& konsole_isspace(char16_t(_image[size_t(loc(endColumn - 1, line))].character)))
|
|
endColumn--;
|
|
}
|
|
|
|
const int bottom = (line + 1) * _metrics.height - 1 + _topMargin;
|
|
// QFontMetrics::underlinePos() is a whole number.
|
|
const int underline = bottom - _metrics.descent + kRound(_metrics.underlinePos);
|
|
segments.push_back({double(startColumn * _metrics.width + _leftBaseMargin), double(underline),
|
|
double((endColumn - startColumn) * _metrics.width)});
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Find */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
void TerminalView::find(const std::string& text, bool forwards, bool fromSelection, bool caseSensitive)
|
|
{
|
|
if (!_screenWindow || !_session)
|
|
return;
|
|
|
|
if (text.empty()) {
|
|
clearFind();
|
|
return;
|
|
}
|
|
|
|
Emulation* emulation = _session->emulation();
|
|
int startColumn = 0;
|
|
int startLine = emulation->lineCount();
|
|
|
|
if (fromSelection && hasSelection()) {
|
|
if (forwards) {
|
|
_screenWindow->screen()->getSelectionEnd(startColumn, startLine);
|
|
startColumn++;
|
|
} else {
|
|
_screenWindow->screen()->getSelectionStart(startColumn, startLine);
|
|
}
|
|
} else if (forwards) {
|
|
startLine = 0;
|
|
}
|
|
|
|
const RegExp regExp(RegExp::escape(strings::utf8ToUtf16(text)),
|
|
caseSensitive ? RegExp::NoOption : RegExp::CaseInsensitive);
|
|
HistorySearch search(emulation, regExp, forwards, startColumn, startLine);
|
|
const std::optional<HistorySearch::Result> match = search.search();
|
|
|
|
if (match) {
|
|
// Centre the match rather than pinning it to the top edge.
|
|
_screenWindow->scrollTo(std::max(0, match->startLine - _screenWindow->windowLines() / 2));
|
|
_screenWindow->setTrackOutput(false);
|
|
_screenWindow->notifyOutputChanged();
|
|
_screenWindow->setSelectionStart(match->startColumn, match->startLine - _screenWindow->currentLine(), false);
|
|
_screenWindow->setSelectionEnd(match->endColumn, match->endLine - _screenWindow->currentLine());
|
|
findResult(true);
|
|
} else {
|
|
_screenWindow->clearSelection();
|
|
findResult(false);
|
|
}
|
|
}
|
|
|
|
void TerminalView::clearFind()
|
|
{
|
|
if (_screenWindow)
|
|
_screenWindow->clearSelection();
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* Painting */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
namespace
|
|
{
|
|
|
|
// What TerminalGpuNode did for a frame: cell backgrounds, the cursor and box
|
|
// drawing as device-pixel rects, glyphs at whole device pixels.
|
|
class FramePainter
|
|
{
|
|
public:
|
|
FramePainter(ui::Painter& painter, ui::PointF origin)
|
|
: m_painter(painter)
|
|
, m_scale(painter.scale())
|
|
, m_origin{origin.x * painter.scale(), origin.y * painter.scale()}
|
|
, m_device(painter.toDevice(origin.x), painter.toDevice(origin.y))
|
|
{
|
|
}
|
|
|
|
double scale() const { return m_scale; }
|
|
|
|
// Window device pixels from the painter's origin, as fillDeviceRect and
|
|
// GlyphRenderer place them.
|
|
int fromWindowX(int device) const { return device - m_device.x(); }
|
|
int fromWindowY(int device) const { return device - m_device.y(); }
|
|
|
|
int deviceX(double logical) const { return kRound(m_origin.x + logical * m_scale); }
|
|
int deviceY(double logical) const { return kRound(m_origin.y + logical * m_scale); }
|
|
|
|
// Device pixels of the window, filled exactly.
|
|
void fill(const cutefish::base::Rect& device, const Color& color)
|
|
{
|
|
if (device.isEmpty() || color.a == 0)
|
|
return;
|
|
ui::fillDeviceRect(m_painter,
|
|
{device.x() - m_device.x(), device.y() - m_device.y(), device.width(), device.height()},
|
|
color);
|
|
}
|
|
|
|
// Filled after the glyphs: decorations, box drawing, thin cursors.
|
|
void fillOver(const cutefish::base::Rect& device, const Color& color)
|
|
{
|
|
if (!device.isEmpty() && color.a != 0)
|
|
m_over.push_back({device, color});
|
|
}
|
|
|
|
void flushOver()
|
|
{
|
|
for (const auto& [rect, color] : m_over)
|
|
fill(rect, color);
|
|
m_over.clear();
|
|
}
|
|
|
|
ui::Painter& painter() { return m_painter; }
|
|
|
|
private:
|
|
ui::Painter& m_painter;
|
|
double m_scale;
|
|
ui::PointF m_origin;
|
|
cutefish::base::Point m_device;
|
|
std::vector<std::pair<cutefish::base::Rect, Color>> m_over;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
static cutefish::base::Rect rectFromCorners(int left, int top, int right, int bottom)
|
|
{
|
|
// QRect(QPoint, QPoint): both corners inside.
|
|
return cutefish::base::Rect(left, top, std::max(0, right - left + 1), std::max(0, bottom - top + 1));
|
|
}
|
|
|
|
// drawLineChar() as device pixel rects. Edges go through the same mapping as
|
|
// the cells, so strokes cover what QPainter's aliased lines would, and meet
|
|
// the neighbouring cells' strokes exactly.
|
|
static void addLineChar(FramePainter& frame, double left, double top, int w, int h, int scaleX, int scaleY,
|
|
uint8_t code, bool bold, const Color& color)
|
|
{
|
|
const uint32_t toDraw = LineChars[code];
|
|
const int cx = w / 2;
|
|
const int cy = h / 2;
|
|
// A bold stroke is three pixels wide, and its square caps reach one further.
|
|
const int grow = bold ? 1 : 0;
|
|
|
|
auto rect = [&](int x0, int y0, int x1, int y1) {
|
|
const int left0 = frame.deviceX(left + std::clamp(x0, 0, w) * scaleX);
|
|
const int top0 = frame.deviceY(top + std::clamp(y0, 0, h) * scaleY);
|
|
const int right = std::max(left0 + 1, frame.deviceX(left + std::clamp(x1, 0, w) * scaleX));
|
|
const int bottom = std::max(top0 + 1, frame.deviceY(top + std::clamp(y1, 0, h) * scaleY));
|
|
frame.fillOver(cutefish::base::Rect(left0, top0, right - left0, bottom - top0), color);
|
|
};
|
|
// Both ends inclusive, as QPainter::drawLine() takes them.
|
|
auto vertical = [&](int x, int y0, int y1) { rect(x - grow, y0 - grow, x + 1 + grow, y1 + 1 + grow); };
|
|
auto horizontal = [&](int y, int x0, int x1) { rect(x0 - grow, y - grow, x1 + 1 + grow, y + 1 + grow); };
|
|
auto point = [&](int x, int y) { rect(x - grow, y - grow, x + 1 + grow, y + 1 + grow); };
|
|
|
|
if (toDraw & TopL)
|
|
vertical(cx - 1, 0, cy - 2);
|
|
if (toDraw & TopC)
|
|
vertical(cx, 0, cy - 2);
|
|
if (toDraw & TopR)
|
|
vertical(cx + 1, 0, cy - 2);
|
|
|
|
if (toDraw & BotL)
|
|
vertical(cx - 1, cy + 2, h - 1);
|
|
if (toDraw & BotC)
|
|
vertical(cx, cy + 2, h - 1);
|
|
if (toDraw & BotR)
|
|
vertical(cx + 1, cy + 2, h - 1);
|
|
|
|
if (toDraw & LeftT)
|
|
horizontal(cy - 1, 0, cx - 2);
|
|
if (toDraw & LeftC)
|
|
horizontal(cy, 0, cx - 2);
|
|
if (toDraw & LeftB)
|
|
horizontal(cy + 1, 0, cx - 2);
|
|
|
|
if (toDraw & RightT)
|
|
horizontal(cy - 1, cx + 2, w - 1);
|
|
if (toDraw & RightC)
|
|
horizontal(cy, cx + 2, w - 1);
|
|
if (toDraw & RightB)
|
|
horizontal(cy + 1, cx + 2, w - 1);
|
|
|
|
if (toDraw & Int11)
|
|
point(cx - 1, cy - 1);
|
|
if (toDraw & Int12)
|
|
point(cx, cy - 1);
|
|
if (toDraw & Int13)
|
|
point(cx + 1, cy - 1);
|
|
|
|
if (toDraw & Int21)
|
|
point(cx - 1, cy);
|
|
if (toDraw & Int22)
|
|
point(cx, cy);
|
|
if (toDraw & Int23)
|
|
point(cx + 1, cy);
|
|
|
|
if (toDraw & Int31)
|
|
point(cx - 1, cy + 1);
|
|
if (toDraw & Int32)
|
|
point(cx, cy + 1);
|
|
if (toDraw & Int33)
|
|
point(cx + 1, cy + 1);
|
|
}
|
|
|
|
// drawOtherLineChar(): the dashes, arcs and diagonals LineChars has no entry
|
|
// for. The glyph atlas rasterised them with an unantialiased QPainter scaled
|
|
// to the device, a pen of one logical pixel (three for bold), so they are
|
|
// drawn the same way into a bitmap and its pixels filled as device rects.
|
|
static void addOtherLineChar(FramePainter& frame, double left, double top, int w, int h, int scaleX, int scaleY,
|
|
uint8_t code, bool bold, const Color& color)
|
|
{
|
|
const double scale = frame.scale();
|
|
const int padX = int(std::ceil(w * scale));
|
|
const int padY = int(std::ceil(h * scale / 2));
|
|
ui::Bitmap bitmap(int(std::ceil(w * scale)) + 2 * padX, int(std::ceil(h * scale)) + 2 * padY,
|
|
ui::Bitmap::Format::Alpha8);
|
|
if (bitmap.isNull())
|
|
return;
|
|
ui::BitmapPainter painter(bitmap);
|
|
painter.translate(padX, padY);
|
|
painter.scale(scale, scale);
|
|
const double penWidth = bold ? 3 : 1;
|
|
const Color ink{255, 255, 255, 255};
|
|
auto line = [&](int x0, int y0, int x1, int y1) {
|
|
painter.drawLine({double(x0), double(y0)}, {double(x1), double(y1)}, ink, penWidth, ui::LineCap::Square);
|
|
};
|
|
|
|
const int cx = w / 2;
|
|
const int cy = h / 2;
|
|
const int ex = w - 1;
|
|
const int ey = h - 1;
|
|
|
|
// Double dashes
|
|
if (0x4C <= code && code <= 0x4F) {
|
|
const int xHalfGap = std::max(w / 15, 1);
|
|
const int yHalfGap = std::max(h / 15, 1);
|
|
switch (code) {
|
|
case 0x4D: // BOX DRAWINGS HEAVY DOUBLE DASH HORIZONTAL
|
|
line(0, cy - 1, cx - xHalfGap - 1, cy - 1);
|
|
line(0, cy + 1, cx - xHalfGap - 1, cy + 1);
|
|
line(cx + xHalfGap, cy - 1, ex, cy - 1);
|
|
line(cx + xHalfGap, cy + 1, ex, cy + 1);
|
|
[[fallthrough]];
|
|
case 0x4C: // BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL
|
|
line(0, cy, cx - xHalfGap - 1, cy);
|
|
line(cx + xHalfGap, cy, ex, cy);
|
|
break;
|
|
case 0x4F: // BOX DRAWINGS HEAVY DOUBLE DASH VERTICAL
|
|
line(cx - 1, 0, cx - 1, cy - yHalfGap - 1);
|
|
line(cx + 1, 0, cx + 1, cy - yHalfGap - 1);
|
|
line(cx - 1, cy + yHalfGap, cx - 1, ey);
|
|
line(cx + 1, cy + yHalfGap, cx + 1, ey);
|
|
[[fallthrough]];
|
|
case 0x4E: // BOX DRAWINGS LIGHT DOUBLE DASH VERTICAL
|
|
line(cx, 0, cx, cy - yHalfGap - 1);
|
|
line(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;
|
|
// QPainter::drawArc(x, y, d, d, start, 90 degrees).
|
|
auto arc = [&](int x, int y, double startDegrees) {
|
|
ui::Path path;
|
|
path.arcTo({double(x), double(y), double(d), double(d)}, startDegrees, 90);
|
|
painter.strokePath(path, ink, penWidth, ui::LineCap::Square, ui::LineJoin::Bevel);
|
|
};
|
|
switch (code) {
|
|
case 0x6D: // BOX DRAWINGS LIGHT ARC DOWN AND RIGHT
|
|
line(cx, cy + r, cx, ey);
|
|
line(cx + r, cy, ex, cy);
|
|
arc(cx, cy, 90);
|
|
break;
|
|
case 0x6E: // BOX DRAWINGS LIGHT ARC DOWN AND LEFT
|
|
line(cx, cy + r, cx, ey);
|
|
line(0, cy, cx - r, cy);
|
|
arc(cx - d, cy, 0);
|
|
break;
|
|
case 0x6F: // BOX DRAWINGS LIGHT ARC UP AND LEFT
|
|
line(cx, 0, cx, cy - r);
|
|
line(0, cy, cx - r, cy);
|
|
arc(cx - d, cy - d, 270);
|
|
break;
|
|
case 0x70: // BOX DRAWINGS LIGHT ARC UP AND RIGHT
|
|
line(cx, 0, cx, cy - r);
|
|
line(cx + r, cy, ex, cy);
|
|
arc(cx, cy - d, 180);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Diagonals
|
|
else if (0x71 <= code && code <= 0x73) {
|
|
switch (code) {
|
|
case 0x71: // BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT
|
|
line(ex, 0, 0, ey);
|
|
break;
|
|
case 0x72: // BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT
|
|
line(0, 0, ex, ey);
|
|
break;
|
|
case 0x73: // BOX DRAWINGS LIGHT DIAGONAL CROSS
|
|
line(ex, 0, 0, ey);
|
|
line(0, 0, ex, ey);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Each run of covered pixels in a row as one rect, stretched as the atlas
|
|
// quad was for double-width and double-height lines.
|
|
const int cellX = frame.deviceX(left);
|
|
const int cellY = frame.deviceY(top);
|
|
for (int y = 0; y < bitmap.height(); ++y) {
|
|
int x = 0;
|
|
while (x < bitmap.width()) {
|
|
if (bitmap.alpha(x, y) < 128) {
|
|
++x;
|
|
continue;
|
|
}
|
|
const int start = x;
|
|
while (x < bitmap.width() && bitmap.alpha(x, y) >= 128)
|
|
++x;
|
|
frame.fillOver(cutefish::base::Rect(cellX + (start - padX) * scaleX, cellY + (y - padY) * scaleY,
|
|
(x - start) * scaleX, scaleY),
|
|
color);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mirrors TerminalGpuNode::build(), which mirrored TerminalDisplay::paint().
|
|
void TerminalView::paint(ui::Painter& painter)
|
|
{
|
|
// The window repaints its focus view when the keyboard comes or goes.
|
|
if (hasKeyboard()) {
|
|
if (_hasBlinkingCursor && !_blinkCursorTimer.isActive())
|
|
_blinkCursorTimer.start(std::chrono::milliseconds(CURSOR_BLINK_INTERVAL));
|
|
} else if (_blinkCursorTimer.isActive()) {
|
|
_blinkCursorTimer.stop();
|
|
_cursorBlinking = false;
|
|
}
|
|
|
|
if (_image.empty() || geometry().width <= 0 || geometry().height <= 0)
|
|
return;
|
|
|
|
FramePainter frame(painter, painter.offset());
|
|
|
|
const int fw = _metrics.width;
|
|
const int fh = _metrics.height;
|
|
const double dpr = frame.scale();
|
|
const int lineThickness = std::max(1, kRound(dpr));
|
|
const double underlinePos = _metrics.underlinePos;
|
|
const double strikeOutPos = _metrics.strikeOutPos;
|
|
const double overlinePos = _metrics.overlinePos;
|
|
const int decorationThickness = std::max(1, kRound(_metrics.lineWidth * dpr));
|
|
const Color frameBackground = _colorTable[DEFAULT_BACK_COLOR].color;
|
|
|
|
if (_opacity > 0) {
|
|
frame.fill(rectFromCorners(frame.deviceX(0), frame.deviceY(0), frame.deviceX(geometry().width) - 1,
|
|
frame.deviceY(geometry().height) - 1),
|
|
frameBackground.withAlpha(_opacity));
|
|
}
|
|
|
|
// The cursor; returns whether the text under it takes the cell's background colour.
|
|
auto addCursor = [&](double left, double top, double width, double scaleY, const Color& foreground) {
|
|
const Color color = foreground;
|
|
const double height = (fh - (_fullCursorHeight ? 0 : _lineSpacing - 1)) * scaleY;
|
|
const cutefish::base::Rect rect = rectFromCorners(frame.deviceX(left), frame.deviceY(top),
|
|
frame.deviceX(left + width) - 1,
|
|
frame.deviceY(top + height) - 1);
|
|
|
|
switch (_cursorShape) {
|
|
case 1:
|
|
frame.fillOver(cutefish::base::Rect(rect.x(), rect.bottom() - lineThickness, rect.width(), lineThickness),
|
|
color);
|
|
return false;
|
|
case 2:
|
|
frame.fillOver(cutefish::base::Rect(rect.x(), rect.y(), lineThickness, rect.height()), color);
|
|
return false;
|
|
default:
|
|
frame.fill(rect, color);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
auto addDecorations = [&](const cutefish::base::Rect& cell, double top, int scaleY, bool underline,
|
|
bool strikeOut, bool overline, const Color& color) {
|
|
const double baseline = top + (_metrics.ascent + _lineSpacing) * scaleY;
|
|
auto line = [&](double y) {
|
|
frame.fillOver(cutefish::base::Rect(cell.x(), frame.deviceY(y), cell.width(), decorationThickness * scaleY),
|
|
color);
|
|
};
|
|
if (underline)
|
|
line(baseline + underlinePos * scaleY);
|
|
if (strikeOut)
|
|
line(baseline - strikeOutPos * scaleY);
|
|
if (overline)
|
|
line(baseline - overlinePos * scaleY);
|
|
};
|
|
|
|
auto styleOf = [&](const Character& c) {
|
|
int style = TerminalFont::Regular;
|
|
if ((c.rendition & RE_BOLD) && _boldIntense)
|
|
style |= TerminalFont::Bold;
|
|
if ((c.rendition & RE_ITALIC) && _italicEnabled)
|
|
style |= TerminalFont::Italic;
|
|
return style;
|
|
};
|
|
|
|
const Point cursorPos = _screenWindow ? _screenWindow->cursorPosition() : Point(0, 0);
|
|
const int preeditCells = _preedit.empty() ? 0 : string_width(_preedit);
|
|
const int usedLines = drawnLines();
|
|
|
|
// The atlas drew a glyph with its origin at the cell's left and its baseline
|
|
// (ascent + line spacing) * dpr below the row's top, stretched from there.
|
|
struct Glyph
|
|
{
|
|
std::u32string cluster;
|
|
int style;
|
|
Color color;
|
|
int x;
|
|
int rowTop;
|
|
int scaleX;
|
|
int scaleY;
|
|
};
|
|
std::vector<Glyph> glyphs;
|
|
|
|
for (int y = 0; y < usedLines; ++y) {
|
|
const LineProperty property = y < int(_lineProperties.size()) ? _lineProperties[size_t(y)] : LineProperty(0);
|
|
const int scaleX = (property & LINE_DOUBLEWIDTH) ? 2 : 1;
|
|
const int scaleY = (property & LINE_DOUBLEHEIGHT) ? 2 : 1;
|
|
const double top = _topMargin + y * fh;
|
|
const int rowTop = frame.deviceY(top);
|
|
const int rowBottom = frame.deviceY(top + fh * scaleY);
|
|
const Character* line = &_image[size_t(y * _columns)];
|
|
|
|
for (int x = 0; x < _usedColumns; ++x) {
|
|
const Character& c = line[x];
|
|
const int cells = (x + 1 < _usedColumns && line[x + 1].character == 0) ? 2 : 1;
|
|
|
|
// The preedit string covers these cells.
|
|
if (preeditCells && y == cursorPos.y() && x >= cursorPos.x() && x < cursorPos.x() + preeditCells) {
|
|
x += cells - 1;
|
|
continue;
|
|
}
|
|
|
|
const double left = _leftMargin + x * fw * scaleX;
|
|
const double width = fw * scaleX * cells;
|
|
const cutefish::base::Rect cell = rectFromCorners(frame.deviceX(left), rowTop,
|
|
frame.deviceX(left + width) - 1, rowBottom - 1);
|
|
const Color foreground = c.foregroundColor.color(_colorTable);
|
|
const Color background = c.backgroundColor.color(_colorTable);
|
|
|
|
if (background != frameBackground)
|
|
frame.fill(cell, background);
|
|
|
|
bool invert = false;
|
|
if ((c.rendition & RE_CURSOR) && !_cursorBlinking)
|
|
invert = addCursor(left, top, width, scaleY, foreground);
|
|
|
|
x += cells - 1;
|
|
|
|
if (_blinking && (c.rendition & RE_BLINK))
|
|
continue;
|
|
|
|
const Color color = invert ? background : foreground;
|
|
const int style = styleOf(c);
|
|
addDecorations(cell, top, scaleY, c.rendition & RE_UNDERLINE, false, false, color);
|
|
|
|
if (!c.character || c.character == L' ')
|
|
continue;
|
|
|
|
if (_drawLineChars && c.isLineChar()) {
|
|
const uint8_t code = uint8_t(c.character & 0xff);
|
|
const bool bold = (c.rendition & RE_BOLD) && _boldIntense;
|
|
if (LineChars[code])
|
|
addLineChar(frame, left, top, fw, fh, scaleX, scaleY, code, bold, color);
|
|
else
|
|
addOtherLineChar(frame, left, top, fw, fh, scaleX, scaleY, code, bold, color);
|
|
continue;
|
|
}
|
|
|
|
std::u32string cluster;
|
|
if (c.rendition & RE_EXTENDED_CHAR) {
|
|
ushort length = 0;
|
|
const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(c.character, length);
|
|
if (chars)
|
|
cluster.assign(chars, chars + length);
|
|
} else {
|
|
cluster.push_back(char32_t(c.character));
|
|
}
|
|
if (!cluster.empty())
|
|
glyphs.push_back({std::move(cluster), style, color, cell.x(), rowTop, scaleX, scaleY});
|
|
}
|
|
|
|
// Both halves of a double-height line hold the same text; the first draws it.
|
|
if (scaleY == 2)
|
|
++y;
|
|
}
|
|
|
|
// drawInputMethodPreeditString(): the default background, the cursor, then the text.
|
|
if (preeditCells && cursorPos.y() < usedLines) {
|
|
const double left = _leftMargin + cursorPos.x() * fw;
|
|
const double top = _topMargin + cursorPos.y() * fh;
|
|
const int rowTop = frame.deviceY(top);
|
|
const Character& style = _image[size_t(cursorPos.y() * _columns + cursorPos.x())];
|
|
|
|
Color background = _colorTable[DEFAULT_BACK_COLOR].color;
|
|
background = background.withAlpha(_opacity);
|
|
frame.fill(rectFromCorners(frame.deviceX(left), rowTop, frame.deviceX(left + fw * preeditCells) - 1,
|
|
frame.deviceY(top + fh) - 1),
|
|
background);
|
|
|
|
bool invert = false;
|
|
if (!_cursorBlinking)
|
|
invert = addCursor(left, top, fw * preeditCells, 1, _colorTable[DEFAULT_FORE_COLOR].color);
|
|
|
|
const Color color = (invert ? style.backgroundColor : style.foregroundColor).color(_colorTable);
|
|
const int glyphStyle = styleOf(style);
|
|
int column = 0;
|
|
size_t i = 0;
|
|
while (i < _preedit.size()) {
|
|
std::u32string cluster(1, char32_t(_preedit[i]));
|
|
const int cells = std::max(1, konsole_wcwidth(_preedit[i]));
|
|
for (++i; i < _preedit.size() && konsole_wcwidth(_preedit[i]) == 0; ++i)
|
|
cluster.push_back(char32_t(_preedit[i]));
|
|
|
|
glyphs.push_back({std::move(cluster), glyphStyle, color, frame.deviceX(left + column * fw), rowTop, 1,
|
|
1});
|
|
column += cells;
|
|
}
|
|
}
|
|
|
|
// Runs of one style, colour and row in one call; a stretched glyph alone, about its own cell.
|
|
const double baselineOffset = (_metrics.ascent + _lineSpacing) * dpr;
|
|
std::vector<ui::GlyphCluster> run;
|
|
for (size_t i = 0; i < glyphs.size();) {
|
|
const Glyph& first = glyphs[i];
|
|
run.clear();
|
|
size_t end = i;
|
|
do {
|
|
run.push_back({glyphs[end].cluster, double(frame.fromWindowX(glyphs[end].x))});
|
|
++end;
|
|
} while (end < glyphs.size() && first.scaleX == 1 && first.scaleY == 1 && glyphs[end].scaleX == 1
|
|
&& glyphs[end].scaleY == 1 && glyphs[end].style == first.style && glyphs[end].color == first.color
|
|
&& glyphs[end].rowTop == first.rowTop);
|
|
|
|
const int rowTop = frame.fromWindowY(first.rowTop);
|
|
_font->draw(painter, first.style, first.color, dpr, rowTop + baselineOffset, run,
|
|
ui::SizeF{double(first.scaleX), double(first.scaleY)},
|
|
ui::PointF{double(frame.fromWindowX(first.x)), double(rowTop)});
|
|
i = end;
|
|
}
|
|
|
|
frame.flushOver();
|
|
|
|
// The link under the pointer: underlined on hover, as one-pixel Rectangles.
|
|
for (const Segment& segment : _linkSegments)
|
|
painter.fillRect({segment.x, segment.y, segment.width, 1}, _linkColor);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* */
|
|
/* The item's MouseArea and key handler */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static uint32_t buttonBit(ui::MouseButton button)
|
|
{
|
|
return 1u << int(button);
|
|
}
|
|
|
|
void TerminalView::updatePointerCursor()
|
|
{
|
|
if (!_linkSegments.empty() && _ctrlHeld)
|
|
View::setCursorShape(ui::CursorShape::Pointer);
|
|
else
|
|
View::setCursorShape(terminalUsesMouse() ? ui::CursorShape::Default : ui::CursorShape::Text);
|
|
}
|
|
|
|
void TerminalView::setLinkColor(Color color)
|
|
{
|
|
_linkColor = color;
|
|
if (!_linkSegments.empty())
|
|
requestPaint();
|
|
}
|
|
|
|
// Only a move to another cell can change the link under the pointer.
|
|
void TerminalView::updateLinkHover(ui::PointF position, uint32_t modifiers)
|
|
{
|
|
const bool ctrlHeld = (modifiers & ui::ControlModifier) != 0;
|
|
if (ctrlHeld != _ctrlHeld) {
|
|
_ctrlHeld = ctrlHeld;
|
|
updatePointerCursor();
|
|
}
|
|
_hoverPosition = position;
|
|
|
|
const std::pair<int, int> cell{int(std::floor(position.x / std::max(1, _metrics.width))),
|
|
int(std::floor(position.y / std::max(1, _metrics.height)))};
|
|
if (cell == _hoverCell)
|
|
return;
|
|
|
|
_hoverCell = cell;
|
|
_linkSegments = linkUnderlineAt(position.x, position.y);
|
|
updatePointerCursor();
|
|
requestPaint();
|
|
}
|
|
|
|
void TerminalView::refreshLinkHover()
|
|
{
|
|
if (_hoverPosition.x >= 0) {
|
|
_linkSegments = linkUnderlineAt(_hoverPosition.x, _hoverPosition.y);
|
|
updatePointerCursor();
|
|
requestPaint();
|
|
}
|
|
}
|
|
|
|
void TerminalView::clearLinkHover()
|
|
{
|
|
_hoverPosition = {-1, -1};
|
|
_hoverCell = {-1, -1};
|
|
if (!_linkSegments.empty()) {
|
|
_linkSegments.clear();
|
|
updatePointerCursor();
|
|
requestPaint();
|
|
}
|
|
}
|
|
|
|
void TerminalView::hoverMove(const ui::PointerEvent& event)
|
|
{
|
|
_lastPointer = event.position;
|
|
if (_buttons == 0)
|
|
updateLinkHover(event.position, event.modifiers);
|
|
}
|
|
|
|
void TerminalView::hoverChanged()
|
|
{
|
|
if (!isHovered())
|
|
clearLinkHover();
|
|
}
|
|
|
|
void TerminalView::pointerPress(const ui::PointerEvent& event)
|
|
{
|
|
const double x = event.position.x;
|
|
const double y = event.position.y;
|
|
_buttons |= buttonBit(event.button);
|
|
|
|
_menuPress = event.button == ui::MouseButton::Right
|
|
&& (!terminalUsesMouse() || (event.modifiers & ui::ShiftModifier));
|
|
|
|
_pressedLink = event.button == ui::MouseButton::Left && (event.modifiers & ui::ControlModifier)
|
|
? linkAt(x, y)
|
|
: std::string();
|
|
if (!_pressedLink.empty())
|
|
return;
|
|
|
|
if (!_menuPress)
|
|
simulateMousePress(x, y, event.button, event.modifiers);
|
|
|
|
// The second press of a double click, as Qt Quick delivers it after the press.
|
|
if (event.clickCount == 2)
|
|
simulateMouseDoubleClick(x, y, event.button, event.modifiers);
|
|
}
|
|
|
|
void TerminalView::pointerMove(const ui::PointerEvent& event)
|
|
{
|
|
_lastPointer = event.position;
|
|
if (!_menuPress && _pressedLink.empty())
|
|
simulateMouseMove(event.position.x, event.position.y, _buttons, event.modifiers);
|
|
}
|
|
|
|
void TerminalView::pointerRelease(const ui::PointerEvent& event)
|
|
{
|
|
const double x = event.position.x;
|
|
const double y = event.position.y;
|
|
_buttons &= ~buttonBit(event.button);
|
|
|
|
if (!_pressedLink.empty()) {
|
|
const std::string link = std::exchange(_pressedLink, std::string());
|
|
if (onLinkActivated)
|
|
onLinkActivated(link);
|
|
return;
|
|
}
|
|
|
|
if (_menuPress) {
|
|
_menuPress = false;
|
|
if (onContextMenu)
|
|
onContextMenu(x, y);
|
|
return;
|
|
}
|
|
|
|
simulateMouseRelease(x, y, event.button, event.modifiers);
|
|
|
|
// A right click the program took stays its own.
|
|
if (event.button == ui::MouseButton::Left && x >= 0 && y >= 0 && x < geometry().width && y < geometry().height)
|
|
setFocus(ui::FocusReason::Mouse);
|
|
}
|
|
|
|
bool TerminalView::keyRelease(const ui::KeyEvent& event)
|
|
{
|
|
if (event.key == XKB_KEY_Control_L || event.key == XKB_KEY_Control_R) {
|
|
_ctrlHeld = false;
|
|
updatePointerCursor();
|
|
}
|
|
return false;
|
|
}
|