From 42ed82973cec46f74f9ac57ac9ee79cd50d74c9f Mon Sep 17 00:00:00 2001 From: Marcus Tillmanns Date: Mon, 17 Jul 2023 13:51:56 +0200 Subject: [PATCH] Terminal: Create Terminal solution Change-Id: If271fd23a84c49bbc25fcc3b9bc0939c7237d095 Reviewed-by: Cristian Adam --- src/libs/solutions/CMakeLists.txt | 1 + src/libs/solutions/terminal/CMakeLists.txt | 12 + .../solutions}/terminal/celliterator.cpp | 4 +- .../solutions}/terminal/celliterator.h | 8 +- .../solutions}/terminal/glyphcache.cpp | 4 +- .../solutions}/terminal/glyphcache.h | 4 +- .../solutions}/terminal/keys.cpp | 11 +- .../solutions}/terminal/keys.h | 4 +- .../solutions}/terminal/scrollback.cpp | 4 +- .../solutions}/terminal/scrollback.h | 4 +- .../solutions/terminal/surfaceintegration.h | 19 + src/libs/solutions/terminal/terminal_global.h | 14 + .../solutions}/terminal/terminalsurface.cpp | 66 +- .../solutions}/terminal/terminalsurface.h | 17 +- src/libs/solutions/terminal/terminalview.cpp | 1263 ++++++++++++++++ src/libs/solutions/terminal/terminalview.h | 221 +++ src/plugins/terminal/CMakeLists.txt | 7 +- src/plugins/terminal/shellintegration.cpp | 21 +- src/plugins/terminal/shellintegration.h | 9 +- src/plugins/terminal/terminalpane.cpp | 7 +- src/plugins/terminal/terminalsearch.cpp | 25 +- src/plugins/terminal/terminalsearch.h | 34 +- src/plugins/terminal/terminalsettings.cpp | 5 +- src/plugins/terminal/terminalwidget.cpp | 1344 ++--------------- src/plugins/terminal/terminalwidget.h | 178 +-- src/plugins/terminal/tests/mouse | 67 + 26 files changed, 1885 insertions(+), 1468 deletions(-) create mode 100644 src/libs/solutions/terminal/CMakeLists.txt rename src/{plugins => libs/solutions}/terminal/celliterator.cpp (96%) rename src/{plugins => libs/solutions}/terminal/celliterator.h (94%) rename src/{plugins => libs/solutions}/terminal/glyphcache.cpp (94%) rename src/{plugins => libs/solutions}/terminal/glyphcache.h (90%) rename src/{plugins => libs/solutions}/terminal/keys.cpp (92%) rename src/{plugins => libs/solutions}/terminal/keys.h (83%) rename src/{plugins => libs/solutions}/terminal/scrollback.cpp (95%) rename src/{plugins => libs/solutions}/terminal/scrollback.h (95%) create mode 100644 src/libs/solutions/terminal/surfaceintegration.h create mode 100644 src/libs/solutions/terminal/terminal_global.h rename src/{plugins => libs/solutions}/terminal/terminalsurface.cpp (89%) rename src/{plugins => libs/solutions}/terminal/terminalsurface.h (87%) create mode 100644 src/libs/solutions/terminal/terminalview.cpp create mode 100644 src/libs/solutions/terminal/terminalview.h create mode 100755 src/plugins/terminal/tests/mouse diff --git a/src/libs/solutions/CMakeLists.txt b/src/libs/solutions/CMakeLists.txt index 2a47fbee5fb..67630f067c7 100644 --- a/src/libs/solutions/CMakeLists.txt +++ b/src/libs/solutions/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(spinner) add_subdirectory(tasking) +add_subdirectory(terminal) diff --git a/src/libs/solutions/terminal/CMakeLists.txt b/src/libs/solutions/terminal/CMakeLists.txt new file mode 100644 index 00000000000..58c3e8780d4 --- /dev/null +++ b/src/libs/solutions/terminal/CMakeLists.txt @@ -0,0 +1,12 @@ +add_qtc_library(TerminalLib + DEPENDS Qt::Core Qt::Widgets libvterm + SOURCES + celliterator.cpp celliterator.h + glyphcache.cpp glyphcache.h + keys.cpp keys.h + scrollback.cpp scrollback.h + surfaceintegration.h + terminal_global.h + terminalsurface.cpp terminalsurface.h + terminalview.cpp terminalview.h +) diff --git a/src/plugins/terminal/celliterator.cpp b/src/libs/solutions/terminal/celliterator.cpp similarity index 96% rename from src/plugins/terminal/celliterator.cpp rename to src/libs/solutions/terminal/celliterator.cpp index 91a70f76ea3..b7053438e08 100644 --- a/src/plugins/terminal/celliterator.cpp +++ b/src/libs/solutions/terminal/celliterator.cpp @@ -7,7 +7,7 @@ #include -namespace Terminal::Internal { +namespace TerminalSolution { CellIterator::CellIterator(const TerminalSurface *surface, QPoint pos) : CellIterator(surface, pos.x() + (pos.y() * surface->liveSize().width())) @@ -91,4 +91,4 @@ CellIterator &CellIterator::operator+=(int n) return *this; } -} // namespace Terminal::Internal +} // namespace TerminalSolution diff --git a/src/plugins/terminal/celliterator.h b/src/libs/solutions/terminal/celliterator.h similarity index 94% rename from src/plugins/terminal/celliterator.h rename to src/libs/solutions/terminal/celliterator.h index c246aaa3114..e1fc6efce74 100644 --- a/src/plugins/terminal/celliterator.h +++ b/src/libs/solutions/terminal/celliterator.h @@ -3,15 +3,17 @@ #pragma once +#include "terminal_global.h" + #include #include -namespace Terminal::Internal { +namespace TerminalSolution { class TerminalSurface; -class CellIterator +class TERMINAL_EXPORT CellIterator { public: using iterator_category = std::bidirectional_iterator_tag; @@ -94,4 +96,4 @@ private: mutable std::u32string::value_type m_char; }; -} // namespace Terminal::Internal +} // namespace TerminalSolution diff --git a/src/plugins/terminal/glyphcache.cpp b/src/libs/solutions/terminal/glyphcache.cpp similarity index 94% rename from src/plugins/terminal/glyphcache.cpp rename to src/libs/solutions/terminal/glyphcache.cpp index 72a0fd7b9d1..d5e4b306e16 100644 --- a/src/plugins/terminal/glyphcache.cpp +++ b/src/libs/solutions/terminal/glyphcache.cpp @@ -5,7 +5,7 @@ #include -namespace Terminal::Internal { +namespace TerminalSolution { size_t qHash(const GlyphCacheKey &key, size_t seed = 0) { @@ -45,4 +45,4 @@ const QGlyphRun *GlyphCache::get(const QFont &font, const QString &text) return nullptr; } -} // namespace Terminal::Internal +} // namespace TerminalSolution diff --git a/src/plugins/terminal/glyphcache.h b/src/libs/solutions/terminal/glyphcache.h similarity index 90% rename from src/plugins/terminal/glyphcache.h rename to src/libs/solutions/terminal/glyphcache.h index 60701098f5f..a5ebfc21453 100644 --- a/src/plugins/terminal/glyphcache.h +++ b/src/libs/solutions/terminal/glyphcache.h @@ -8,7 +8,7 @@ #include #include -namespace Terminal::Internal { +namespace TerminalSolution { struct GlyphCacheKey { @@ -31,4 +31,4 @@ public: const QGlyphRun *get(const QFont &font, const QString &text); }; -} // namespace Terminal::Internal +} // namespace TerminalSolution diff --git a/src/plugins/terminal/keys.cpp b/src/libs/solutions/terminal/keys.cpp similarity index 92% rename from src/plugins/terminal/keys.cpp rename to src/libs/solutions/terminal/keys.cpp index ce14cbe5fbc..adbcda10ea7 100644 --- a/src/plugins/terminal/keys.cpp +++ b/src/libs/solutions/terminal/keys.cpp @@ -1,11 +1,9 @@ // Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 -#include - #include "keys.h" -namespace Terminal::Internal { +namespace TerminalSolution { VTermModifier qtModifierToVTerm(Qt::KeyboardModifiers mod) { @@ -77,8 +75,9 @@ VTermKey qtKeyToVTerm(Qt::Key key, bool keypad) case Qt::Key_Enter: { VTermKey enterKey = VTERM_KEY_KP_ENTER; - if (Utils::HostOsInfo::isWindowsHost()) - enterKey = VTERM_KEY_ENTER; +#ifdef Q_OS_WIN + enterKey = VTERM_KEY_ENTER; +#endif return keypad ? enterKey : VTERM_KEY_NONE; } @@ -88,4 +87,4 @@ VTermKey qtKeyToVTerm(Qt::Key key, bool keypad) return VTERM_KEY_NONE; } } -} // namespace Terminal::Internal +} // namespace TerminalSolution diff --git a/src/plugins/terminal/keys.h b/src/libs/solutions/terminal/keys.h similarity index 83% rename from src/plugins/terminal/keys.h rename to src/libs/solutions/terminal/keys.h index f3df9330013..2f967010db9 100644 --- a/src/plugins/terminal/keys.h +++ b/src/libs/solutions/terminal/keys.h @@ -7,9 +7,9 @@ #include -namespace Terminal::Internal { +namespace TerminalSolution { VTermKey qtKeyToVTerm(Qt::Key key, bool keypad); VTermModifier qtModifierToVTerm(Qt::KeyboardModifiers mod); -} // namespace Terminal::Internal +} // namespace TerminalSolution diff --git a/src/plugins/terminal/scrollback.cpp b/src/libs/solutions/terminal/scrollback.cpp similarity index 95% rename from src/plugins/terminal/scrollback.cpp rename to src/libs/solutions/terminal/scrollback.cpp index e22d5fa2436..b3fa9af8433 100644 --- a/src/plugins/terminal/scrollback.cpp +++ b/src/libs/solutions/terminal/scrollback.cpp @@ -8,7 +8,7 @@ #include #include -namespace Terminal::Internal { +namespace TerminalSolution { Scrollback::Line::Line(int cols, const VTermScreenCell *cells) : m_cols(cols) @@ -58,4 +58,4 @@ void Scrollback::clear() m_deque.clear(); } -} // namespace Terminal::Internal +} // namespace TerminalSolution diff --git a/src/plugins/terminal/scrollback.h b/src/libs/solutions/terminal/scrollback.h similarity index 95% rename from src/plugins/terminal/scrollback.h rename to src/libs/solutions/terminal/scrollback.h index 9ca71eec615..a03f9891e64 100644 --- a/src/plugins/terminal/scrollback.h +++ b/src/libs/solutions/terminal/scrollback.h @@ -13,7 +13,7 @@ #include #include -namespace Terminal::Internal { +namespace TerminalSolution { class Scrollback { @@ -54,4 +54,4 @@ private: std::deque m_deque; }; -} // namespace Terminal::Internal +} // namespace TerminalSolution diff --git a/src/libs/solutions/terminal/surfaceintegration.h b/src/libs/solutions/terminal/surfaceintegration.h new file mode 100644 index 00000000000..5959e2b53a7 --- /dev/null +++ b/src/libs/solutions/terminal/surfaceintegration.h @@ -0,0 +1,19 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +namespace TerminalSolution { + +class SurfaceIntegration +{ +public: + virtual void onOsc(int cmd, std::string_view str, bool initial, bool final) = 0; + + virtual void onBell() {} + virtual void onTitle(const QString &title) { Q_UNUSED(title); } +}; + +} // namespace TerminalSolution diff --git a/src/libs/solutions/terminal/terminal_global.h b/src/libs/solutions/terminal/terminal_global.h new file mode 100644 index 00000000000..b7fec1c77c5 --- /dev/null +++ b/src/libs/solutions/terminal/terminal_global.h @@ -0,0 +1,14 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +#if defined(TERMINALLIB_LIBRARY) +#define TERMINAL_EXPORT Q_DECL_EXPORT +#elif defined(TERMINALLIB_STATIC_LIBRARY) +#define TERMINAL_EXPORT +#else +#define TERMINAL_EXPORT Q_DECL_IMPORT +#endif diff --git a/src/plugins/terminal/terminalsurface.cpp b/src/libs/solutions/terminal/terminalsurface.cpp similarity index 89% rename from src/plugins/terminal/terminalsurface.cpp rename to src/libs/solutions/terminal/terminalsurface.cpp index 24e881b35cf..aedf8533d29 100644 --- a/src/plugins/terminal/terminalsurface.cpp +++ b/src/libs/solutions/terminal/terminalsurface.cpp @@ -2,17 +2,16 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "terminalsurface.h" +#include "surfaceintegration.h" #include "keys.h" #include "scrollback.h" -#include - #include #include -namespace Terminal::Internal { +namespace TerminalSolution { Q_LOGGING_CATEGORY(log, "qtc.terminal.surface", QtWarningMsg); @@ -23,13 +22,10 @@ QColor toQColor(const VTermColor &c) struct TerminalSurfacePrivate { - TerminalSurfacePrivate(TerminalSurface *surface, - const QSize &initialGridSize, - ShellIntegration *shellIntegration) + TerminalSurfacePrivate(TerminalSurface *surface, const QSize &initialGridSize) : m_vterm(vterm_new(initialGridSize.height(), initialGridSize.width()), vterm_free) , m_vtermScreen(vterm_obtain_screen(m_vterm.get())) - , m_scrollback(std::make_unique(5000)) - , m_shellIntegration(shellIntegration) + , m_scrollback(std::make_unique(5000)) , q(surface) {} @@ -74,7 +70,8 @@ struct TerminalSurfacePrivate }; m_vtermScreenCallbacks.bell = [](void *user) { auto p = static_cast(user); - emit p->q->bell(); + if (p->m_surfaceIntegration) + p->m_surfaceIntegration->onBell(); return 1; }; @@ -219,8 +216,12 @@ struct TerminalSurfacePrivate int osc(int cmd, const VTermStringFragment &fragment) { - if (m_shellIntegration) - m_shellIntegration->onOsc(cmd, fragment); + if (m_surfaceIntegration) { + m_surfaceIntegration->onOsc(cmd, + {fragment.str, fragment.len}, + fragment.initial, + fragment.final); + } return 1; } @@ -249,7 +250,8 @@ struct TerminalSurfacePrivate case VTERM_PROP_ICONNAME: break; case VTERM_PROP_TITLE: - emit q->titleChanged(QString::fromUtf8(val->string.str, val->string.len)); + if (m_surfaceIntegration) + m_surfaceIntegration->onTitle(QString::fromUtf8(val->string.str, val->string.len)); break; case VTERM_PROP_ALTSCREEN: m_altscreen = val->boolean; @@ -278,8 +280,11 @@ struct TerminalSurfacePrivate const VTermScreenCell *cellAt(int x, int y) { - QTC_ASSERT(y >= 0 && x >= 0, return nullptr); - QTC_ASSERT(y < q->fullSize().height() && x < liveSize().width(), return nullptr); + if (y < 0 || x < 0 || y >= q->fullSize().height() || x >= liveSize().width()) { + qCWarning(log) << "Invalid Parameter for cellAt:" << x << y << "liveSize:" << liveSize() + << "fullSize:" << q->fullSize(); + return nullptr; + } if (!m_altscreen && y < m_scrollback->size()) { const auto &sbl = m_scrollback->line((m_scrollback->size() - 1) - y); @@ -309,15 +314,15 @@ struct TerminalSurfacePrivate bool m_altscreen{false}; - std::unique_ptr m_scrollback; + std::unique_ptr m_scrollback; - ShellIntegration *m_shellIntegration{nullptr}; + SurfaceIntegration *m_surfaceIntegration{nullptr}; TerminalSurface *q; }; -TerminalSurface::TerminalSurface(QSize initialGridSize, ShellIntegration *shellIntegration) - : d(std::make_unique(this, initialGridSize, shellIntegration)) +TerminalSurface::TerminalSurface(QSize initialGridSize) + : d(std::make_unique(this, initialGridSize)) { d->init(); } @@ -369,8 +374,10 @@ TerminalCell TerminalSurface::fetchCell(int x, int y) const QTextCharFormat::NoUnderline, false}; - QTC_ASSERT(y >= 0, return emptyCell); - QTC_ASSERT(y < fullSize().height() && x < fullSize().width(), return emptyCell); + if (y < 0 || y >= fullSize().height() || x >= fullSize().width()) { + qCWarning(log) << "Invalid Parameter for fetchCell:" << x << y << "fullSize:" << fullSize(); + return emptyCell; + } const VTermScreenCell *refCell = d->cellAt(x, y); if (!refCell) @@ -450,8 +457,8 @@ void TerminalSurface::sendKey(const QString &text) void TerminalSurface::sendKey(QKeyEvent *event) { bool keypad = event->modifiers() & Qt::KeypadModifier; - VTermModifier mod = Internal::qtModifierToVTerm(event->modifiers()); - VTermKey key = Internal::qtKeyToVTerm(Qt::Key(event->key()), keypad); + VTermModifier mod = qtModifierToVTerm(event->modifiers()); + VTermKey key = qtKeyToVTerm(Qt::Key(event->key()), keypad); if (key != VTERM_KEY_NONE) { if (mod == VTERM_MOD_SHIFT && (key == VTERM_KEY_ESCAPE || key == VTERM_KEY_BACKSPACE)) @@ -489,14 +496,19 @@ Cursor TerminalSurface::cursor() const return cursor; } -ShellIntegration *TerminalSurface::shellIntegration() const +SurfaceIntegration *TerminalSurface::surfaceIntegration() const { - return d->m_shellIntegration; + return d->m_surfaceIntegration; +} + +void TerminalSurface::setSurfaceIntegration(SurfaceIntegration *surfaceIntegration) +{ + d->m_surfaceIntegration = surfaceIntegration; } void TerminalSurface::mouseMove(QPoint pos, Qt::KeyboardModifiers modifiers) { - vterm_mouse_move(d->m_vterm.get(), pos.y(), pos.x(), Internal::qtModifierToVTerm(modifiers)); + vterm_mouse_move(d->m_vterm.get(), pos.y(), pos.x(), qtModifierToVTerm(modifiers)); } void TerminalSurface::mouseButton(Qt::MouseButton button, @@ -524,7 +536,7 @@ void TerminalSurface::mouseButton(Qt::MouseButton button, return; } - vterm_mouse_button(d->m_vterm.get(), btnIdx, pressed, Internal::qtModifierToVTerm(modifiers)); + vterm_mouse_button(d->m_vterm.get(), btnIdx, pressed, qtModifierToVTerm(modifiers)); } CellIterator TerminalSurface::begin() const @@ -568,4 +580,4 @@ std::reverse_iterator TerminalSurface::rIteratorAt(int pos) const return std::make_reverse_iterator(iteratorAt(pos)); } -} // namespace Terminal::Internal +} // namespace TerminalSolution diff --git a/src/plugins/terminal/terminalsurface.h b/src/libs/solutions/terminal/terminalsurface.h similarity index 87% rename from src/plugins/terminal/terminalsurface.h rename to src/libs/solutions/terminal/terminalsurface.h index 09a86f26e73..be20a4c0aa2 100644 --- a/src/plugins/terminal/terminalsurface.h +++ b/src/libs/solutions/terminal/terminalsurface.h @@ -3,8 +3,9 @@ #pragma once +#include "terminal_global.h" + #include "celliterator.h" -#include "shellintegration.h" #include #include @@ -12,9 +13,10 @@ #include -namespace Terminal::Internal { +namespace TerminalSolution { class Scrollback; +class SurfaceIntegration; struct TerminalSurfacePrivate; @@ -45,12 +47,12 @@ struct Cursor bool blink{false}; }; -class TerminalSurface : public QObject +class TERMINAL_EXPORT TerminalSurface : public QObject { Q_OBJECT; public: - TerminalSurface(QSize initialGridSize, ShellIntegration *shellIntegration); + TerminalSurface(QSize initialGridSize); ~TerminalSurface(); public: @@ -93,7 +95,8 @@ public: Cursor cursor() const; - ShellIntegration *shellIntegration() const; + SurfaceIntegration *surfaceIntegration() const; + void setSurfaceIntegration(SurfaceIntegration *surfaceIntegration); void mouseMove(QPoint pos, Qt::KeyboardModifiers modifiers); void mouseButton(Qt::MouseButton button, bool pressed, Qt::KeyboardModifiers modifiers); @@ -104,11 +107,9 @@ signals: void cursorChanged(Cursor oldCursor, Cursor newCursor); void altscreenChanged(bool altScreen); void unscroll(); - void bell(); - void titleChanged(const QString &title); private: std::unique_ptr d; }; -} // namespace Terminal::Internal +} // namespace TerminalSolution diff --git a/src/libs/solutions/terminal/terminalview.cpp b/src/libs/solutions/terminal/terminalview.cpp new file mode 100644 index 00000000000..3924f09d33a --- /dev/null +++ b/src/libs/solutions/terminal/terminalview.cpp @@ -0,0 +1,1263 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "terminalview.h" +#include "glyphcache.h" +#include "terminalsurface.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(terminalLog, "qtc.terminal", QtWarningMsg) +Q_LOGGING_CATEGORY(selectionLog, "qtc.terminal.selection", QtWarningMsg) +Q_LOGGING_CATEGORY(paintLog, "qtc.terminal.paint", QtWarningMsg) + +namespace TerminalSolution { + +using namespace std::chrono_literals; + +// Minimum time between two refreshes. (30fps) +static constexpr std::chrono::milliseconds minRefreshInterval = 33ms; + +class TerminalViewPrivate +{ +public: + TerminalViewPrivate() + { + m_cursorBlinkTimer.setInterval(750ms); + m_cursorBlinkTimer.setSingleShot(false); + + m_flushDelayTimer.setSingleShot(true); + m_flushDelayTimer.setInterval(minRefreshInterval); + + m_scrollTimer.setSingleShot(false); + m_scrollTimer.setInterval(500ms); + } + + std::optional m_selection; + std::unique_ptr m_surface; + + QSizeF m_cellSize; + + bool m_ignoreScroll{false}; + + QString m_preEditString; + + std::optional m_linkSelection; + + struct + { + QPoint start; + QPoint end; + } m_activeMouseSelect; + + QTimer m_flushDelayTimer; + + QTimer m_scrollTimer; + int m_scrollDirection{0}; + + std::array m_currentColors; + + std::chrono::system_clock::time_point m_lastFlush{std::chrono::system_clock::now()}; + std::chrono::system_clock::time_point m_lastDoubleClick{std::chrono::system_clock::now()}; + bool m_selectLineMode{false}; + Cursor m_cursor; + QTimer m_cursorBlinkTimer; + bool m_cursorBlinkState{true}; + bool m_allowBlinkingCursor{true}; + bool m_allowMouseTracking{true}; + + SurfaceIntegration *m_surfaceIntegration{nullptr}; +}; + +QString defaultFontFamily() +{ +#ifdef Q_OS_DARWIN + return QLatin1String("Menlo"); +#elif defined(Q_OS_WIN) + return QLatin1String("Consolas"); +#else + return QLatin1String("Monospace"); +#endif +} + +int defaultFontSize() +{ +#ifdef Q_OS_DARWIN + return 12; +#elif defined(Q_OS_WIN) + return 10; +#else + return 9; +#endif +} + +TerminalView::TerminalView(QWidget *parent) + : QAbstractScrollArea(parent) + , d(std::make_unique()) +{ + setupSurface(); + setFont(QFont(defaultFontFamily(), defaultFontSize())); + + connect(&d->m_cursorBlinkTimer, &QTimer::timeout, this, [this]() { + if (hasFocus()) + d->m_cursorBlinkState = !d->m_cursorBlinkState; + else + d->m_cursorBlinkState = true; + updateViewportRect(gridToViewport(QRect{d->m_cursor.position, d->m_cursor.position})); + }); + + setAttribute(Qt::WA_InputMethodEnabled); + setAttribute(Qt::WA_MouseTracking); + setAcceptDrops(true); + + setCursor(Qt::IBeamCursor); + + setViewportMargins(1, 1, 1, 1); + + setFocus(); + setFocusPolicy(Qt::StrongFocus); + + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + + connect(&d->m_flushDelayTimer, &QTimer::timeout, this, [this]() { flushVTerm(true); }); + + connect(&d->m_scrollTimer, &QTimer::timeout, this, [this] { + if (d->m_scrollDirection < 0) + verticalScrollBar()->triggerAction(QAbstractSlider::SliderSingleStepSub); + else if (d->m_scrollDirection > 0) + verticalScrollBar()->triggerAction(QAbstractSlider::SliderSingleStepAdd); + }); +} + +TerminalView::~TerminalView() = default; + +void TerminalView::setSurfaceIntegration(SurfaceIntegration *surfaceIntegration) +{ + d->m_surfaceIntegration = surfaceIntegration; + if (d->m_surface) + d->m_surface->setSurfaceIntegration(d->m_surfaceIntegration); +} + +TerminalSurface *TerminalView::surface() const +{ + return d->m_surface.get(); +} + +void TerminalView::setupSurface() +{ + d->m_surface = std::make_unique(QSize{80, 60}); + + if (d->m_surfaceIntegration) + d->m_surface->setSurfaceIntegration(d->m_surfaceIntegration); + + connect(d->m_surface.get(), &TerminalSurface::writeToPty, this, &TerminalView::writeToPty); + + connect(d->m_surface.get(), &TerminalSurface::fullSizeChanged, this, [this] { + updateScrollBars(); + }); + connect(d->m_surface.get(), &TerminalSurface::invalidated, this, [this](const QRect &rect) { + setSelection(std::nullopt); + updateViewportRect(gridToViewport(rect)); + verticalScrollBar()->setValue(d->m_surface->fullSize().height()); + }); + connect( + d->m_surface.get(), + &TerminalSurface::cursorChanged, + this, + [this](const Cursor &oldCursor, const Cursor &newCursor) { + int startX = oldCursor.position.x(); + int endX = newCursor.position.x(); + + if (startX > endX) + std::swap(startX, endX); + + int startY = oldCursor.position.y(); + int endY = newCursor.position.y(); + if (startY > endY) + std::swap(startY, endY); + + d->m_cursor = newCursor; + + updateViewportRect(gridToViewport(QRect{QPoint{startX, startY}, QPoint{endX, endY}})); + configBlinkTimer(); + }); + connect(d->m_surface.get(), &TerminalSurface::altscreenChanged, this, [this] { + updateScrollBars(); + if (!setSelection(std::nullopt)) + updateViewport(); + }); + connect(d->m_surface.get(), &TerminalSurface::unscroll, this, [this] { + verticalScrollBar()->setValue(verticalScrollBar()->maximum()); + }); + + surfaceChanged(); + updateScrollBars(); +} + +void TerminalView::setAllowBlinkingCursor(bool allow) +{ + d->m_allowBlinkingCursor = allow; +} +bool TerminalView::allowBlinkingCursor() const +{ + return d->m_allowBlinkingCursor; +} + +void TerminalView::configBlinkTimer() +{ + bool shouldRun = d->m_cursor.visible && d->m_cursor.blink && hasFocus() + && d->m_allowBlinkingCursor; + if (shouldRun != d->m_cursorBlinkTimer.isActive()) { + if (shouldRun) + d->m_cursorBlinkTimer.start(); + else + d->m_cursorBlinkTimer.stop(); + } +} + +QColor TerminalView::toQColor(std::variant color) const +{ + if (std::holds_alternative(color)) { + int idx = std::get(color); + if (idx >= 0 && idx < 18) + return d->m_currentColors[idx]; + + return d->m_currentColors[(int) WidgetColorIdx::Background]; + } + return std::get(color); +} + +void TerminalView::setColors(const std::array &newColors) +{ + if (d->m_currentColors == newColors) + return; + + d->m_currentColors = newColors; + + updateViewport(); + update(); +} + +void TerminalView::setFont(const QFont &font) +{ + QAbstractScrollArea::setFont(font); + + QFontMetricsF qfm{font}; + qCInfo(terminalLog) << font.family() << font.pointSize() << qfm.averageCharWidth() + << qfm.maxWidth() << viewport()->size(); + + d->m_cellSize = {qfm.averageCharWidth(), (double) qCeil(qfm.height())}; + + QAbstractScrollArea::setFont(font); + + applySizeChange(); +} + +void TerminalView::copyToClipboard() +{ + if (!d->m_selection.has_value()) + return; + + QString text = textFromSelection(); + + qCDebug(selectionLog) << "Copied to clipboard: " << text; + + setClipboard(text); +} + +void TerminalView::pasteFromClipboard() +{ + QClipboard *clipboard = QApplication::clipboard(); + const QString clipboardText = clipboard->text(QClipboard::Clipboard); + + if (clipboardText.isEmpty()) + return; + + d->m_surface->pasteFromClipboard(clipboardText); +} + +void TerminalView::copyLinkToClipboard() +{ + if (d->m_linkSelection) + setClipboard(d->m_linkSelection->link.text); +} + +std::optional TerminalView::selection() const +{ + return d->m_selection; +} + +void TerminalView::clearSelection() +{ + setSelection(std::nullopt); + d->m_surface->sendKey(Qt::Key_Escape); +} + +void TerminalView::zoomIn() +{ + QFont f = font(); + f.setPointSize(f.pointSize() + 1); + setFont(f); +} + +void TerminalView::zoomOut() +{ + QFont f = font(); + f.setPointSize(qMax(f.pointSize() - 1, 1)); + setFont(f); +} + +void TerminalView::moveCursorWordLeft() +{ + writeToPty("\x1b\x62"); +} + +void TerminalView::moveCursorWordRight() +{ + writeToPty("\x1b\x66"); +} + +void TerminalView::clearContents() +{ + d->m_surface->clearAll(); +} + +void TerminalView::writeToTerminal(const QByteArray &data, bool forceFlush) +{ + d->m_surface->dataFromPty(data); + flushVTerm(forceFlush); +} + +void TerminalView::flushVTerm(bool force) +{ + const std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); + const std::chrono::milliseconds timeSinceLastFlush + = std::chrono::duration_cast(now - d->m_lastFlush); + + const bool shouldFlushImmediately = timeSinceLastFlush > minRefreshInterval; + if (force || shouldFlushImmediately) { + if (d->m_flushDelayTimer.isActive()) + d->m_flushDelayTimer.stop(); + + d->m_lastFlush = now; + d->m_surface->flush(); + return; + } + + if (!d->m_flushDelayTimer.isActive()) { + const std::chrono::milliseconds timeToNextFlush = (minRefreshInterval - timeSinceLastFlush); + d->m_flushDelayTimer.start(timeToNextFlush.count()); + } +} + +QString TerminalView::textFromSelection() const +{ + if (!d->m_selection) + return {}; + + CellIterator it = d->m_surface->iteratorAt(d->m_selection->start); + CellIterator end = d->m_surface->iteratorAt(d->m_selection->end); + + if (it.position() >= end.position()) { + qCWarning(selectionLog) << "Invalid selection: start >= end"; + return {}; + } + + std::u32string s; + bool previousWasZero = false; + for (; it != end; ++it) { + if (it.gridPos().x() == 0 && !s.empty() && previousWasZero) + s += U'\n'; + + if (*it != 0) { + previousWasZero = false; + s += *it; + } else { + previousWasZero = true; + } + } + + return QString::fromUcs4(s.data(), static_cast(s.size())); +} + +bool TerminalView::setSelection(const std::optional &selection, bool scroll) +{ + qCDebug(selectionLog) << "setSelection" << selection.has_value(); + if (selection.has_value()) + qCDebug(selectionLog) << "start:" << selection->start << "end:" << selection->end + << "final:" << selection->final; + + if (selectionLog().isDebugEnabled()) + updateViewport(); + + if (selection == d->m_selection) + return false; + + d->m_selection = selection; + selectionChanged(d->m_selection); + + if (d->m_selection && d->m_selection->final && scroll) { + QPoint start = d->m_surface->posToGrid(d->m_selection->start); + QPoint end = d->m_surface->posToGrid(d->m_selection->end); + QRect viewRect = gridToViewport(QRect{start, end}); + if (viewRect.y() >= viewport()->height() || viewRect.y() < 0) { + // Selection is outside of the viewport, scroll to it. + verticalScrollBar()->setValue(start.y()); + } + } + + if (!selectionLog().isDebugEnabled()) + updateViewport(); + + return true; +} + +void TerminalView::restart() +{ + setupSurface(); + applySizeChange(); +} + +QPoint TerminalView::viewportToGlobal(QPoint p) const +{ + int y = p.y() - topMargin(); + const double offset = verticalScrollBar()->value() * d->m_cellSize.height(); + y += offset; + + return {p.x(), y}; +} + +QPoint TerminalView::globalToViewport(QPoint p) const +{ + int y = p.y() + topMargin(); + const double offset = verticalScrollBar()->value() * d->m_cellSize.height(); + y -= offset; + + return {p.x(), y}; +} + +QPoint TerminalView::globalToGrid(QPointF p) const +{ + return QPoint(p.x() / d->m_cellSize.width(), p.y() / d->m_cellSize.height()); +} + +QPointF TerminalView::gridToGlobal(QPoint p, bool bottom, bool right) const +{ + QPointF result = QPointF(p.x() * d->m_cellSize.width(), p.y() * d->m_cellSize.height()); + if (bottom || right) + result += {right ? d->m_cellSize.width() : 0, bottom ? d->m_cellSize.height() : 0}; + return result; +} + +qreal TerminalView::topMargin() const +{ + return viewport()->size().height() + - (d->m_surface->liveSize().height() * d->m_cellSize.height()); +} + +static QPixmap generateWavyPixmap(qreal maxRadius, const QPen &pen) +{ + const qreal radiusBase = qMax(qreal(1), maxRadius); + const qreal pWidth = pen.widthF(); + + const QString key = QLatin1String("WaveUnderline-") % pen.color().name() + % QString::number(int(radiusBase), 16) % QString::number(int(pWidth), 16); + + QPixmap pixmap; + if (QPixmapCache::find(key, &pixmap)) + return pixmap; + + const qreal halfPeriod = qMax(qreal(2), qreal(radiusBase * 1.61803399)); // the golden ratio + const int width = qCeil(100 / (2 * halfPeriod)) * (2 * halfPeriod); + const qreal radius = qFloor(radiusBase * 2) / 2.; + + QPainterPath path; + + qreal xs = 0; + qreal ys = radius; + + while (xs < width) { + xs += halfPeriod; + ys = -ys; + path.quadTo(xs - halfPeriod / 2, ys, xs, 0); + } + + pixmap = QPixmap(width, radius * 2); + pixmap.fill(Qt::transparent); + { + QPen wavePen = pen; + wavePen.setCapStyle(Qt::SquareCap); + + // This is to protect against making the line too fat, as happens on macOS + // due to it having a rather thick width for the regular underline. + const qreal maxPenWidth = .8 * radius; + if (wavePen.widthF() > maxPenWidth) + wavePen.setWidthF(maxPenWidth); + + QPainter imgPainter(&pixmap); + imgPainter.setPen(wavePen); + imgPainter.setRenderHint(QPainter::Antialiasing); + imgPainter.translate(0, radius); + imgPainter.drawPath(path); + } + + QPixmapCache::insert(key, pixmap); + + return pixmap; +} + +// Copied from qpainter.cpp +static void drawTextItemDecoration(QPainter &painter, + const QPointF &pos, + QTextCharFormat::UnderlineStyle underlineStyle, + QTextItem::RenderFlags flags, + qreal width, + const QColor &underlineColor, + const QRawFont &font) +{ + if (underlineStyle == QTextCharFormat::NoUnderline + && !(flags & (QTextItem::StrikeOut | QTextItem::Overline))) + return; + + const QPen oldPen = painter.pen(); + const QBrush oldBrush = painter.brush(); + painter.setBrush(Qt::NoBrush); + QPen pen = oldPen; + pen.setStyle(Qt::SolidLine); + pen.setWidthF(font.lineThickness()); + pen.setCapStyle(Qt::FlatCap); + + QLineF line(qFloor(pos.x()), pos.y(), qFloor(pos.x() + width), pos.y()); + + const qreal underlineOffset = font.underlinePosition(); + + /*if (underlineStyle == QTextCharFormat::SpellCheckUnderline) { + QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme(); + if (theme) + underlineStyle = QTextCharFormat::UnderlineStyle( + theme->themeHint(QPlatformTheme::SpellCheckUnderlineStyle).toInt()); + if (underlineStyle == QTextCharFormat::SpellCheckUnderline) // still not resolved + underlineStyle = QTextCharFormat::WaveUnderline; + }*/ + + if (underlineStyle == QTextCharFormat::WaveUnderline) { + painter.save(); + painter.translate(0, pos.y() + 1); + qreal maxHeight = font.descent() - qreal(1); + + QColor uc = underlineColor; + if (uc.isValid()) + pen.setColor(uc); + + // Adapt wave to underlineOffset or pen width, whatever is larger, to make it work on all platforms + const QPixmap wave = generateWavyPixmap(qMin(qMax(underlineOffset, pen.widthF()), + maxHeight / qreal(2.)), + pen); + const int descent = qFloor(maxHeight); + + painter.setBrushOrigin(painter.brushOrigin().x(), 0); + painter.fillRect(pos.x(), 0, qCeil(width), qMin(wave.height(), descent), wave); + painter.restore(); + } else if (underlineStyle != QTextCharFormat::NoUnderline) { + // Deliberately ceil the offset to avoid the underline coming too close to + // the text above it, but limit it to stay within descent. + qreal adjustedUnderlineOffset = std::ceil(underlineOffset) + 0.5; + if (underlineOffset <= font.descent()) + adjustedUnderlineOffset = qMin(adjustedUnderlineOffset, font.descent() - qreal(0.5)); + const qreal underlinePos = pos.y() + adjustedUnderlineOffset; + QColor uc = underlineColor; + if (uc.isValid()) + pen.setColor(uc); + + pen.setStyle((Qt::PenStyle)(underlineStyle)); + painter.setPen(pen); + QLineF underline(line.x1(), underlinePos, line.x2(), underlinePos); + painter.drawLine(underline); + } + + pen.setStyle(Qt::SolidLine); + pen.setColor(oldPen.color()); + + if (flags & QTextItem::StrikeOut) { + QLineF strikeOutLine = line; + strikeOutLine.translate(0., -font.ascent() / 3.); + QColor uc = underlineColor; + if (uc.isValid()) + pen.setColor(uc); + painter.setPen(pen); + painter.drawLine(strikeOutLine); + } + + if (flags & QTextItem::Overline) { + QLineF overline = line; + overline.translate(0., -font.ascent()); + QColor uc = underlineColor; + if (uc.isValid()) + pen.setColor(uc); + painter.setPen(pen); + painter.drawLine(overline); + } + + painter.setPen(oldPen); + painter.setBrush(oldBrush); +} + +bool TerminalView::paintFindMatches(QPainter &p, + QList::const_iterator &it, + const QRectF &cellRect, + const QPoint gridPos) const +{ + if (it == searchHits().constEnd()) + return false; + + const int pos = d->m_surface->gridToPos(gridPos); + while (it != searchHits().constEnd()) { + if (pos < it->start) + return false; + + if (pos >= it->end) { + ++it; + continue; + } + break; + } + + if (it == searchHits().constEnd()) + return false; + + p.fillRect(cellRect, d->m_currentColors[(size_t) WidgetColorIdx::FindMatch]); + + return true; +} + +bool TerminalView::paintSelection(QPainter &p, const QRectF &cellRect, const QPoint gridPos) const +{ + bool isInSelection = false; + const int pos = d->m_surface->gridToPos(gridPos); + + if (d->m_selection) + isInSelection = pos >= d->m_selection->start && pos < d->m_selection->end; + + if (isInSelection) + p.fillRect(cellRect, d->m_currentColors[(size_t) WidgetColorIdx::Selection]); + + return isInSelection; +} + +int TerminalView::paintCell(QPainter &p, + const QRectF &cellRect, + QPoint gridPos, + const TerminalCell &cell, + QFont &f, + QList::const_iterator &searchIt) const +{ + bool paintBackground = !paintSelection(p, cellRect, gridPos) + && !paintFindMatches(p, searchIt, cellRect, gridPos); + + bool isDefaultBg = std::holds_alternative(cell.backgroundColor) + && std::get(cell.backgroundColor) == 17; + + if (paintBackground && !isDefaultBg) + p.fillRect(cellRect, toQColor(cell.backgroundColor)); + + p.setPen(toQColor(cell.foregroundColor)); + + f.setBold(cell.bold); + f.setItalic(cell.italic); + + if (!cell.text.isEmpty()) { + const auto r = GlyphCache::instance().get(f, cell.text); + + if (r) { + const auto brSize = r->boundingRect().size(); + QPointF brOffset; + if (brSize.width() > cellRect.size().width()) + brOffset.setX(-(brSize.width() - cellRect.size().width()) / 2.0); + if (brSize.height() > cellRect.size().height()) + brOffset.setY(-(brSize.height() - cellRect.size().height()) / 2.0); + + QPointF finalPos = cellRect.topLeft() + brOffset; + + p.drawGlyphRun(finalPos, *r); + + bool tempLink = false; + if (d->m_linkSelection) { + int chPos = d->m_surface->gridToPos(gridPos); + tempLink = chPos >= d->m_linkSelection->start && chPos < d->m_linkSelection->end; + } + if (cell.underlineStyle != QTextCharFormat::NoUnderline || cell.strikeOut || tempLink) { + QTextItem::RenderFlags flags; + //flags.setFlag(QTextItem::RenderFlag::Underline, cell.format.fontUnderline()); + flags.setFlag(QTextItem::StrikeOut, cell.strikeOut); + finalPos.setY(finalPos.y() + r->rawFont().ascent()); + drawTextItemDecoration(p, + finalPos, + tempLink ? QTextCharFormat::DashUnderline + : cell.underlineStyle, + flags, + cellRect.size().width(), + {}, + r->rawFont()); + } + } + } + + return cell.width; +} + +void TerminalView::paintCursor(QPainter &p) const +{ + auto cursor = d->m_surface->cursor(); + + if (!d->m_preEditString.isEmpty()) + cursor.shape = Cursor::Shape::Underline; + + const bool blinkState = !cursor.blink || d->m_cursorBlinkState || !d->m_allowBlinkingCursor + || !d->m_cursorBlinkTimer.isActive(); + + if (cursor.visible && blinkState) { + const int cursorCellWidth = d->m_surface->cellWidthAt(cursor.position.x(), + cursor.position.y()); + + QRectF cursorRect = QRectF(gridToGlobal(cursor.position), + gridToGlobal({cursor.position.x() + cursorCellWidth, + cursor.position.y()}, + true)) + .toAlignedRect(); + + cursorRect.adjust(1, 1, -1, -1); + + QPen pen(Qt::white, 0, Qt::SolidLine); + p.setPen(pen); + + if (hasFocus()) { + QPainter::CompositionMode oldMode = p.compositionMode(); + p.setCompositionMode(QPainter::RasterOp_NotDestination); + switch (cursor.shape) { + case Cursor::Shape::Block: + p.fillRect(cursorRect, p.pen().brush()); + break; + case Cursor::Shape::Underline: + p.drawLine(cursorRect.bottomLeft(), cursorRect.bottomRight()); + break; + case Cursor::Shape::LeftBar: + p.drawLine(cursorRect.topLeft(), cursorRect.bottomLeft()); + break; + } + p.setCompositionMode(oldMode); + } else { + p.drawRect(cursorRect); + } + } +} + +void TerminalView::paintPreedit(QPainter &p) const +{ + auto cursor = d->m_surface->cursor(); + if (!d->m_preEditString.isEmpty()) { + QRectF rect = QRectF(gridToGlobal(cursor.position), + gridToGlobal({cursor.position.x(), cursor.position.y()}, true, true)); + + rect.setWidth(viewport()->width() - rect.x()); + + p.setPen(toQColor((int) WidgetColorIdx::Foreground)); + QFont f = font(); + f.setUnderline(true); + p.setFont(f); + p.drawText(rect, Qt::TextDontClip | Qt::TextWrapAnywhere, d->m_preEditString); + } +} + +void TerminalView::paintCells(QPainter &p, QPaintEvent *event) const +{ + QFont f = font(); + + const int scrollOffset = verticalScrollBar()->value(); + + const int maxRow = d->m_surface->fullSize().height(); + const int startRow = qFloor((qreal) event->rect().y() / d->m_cellSize.height()) + scrollOffset; + const int endRow = qMin(maxRow, + qCeil((event->rect().y() + event->rect().height()) + / d->m_cellSize.height()) + + scrollOffset); + + QList::const_iterator searchIt + = std::lower_bound(searchHits().constBegin(), + searchHits().constEnd(), + startRow, + [this](const SearchHit &hit, int value) { + return d->m_surface->posToGrid(hit.start).y() < value; + }); + + for (int cellY = startRow; cellY < endRow; ++cellY) { + for (int cellX = 0; cellX < d->m_surface->liveSize().width();) { + const auto cell = d->m_surface->fetchCell(cellX, cellY); + + QRectF cellRect(gridToGlobal({cellX, cellY}), + QSizeF{d->m_cellSize.width() * cell.width, d->m_cellSize.height()}); + + int numCells = paintCell(p, cellRect, {cellX, cellY}, cell, f, searchIt); + + cellX += numCells; + } + } +} + +void TerminalView::paintDebugSelection(QPainter &p, const Selection &selection) const +{ + auto s = globalToViewport(gridToGlobal(d->m_surface->posToGrid(selection.start)).toPoint()); + const auto e = globalToViewport( + gridToGlobal(d->m_surface->posToGrid(selection.end), true).toPoint()); + + p.setPen(QPen(Qt::green, 1, Qt::DashLine)); + p.drawLine(s.x(), 0, s.x(), height()); + p.drawLine(0, s.y(), width(), s.y()); + + p.setPen(QPen(Qt::red, 1, Qt::DashLine)); + + p.drawLine(e.x(), 0, e.x(), height()); + p.drawLine(0, e.y(), width(), e.y()); +} + +void TerminalView::paintEvent(QPaintEvent *event) +{ + QElapsedTimer t; + t.start(); + event->accept(); + QPainter p(viewport()); + + p.save(); + + if (paintLog().isDebugEnabled()) + p.fillRect(event->rect(), QColor::fromRgb(rand() % 60, rand() % 60, rand() % 60)); + else + p.fillRect(event->rect(), d->m_currentColors[(size_t) WidgetColorIdx::Background]); + + int scrollOffset = verticalScrollBar()->value(); + int offset = -(scrollOffset * d->m_cellSize.height()); + + qreal margin = topMargin(); + + p.translate(QPointF{0.0, offset + margin}); + + paintCells(p, event); + paintCursor(p); + paintPreedit(p); + + p.restore(); + + p.fillRect(QRectF{{0, 0}, QSizeF{(qreal) width(), topMargin()}}, + d->m_currentColors[(size_t) WidgetColorIdx::Background]); + + if (selectionLog().isDebugEnabled()) { + if (d->m_selection) + paintDebugSelection(p, *d->m_selection); + if (d->m_linkSelection) + paintDebugSelection(p, *d->m_linkSelection); + } + + if (paintLog().isDebugEnabled()) { + QToolTip::showText(this->mapToGlobal(QPoint(width() - 200, 0)), + QString("Paint: %1ms").arg(t.elapsed())); + } +} + +void TerminalView::keyPressEvent(QKeyEvent *event) +{ + // Don't blink during typing + if (d->m_cursorBlinkTimer.isActive()) { + d->m_cursorBlinkTimer.start(); + d->m_cursorBlinkState = true; + } + + if (event->key() == Qt::Key_Control) { + if (!d->m_linkSelection.has_value() && checkLinkAt(mapFromGlobal(QCursor::pos()))) { + setCursor(Qt::PointingHandCursor); + } + } + + event->accept(); + + d->m_surface->sendKey(event); +} + +void TerminalView::keyReleaseEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Control && d->m_linkSelection.has_value()) { + d->m_linkSelection.reset(); + setCursor(Qt::IBeamCursor); + updateViewport(); + } +} + +void TerminalView::applySizeChange() +{ + QSize newLiveSize = { + qFloor((qreal) (viewport()->size().width()) / (qreal) d->m_cellSize.width()), + qFloor((qreal) (viewport()->size().height()) / d->m_cellSize.height()), + }; + + if (newLiveSize.height() <= 0) + newLiveSize.setHeight(1); + + if (newLiveSize.width() <= 0) + newLiveSize.setWidth(1); + + resizePty(newLiveSize); + d->m_surface->resize(newLiveSize); + flushVTerm(true); +} + +void TerminalView::updateScrollBars() +{ + int scrollSize = d->m_surface->fullSize().height() - d->m_surface->liveSize().height(); + verticalScrollBar()->setRange(0, scrollSize); + verticalScrollBar()->setValue(verticalScrollBar()->maximum()); + updateViewport(); +} + +void TerminalView::resizeEvent(QResizeEvent *event) +{ + event->accept(); + + // If increasing in size, we'll trigger libvterm to call sb_popline in + // order to pull lines out of the history. This will cause the scrollback + // to decrease in size which reduces the size of the verticalScrollBar. + // That will trigger a scroll offset increase which we want to ignore. + d->m_ignoreScroll = true; + + applySizeChange(); + + setSelection(std::nullopt); + d->m_ignoreScroll = false; +} + +QRect TerminalView::gridToViewport(QRect rect) const +{ + int offset = verticalScrollBar()->value(); + + int startRow = rect.y() - offset; + int numRows = rect.height(); + int numCols = rect.width(); + + QRectF r{rect.x() * d->m_cellSize.width(), + startRow * d->m_cellSize.height(), + numCols * d->m_cellSize.width(), + numRows * d->m_cellSize.height()}; + + r.translate(0, topMargin()); + + return r.toAlignedRect(); +} + +QPoint TerminalView::toGridPos(QMouseEvent *event) const +{ + return globalToGrid(event->pos().toPointF() + QPointF(0, -topMargin() + 0.5)); +} + +void TerminalView::updateViewport() +{ + viewport()->update(); +} + +void TerminalView::updateViewportRect(const QRect &rect) +{ + viewport()->update(rect); +} + +void TerminalView::focusInEvent(QFocusEvent *) +{ + updateViewport(); + configBlinkTimer(); + selectionChanged(d->m_selection); +} +void TerminalView::focusOutEvent(QFocusEvent *) +{ + updateViewport(); + configBlinkTimer(); +} + +void TerminalView::inputMethodEvent(QInputMethodEvent *event) +{ + d->m_preEditString = event->preeditString(); + + if (event->commitString().isEmpty()) { + updateViewport(); + return; + } + + d->m_surface->sendKey(event->commitString()); +} + +void TerminalView::mousePressEvent(QMouseEvent *event) +{ + if (d->m_allowMouseTracking) { + d->m_surface->mouseMove(toGridPos(event), event->modifiers()); + d->m_surface->mouseButton(event->button(), true, event->modifiers()); + } + + d->m_scrollDirection = 0; + + d->m_activeMouseSelect.start = viewportToGlobal(event->pos()); + + if (event->button() == Qt::LeftButton && event->modifiers() & Qt::ControlModifier) { + if (d->m_linkSelection) { + if (event->modifiers() & Qt::ShiftModifier) { + copyLinkToClipboard(); + return; + } + + linkActivated(d->m_linkSelection->link); + } + return; + } + + if (event->button() == Qt::LeftButton) { + if (std::chrono::system_clock::now() - d->m_lastDoubleClick < 500ms) { + d->m_selectLineMode = true; + const Selection newSelection{d->m_surface->gridToPos( + {0, + d->m_surface->posToGrid(d->m_selection->start).y()}), + d->m_surface->gridToPos( + {d->m_surface->liveSize().width(), + d->m_surface->posToGrid(d->m_selection->end).y()}), + false}; + setSelection(newSelection); + } else { + d->m_selectLineMode = false; + int pos = d->m_surface->gridToPos(globalToGrid(viewportToGlobal(event->pos()))); + setSelection(Selection{pos, pos, false}); + } + event->accept(); + updateViewport(); + } else if (event->button() == Qt::RightButton) { + if (event->modifiers() & Qt::ShiftModifier) { + contextMenuRequested(event->pos()); + } else if (d->m_selection) { + copyToClipboard(); + setSelection(std::nullopt); + } else { + pasteFromClipboard(); + } + } else if (event->button() == Qt::MiddleButton) { + QClipboard *clipboard = QApplication::clipboard(); + if (clipboard->supportsSelection()) { + const QString selectionText = clipboard->text(QClipboard::Selection); + if (!selectionText.isEmpty()) + d->m_surface->pasteFromClipboard(selectionText); + } else { + d->m_surface->pasteFromClipboard(textFromSelection()); + } + } +} +void TerminalView::mouseMoveEvent(QMouseEvent *event) +{ + if (d->m_allowMouseTracking) + d->m_surface->mouseMove(toGridPos(event), event->modifiers()); + + if (d->m_selection && event->buttons() & Qt::LeftButton) { + Selection newSelection = *d->m_selection; + int scrollVelocity = 0; + if (event->pos().y() < 0) { + scrollVelocity = (event->pos().y()); + } else if (event->pos().y() > viewport()->height()) { + scrollVelocity = (event->pos().y() - viewport()->height()); + } + + if ((scrollVelocity != 0) != d->m_scrollTimer.isActive()) { + if (scrollVelocity != 0) + d->m_scrollTimer.start(); + else + d->m_scrollTimer.stop(); + } + + d->m_scrollDirection = scrollVelocity; + + if (d->m_scrollTimer.isActive() && scrollVelocity != 0) { + const std::chrono::milliseconds scrollInterval = 1000ms / qAbs(scrollVelocity); + if (d->m_scrollTimer.intervalAsDuration() != scrollInterval) + d->m_scrollTimer.setInterval(scrollInterval); + } + + QPoint posBoundedToViewport = event->pos(); + posBoundedToViewport.setX(qBound(0, posBoundedToViewport.x(), viewport()->width())); + + int start = d->m_surface->gridToPos(globalToGrid(d->m_activeMouseSelect.start)); + int newEnd = d->m_surface->gridToPos(globalToGrid(viewportToGlobal(posBoundedToViewport))); + + if (start > newEnd) { + std::swap(start, newEnd); + } + if (start < 0) + start = 0; + + if (d->m_selectLineMode) { + newSelection.start = d->m_surface->gridToPos({0, d->m_surface->posToGrid(start).y()}); + newSelection.end = d->m_surface->gridToPos( + {d->m_surface->liveSize().width(), d->m_surface->posToGrid(newEnd).y()}); + } else { + newSelection.start = start; + newSelection.end = newEnd; + } + + setSelection(newSelection); + } else if (event->modifiers() & Qt::ControlModifier) { + checkLinkAt(event->pos()); + } else if (d->m_linkSelection) { + d->m_linkSelection.reset(); + updateViewport(); + } + + if (d->m_linkSelection) { + setCursor(Qt::PointingHandCursor); + } else { + setCursor(Qt::IBeamCursor); + } +} + +void TerminalView::mouseReleaseEvent(QMouseEvent *event) +{ + if (d->m_allowMouseTracking) { + d->m_surface->mouseMove(toGridPos(event), event->modifiers()); + d->m_surface->mouseButton(event->button(), false, event->modifiers()); + } + + d->m_scrollTimer.stop(); + + if (d->m_selection && event->button() == Qt::LeftButton) { + if (d->m_selection->end - d->m_selection->start == 0) + setSelection(std::nullopt); + else + setSelection(Selection{d->m_selection->start, d->m_selection->end, true}); + } +} + +void TerminalView::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (d->m_allowMouseTracking) { + d->m_surface->mouseMove(toGridPos(event), event->modifiers()); + d->m_surface->mouseButton(event->button(), true, event->modifiers()); + d->m_surface->mouseButton(event->button(), false, event->modifiers()); + } + + const auto hit = textAt(event->pos()); + + setSelection(Selection{hit.start, hit.end, true}); + + d->m_lastDoubleClick = std::chrono::system_clock::now(); + + event->accept(); +} + +void TerminalView::wheelEvent(QWheelEvent *event) +{ + verticalScrollBar()->event(event); + + if (!d->m_allowMouseTracking) + return; + + if (event->angleDelta().ry() > 0) + d->m_surface->mouseButton(Qt::ExtraButton1, true, event->modifiers()); + else if (event->angleDelta().ry() < 0) + d->m_surface->mouseButton(Qt::ExtraButton2, true, event->modifiers()); +} + +bool TerminalView::checkLinkAt(const QPoint &pos) +{ + const TextAndOffsets hit = textAt(pos); + + if (hit.text.size() > 0) { + QString t = QString::fromUcs4(hit.text.c_str(), hit.text.size()).trimmed(); + auto newLink = toLink(t); + if (newLink) { + const LinkSelection newSelection = LinkSelection{{hit.start, hit.end}, newLink.value()}; + if (!d->m_linkSelection || *d->m_linkSelection != newSelection) { + d->m_linkSelection = newSelection; + updateViewport(); + } + + return true; + } + } + + if (d->m_linkSelection) { + d->m_linkSelection.reset(); + updateViewport(); + } + return false; +} + +TerminalView::TextAndOffsets TerminalView::textAt(const QPoint &pos) const +{ + auto it = d->m_surface->iteratorAt(globalToGrid(viewportToGlobal(pos))); + auto itRev = d->m_surface->rIteratorAt(globalToGrid(viewportToGlobal(pos))); + + std::u32string whiteSpaces = U" \t\x00a0"; + + const bool inverted = whiteSpaces.find(*it) != std::u32string::npos || *it == 0; + + auto predicate = [inverted, whiteSpaces](const std::u32string::value_type &ch) { + if (inverted) + return ch != 0 && whiteSpaces.find(ch) == std::u32string::npos; + else + return ch == 0 || whiteSpaces.find(ch) != std::u32string::npos; + }; + + auto itRight = std::find_if(it, d->m_surface->end(), predicate); + auto itLeft = std::find_if(itRev, d->m_surface->rend(), predicate); + + std::u32string text; + std::copy(itLeft.base(), it, std::back_inserter(text)); + std::copy(it, itRight, std::back_inserter(text)); + std::transform(text.begin(), text.end(), text.begin(), [](const char32_t &ch) { + return ch == 0 ? U' ' : ch; + }); + + return {(itLeft.base()).position(), itRight.position(), text}; +} + +bool TerminalView::event(QEvent *event) +{ + if (event->type() == QEvent::Paint) { + QPainter p(this); + p.fillRect(QRect(QPoint(0, 0), size()), + d->m_currentColors[(size_t) WidgetColorIdx::Background]); + return true; + } + + // TODO: Is this necessary? + if (event->type() == QEvent::KeyPress) { + auto k = static_cast(event); + keyPressEvent(k); + return true; + } + if (event->type() == QEvent::KeyRelease) { + auto k = static_cast(event); + keyReleaseEvent(k); + return true; + } + + return QAbstractScrollArea::event(event); +} + +} // namespace TerminalSolution diff --git a/src/libs/solutions/terminal/terminalview.h b/src/libs/solutions/terminal/terminalview.h new file mode 100644 index 00000000000..7b0a2f7bc70 --- /dev/null +++ b/src/libs/solutions/terminal/terminalview.h @@ -0,0 +1,221 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "terminal_global.h" +#include "terminalsurface.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace TerminalSolution { + +class SurfaceIntegration; +class TerminalViewPrivate; + +struct SearchHit +{ + int start{-1}; + int end{-1}; + + bool operator!=(const SearchHit &other) const + { + return start != other.start || end != other.end; + } + bool operator==(const SearchHit &other) const { return !operator!=(other); } +}; + +QString defaultFontFamily(); +int defaultFontSize(); + +class TERMINAL_EXPORT TerminalView : public QAbstractScrollArea +{ + friend class CellIterator; + Q_OBJECT +public: + enum class WidgetColorIdx { + Foreground = ColorIndex::Foreground, + Background = ColorIndex::Background, + Selection, + FindMatch, + }; + + TerminalView(QWidget *parent = nullptr); + ~TerminalView() override; + + void setAllowBlinkingCursor(bool allow); + bool allowBlinkingCursor() const; + + void setFont(const QFont &font); + + void copyToClipboard(); + void pasteFromClipboard(); + void copyLinkToClipboard(); + + struct Selection + { + int start; + int end; + bool final{false}; + + bool operator!=(const Selection &other) const + { + return start != other.start || end != other.end || final != other.final; + } + + bool operator==(const Selection &other) const { return !operator!=(other); } + }; + + std::optional selection() const; + void clearSelection(); + + void zoomIn(); + void zoomOut(); + + void moveCursorWordLeft(); + void moveCursorWordRight(); + + void clearContents(); + + void setSurfaceIntegration(SurfaceIntegration *surfaceIntegration); + void setColors(const std::array &colors); + + struct Link + { + QString text; + int targetLine = 0; + int targetColumn = 0; + }; + + struct LinkSelection : public Selection + { + Link link; + + bool operator!=(const LinkSelection &other) const + { + return link.text != other.link.text || link.targetLine != other.link.targetLine + || link.targetColumn != other.link.targetColumn || Selection::operator!=(other); + } + }; + + virtual void writeToPty(const QByteArray &data) { Q_UNUSED(data); } + void writeToTerminal(const QByteArray &data, bool forceFlush); + + void restart(); + + virtual const QList &searchHits() const + { + static QList noHits; + return noHits; + } + + virtual void resizePty(QSize newSize) { Q_UNUSED(newSize); } + virtual void setClipboard(const QString &text) { Q_UNUSED(text); } + virtual std::optional toLink(const QString &text) + { + Q_UNUSED(text); + return std::nullopt; + } + + virtual void selectionChanged(const std::optional &newSelection) + { + Q_UNUSED(newSelection); + } + virtual void linkActivated(const Link &link) { Q_UNUSED(link); } + virtual void contextMenuRequested(const QPoint &pos) { Q_UNUSED(pos); } + + virtual void surfaceChanged(){}; + + TerminalSurface *surface() const; + +protected: + void paintEvent(QPaintEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void focusInEvent(QFocusEvent *event) override; + void focusOutEvent(QFocusEvent *event) override; + void inputMethodEvent(QInputMethodEvent *event) override; + + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + + bool event(QEvent *event) override; + +protected: + void setupSurface(); + + int paintCell(QPainter &p, + const QRectF &cellRect, + QPoint gridPos, + const TerminalCell &cell, + QFont &f, + QList::const_iterator &searchIt) const; + void paintCells(QPainter &painter, QPaintEvent *event) const; + void paintCursor(QPainter &painter) const; + void paintPreedit(QPainter &painter) const; + bool paintFindMatches(QPainter &painter, + QList::const_iterator &searchIt, + const QRectF &cellRect, + const QPoint gridPos) const; + + bool paintSelection(QPainter &painter, const QRectF &cellRect, const QPoint gridPos) const; + void paintDebugSelection(QPainter &painter, const Selection &selection) const; + + qreal topMargin() const; + + QPoint viewportToGlobal(QPoint p) const; + QPoint globalToViewport(QPoint p) const; + QPoint globalToGrid(QPointF p) const; + QPointF gridToGlobal(QPoint p, bool bottom = false, bool right = false) const; + QRect gridToViewport(QRect rect) const; + QPoint toGridPos(QMouseEvent *event) const; + + void updateViewport(); + void updateViewportRect(const QRect &rect); + + int textLineFromPixel(int y) const; + std::optional textPosFromPoint(const QTextLayout &textLayout, QPoint p) const; + + std::optional selectionToFormatRange(Selection selection, + const QTextLayout &layout, + int rowOffset) const; + + bool checkLinkAt(const QPoint &pos); + + struct TextAndOffsets + { + int start; + int end; + std::u32string text; + }; + + TextAndOffsets textAt(const QPoint &pos) const; + + void applySizeChange(); + void updateScrollBars(); + + void flushVTerm(bool force); + + bool setSelection(const std::optional &selection, bool scroll = true); + QString textFromSelection() const; + + void configBlinkTimer(); + + QColor toQColor(std::variant color) const; + +private: + std::unique_ptr d; +}; + +} // namespace TerminalSolution diff --git a/src/plugins/terminal/CMakeLists.txt b/src/plugins/terminal/CMakeLists.txt index bf405b672ab..28037780af2 100644 --- a/src/plugins/terminal/CMakeLists.txt +++ b/src/plugins/terminal/CMakeLists.txt @@ -1,12 +1,8 @@ add_qtc_plugin(Terminal PLUGIN_DEPENDS Core ProjectExplorer - DEPENDS libvterm ptyqt + DEPENDS TerminalLib SOURCES - celliterator.cpp celliterator.h - glyphcache.cpp glyphcache.h - keys.cpp keys.h - scrollback.cpp scrollback.h shellintegration.cpp shellintegration.h shellmodel.cpp shellmodel.h shortcutmap.cpp shortcutmap.h @@ -18,7 +14,6 @@ add_qtc_plugin(Terminal terminalprocessimpl.cpp terminalprocessimpl.h terminalsearch.cpp terminalsearch.h terminalsettings.cpp terminalsettings.h - terminalsurface.cpp terminalsurface.h terminaltr.h terminalwidget.cpp terminalwidget.h ) diff --git a/src/plugins/terminal/shellintegration.cpp b/src/plugins/terminal/shellintegration.cpp index d981dc7269e..44cee54b978 100644 --- a/src/plugins/terminal/shellintegration.cpp +++ b/src/plugins/terminal/shellintegration.cpp @@ -3,10 +3,13 @@ #include "shellintegration.h" +#include "terminalsettings.h" + #include #include #include +#include #include Q_LOGGING_CATEGORY(integrationLog, "qtc.terminal.shellintegration", QtWarningMsg) @@ -74,9 +77,12 @@ bool ShellIntegration::canIntegrate(const Utils::CommandLine &cmdLine) return false; } -void ShellIntegration::onOsc(int cmd, const VTermStringFragment &fragment) +void ShellIntegration::onOsc(int cmd, std::string_view str, bool initial, bool final) { - QString d = QString::fromLocal8Bit(fragment.str, fragment.len); + Q_UNUSED(initial); + Q_UNUSED(final); + + QString d = QString::fromLocal8Bit(str); const auto [command, data] = Utils::splitAtFirst(d, ';'); if (cmd == 1337) { @@ -103,6 +109,17 @@ void ShellIntegration::onOsc(int cmd, const VTermStringFragment &fragment) } } +void ShellIntegration::onBell() +{ + if (settings().audibleBell.value()) + QApplication::beep(); +} + +void ShellIntegration::onTitle(const QString &title) +{ + emit titleChanged(title); +} + void ShellIntegration::prepareProcess(Utils::Process &process) { Environment env = process.environment().hasChanges() ? process.environment() diff --git a/src/plugins/terminal/shellintegration.h b/src/plugins/terminal/shellintegration.h index a4a813c8a65..aac63e63e84 100644 --- a/src/plugins/terminal/shellintegration.h +++ b/src/plugins/terminal/shellintegration.h @@ -7,25 +7,28 @@ #include #include -#include +#include #include namespace Terminal { -class ShellIntegration : public QObject +class ShellIntegration : public QObject, public TerminalSolution::SurfaceIntegration { Q_OBJECT public: static bool canIntegrate(const Utils::CommandLine &cmdLine); - void onOsc(int cmd, const VTermStringFragment &fragment); + void onOsc(int cmd, std::string_view str, bool initial, bool final) override; + void onBell() override; + void onTitle(const QString &title) override; void prepareProcess(Utils::Process &process); signals: void commandChanged(const Utils::CommandLine &command); void currentDirChanged(const QString &dir); + void titleChanged(const QString &title); private: QTemporaryDir m_tempDir; diff --git a/src/plugins/terminal/terminalpane.cpp b/src/plugins/terminal/terminalpane.cpp index 3e4327dc235..13a1abac2f5 100644 --- a/src/plugins/terminal/terminalpane.cpp +++ b/src/plugins/terminal/terminalpane.cpp @@ -80,7 +80,8 @@ TerminalPane::TerminalPane(QObject *parent) m_escSettingButton->setToolTip(Tr::tr("Sends Esc to terminal instead of Qt Creator.")); } else { m_escSettingButton->setText(shiftEsc); - m_escSettingButton->setToolTip(Tr::tr("Press %1 to send Esc to terminal.").arg(shiftEsc)); + m_escSettingButton->setToolTip( + Tr::tr("Press %1 to send Esc to terminal.").arg(shiftEsc)); } }; @@ -161,8 +162,8 @@ void TerminalPane::openTerminal(const OpenTerminalParameters ¶meters) if (icon.isNull()) { QFileIconProvider iconProvider; const FilePath command = parametersCopy.shellCommand - ? parametersCopy.shellCommand->executable() - : settings().shell(); + ? parametersCopy.shellCommand->executable() + : settings().shell(); icon = iconProvider.icon(command.toFileInfo()); } } diff --git a/src/plugins/terminal/terminalsearch.cpp b/src/plugins/terminal/terminalsearch.cpp index d1aa392e9f4..20fa12908cb 100644 --- a/src/plugins/terminal/terminalsearch.cpp +++ b/src/plugins/terminal/terminalsearch.cpp @@ -17,17 +17,18 @@ using namespace std::chrono_literals; namespace Terminal { -using namespace Terminal::Internal; - constexpr std::chrono::milliseconds debounceInterval = 100ms; -TerminalSearch::TerminalSearch(TerminalSurface *surface) +TerminalSearch::TerminalSearch(TerminalSolution::TerminalSurface *surface) : m_surface(surface) { m_debounceTimer.setInterval(debounceInterval); m_debounceTimer.setSingleShot(true); - connect(surface, &TerminalSurface::invalidated, this, &TerminalSearch::updateHits); + connect(surface, + &TerminalSolution::TerminalSurface::invalidated, + this, + &TerminalSearch::updateHits); connect(&m_debounceTimer, &QTimer::timeout, this, &TerminalSearch::debouncedUpdateHits); } @@ -85,9 +86,9 @@ bool isSpace(char32_t a, char32_t b) return false; } -QList TerminalSearch::search() +QList TerminalSearch::search() { - QList hits; + QList hits; std::function compare; @@ -108,12 +109,12 @@ QList TerminalSearch::search() searchString.insert(searchString.begin(), std::numeric_limits::max()); } - Internal::CellIterator it = m_surface->begin(); + TerminalSolution::CellIterator it = m_surface->begin(); while (it != m_surface->end()) { it = std::search(it, m_surface->end(), searchString.begin(), searchString.end(), compare); if (it != m_surface->end()) { - auto hit = SearchHit{it.position(), + auto hit = TerminalSolution::SearchHit{it.position(), static_cast(it.position() + searchString.size())}; if (m_findFlags.testFlag(FindFlag::FindWholeWords)) { hit.start++; @@ -127,9 +128,9 @@ QList TerminalSearch::search() return hits; } -QList TerminalSearch::searchRegex() +QList TerminalSearch::searchRegex() { - QList hits; + QList hits; QString allText; allText.reserve(1000); @@ -170,7 +171,7 @@ QList TerminalSearch::searchRegex() } e -= adjust; } - hits << SearchHit{s, e}; + hits << TerminalSolution::SearchHit{s, e}; } return hits; @@ -185,7 +186,7 @@ void TerminalSearch::debouncedUpdateHits() const bool regex = m_findFlags.testFlag(FindFlag::FindRegularExpression); - QList hits = regex ? searchRegex() : search(); + QList hits = regex ? searchRegex() : search(); if (hits != m_hits) { m_currentHit = -1; diff --git a/src/plugins/terminal/terminalsearch.h b/src/plugins/terminal/terminalsearch.h index a5a66edbcd5..4a176e7c643 100644 --- a/src/plugins/terminal/terminalsearch.h +++ b/src/plugins/terminal/terminalsearch.h @@ -3,7 +3,9 @@ #pragma once -#include "terminalsurface.h" +#include + +#include #include #include @@ -12,19 +14,7 @@ namespace Terminal { -struct SearchHit -{ - int start{-1}; - int end{-1}; - - bool operator!=(const SearchHit &other) const - { - return start != other.start || end != other.end; - } - bool operator==(const SearchHit &other) const { return !operator!=(other); } -}; - -struct SearchHitWithText : SearchHit +struct SearchHitWithText : TerminalSolution::SearchHit { QString text; }; @@ -33,17 +23,17 @@ class TerminalSearch : public Core::IFindSupport { Q_OBJECT public: - TerminalSearch(Internal::TerminalSurface *surface); + TerminalSearch(TerminalSolution::TerminalSurface *surface); void setCurrentSelection(std::optional selection); void setSearchString(const QString &searchString, Utils::FindFlags findFlags); void nextHit(); void previousHit(); - const QList &hits() const { return m_hits; } - SearchHit currentHit() const + const QList &hits() const { return m_hits; } + TerminalSolution::SearchHit currentHit() const { - return m_currentHit >= 0 ? m_hits.at(m_currentHit) : SearchHit{}; + return m_currentHit >= 0 ? m_hits.at(m_currentHit) : TerminalSolution::SearchHit{}; } public: @@ -65,17 +55,17 @@ signals: protected: void updateHits(); void debouncedUpdateHits(); - QList search(); - QList searchRegex(); + QList search(); + QList searchRegex(); private: std::optional m_currentSelection; QString m_currentSearchString; Utils::FindFlags m_findFlags; - Internal::TerminalSurface *m_surface; + TerminalSolution::TerminalSurface *m_surface; int m_currentHit{-1}; - QList m_hits; + QList m_hits; QTimer m_debounceTimer; }; diff --git a/src/plugins/terminal/terminalsettings.cpp b/src/plugins/terminal/terminalsettings.cpp index 9240a100b60..b86cce402d8 100644 --- a/src/plugins/terminal/terminalsettings.cpp +++ b/src/plugins/terminal/terminalsettings.cpp @@ -8,8 +8,6 @@ #include #include -#include - #include #include #include @@ -72,7 +70,6 @@ void setupColor(TerminalSettings *settings, color.setSettingsKey(label); color.setDefaultValue(defaultColor); color.setToolTip(Tr::tr("The color used for %1.").arg(label)); - settings->registerAspect(&color); } @@ -479,7 +476,7 @@ TerminalSettings::TerminalSettings() fontComboBox->setCurrentFont(font()); connect(fontComboBox, &QFontComboBox::currentFontChanged, this, [this](const QFont &f) { - font.setValue(f.family()); + font.setVolatileValue(f.family()); }); auto loadThemeButton = new QPushButton(Tr::tr("Load Theme...")); diff --git a/src/plugins/terminal/terminalwidget.cpp b/src/plugins/terminal/terminalwidget.cpp index 72b7e103483..8916328971f 100644 --- a/src/plugins/terminal/terminalwidget.cpp +++ b/src/plugins/terminal/terminalwidget.cpp @@ -2,10 +2,8 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "terminalwidget.h" -#include "glyphcache.h" #include "terminalconstants.h" #include "terminalsettings.h" -#include "terminalsurface.h" #include "terminaltr.h" #include @@ -25,8 +23,6 @@ #include #include -#include - #include #include #include @@ -47,90 +43,35 @@ #include #include -Q_LOGGING_CATEGORY(terminalLog, "qtc.terminal", QtWarningMsg) -Q_LOGGING_CATEGORY(selectionLog, "qtc.terminal.selection", QtWarningMsg) -Q_LOGGING_CATEGORY(paintLog, "qtc.terminal.paint", QtWarningMsg) - using namespace Utils; using namespace Utils::Terminal; using namespace Core; namespace Terminal { - -namespace ColorIndex { -enum Indices { - Foreground = Internal::ColorIndex::Foreground, - Background = Internal::ColorIndex::Background, - Selection, - FindMatch, -}; -} - -using namespace std::chrono_literals; - -// Minimum time between two refreshes. (30fps) -static constexpr std::chrono::milliseconds minRefreshInterval = 1s / 30; - TerminalWidget::TerminalWidget(QWidget *parent, const OpenTerminalParameters &openParameters) - : QAbstractScrollArea(parent) + : TerminalSolution::TerminalView(parent) , m_context(Utils::Id("TerminalWidget_").withSuffix(QString::number((uintptr_t) this))) , m_openParameters(openParameters) - , m_lastFlush(std::chrono::system_clock::now()) - , m_lastDoubleClick(std::chrono::system_clock::now()) { auto contextObj = new IContext(this); contextObj->setWidget(this); contextObj->setContext(m_context); ICore::addContextObject(contextObj); - setupSurface(); setupFont(); setupColors(); setupActions(); - m_cursorBlinkTimer.setInterval(750); - m_cursorBlinkTimer.setSingleShot(false); + surfaceChanged(); - connect(&m_cursorBlinkTimer, &QTimer::timeout, this, [this]() { - if (hasFocus()) - m_cursorBlinkState = !m_cursorBlinkState; - else - m_cursorBlinkState = true; - updateViewportRect(gridToViewport(QRect{m_cursor.position, m_cursor.position})); - }); - - setAttribute(Qt::WA_InputMethodEnabled); - setAttribute(Qt::WA_MouseTracking); - setAcceptDrops(true); - - setCursor(Qt::IBeamCursor); - - setViewportMargins(1, 1, 1, 1); - - setFocus(); - setFocusPolicy(Qt::StrongFocus); - - setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); - - m_flushDelayTimer.setSingleShot(true); - m_flushDelayTimer.setInterval(minRefreshInterval); - - connect(&m_flushDelayTimer, &QTimer::timeout, this, [this]() { flushVTerm(true); }); - - m_scrollTimer.setSingleShot(false); - m_scrollTimer.setInterval(1s / 2); - connect(&m_scrollTimer, &QTimer::timeout, this, [this] { - if (m_scrollDirection < 0) - verticalScrollBar()->triggerAction(QAbstractSlider::SliderSingleStepSub); - else if (m_scrollDirection > 0) - verticalScrollBar()->triggerAction(QAbstractSlider::SliderSingleStepAdd); - }); + setAllowBlinkingCursor(settings().allowBlinkingCursor()); connect(&settings(), &AspectContainer::applied, this, [this] { // Setup colors first, as setupFont will redraw the screen. setupColors(); setupFont(); configBlinkTimer(); + setAllowBlinkingCursor(settings().allowBlinkingCursor()); }); m_aggregate = new Aggregation::Aggregate(this); @@ -160,9 +101,8 @@ void TerminalWidget::setupPty() m_process->setWorkingDirectory(*m_openParameters.workingDirectory); m_process->setEnvironment(env); - if (m_surface->shellIntegration()) { - m_surface->shellIntegration()->prepareProcess(*m_process.get()); - } + if (m_shellIntegration) + m_shellIntegration->prepareProcess(*m_process.get()); connect(m_process.get(), &Process::readyReadStandardOutput, this, [this]() { onReadyRead(false); @@ -199,12 +139,12 @@ void TerminalWidget::setupPty() if (!errorMessage.isEmpty()) { QByteArray msg = QString("\r\n\033[31m%1").arg(errorMessage).toUtf8(); - m_surface->dataFromPty(msg); + writeToTerminal(msg, true); } else { QString exitMsg = Tr::tr("Process exited with code: %1") .arg(m_process ? m_process->exitCode() : -1); QByteArray msg = QString("\r\n%1").arg(exitMsg).toUtf8(); - m_surface->dataFromPty(msg); + writeToTerminal(msg, true); } } else if (!errorMessage.isEmpty()) { Core::MessageManager::writeFlashing(errorMessage); @@ -241,18 +181,12 @@ void TerminalWidget::setupColors() for (int i = 0; i < 16; ++i) { newColors[i] = settings().colors[i](); } - newColors[ColorIndex::Background] = settings().backgroundColor(); - newColors[ColorIndex::Foreground] = settings().foregroundColor(); - newColors[ColorIndex::Selection] = settings().selectionColor(); - newColors[ColorIndex::FindMatch] = settings().findMatchColor(); + newColors[(size_t) WidgetColorIdx::Background] = settings().backgroundColor.value(); + newColors[(size_t) WidgetColorIdx::Foreground] = settings().foregroundColor.value(); + newColors[(size_t) WidgetColorIdx::Selection] = settings().selectionColor.value(); + newColors[(size_t) WidgetColorIdx::FindMatch] = settings().findMatchColor.value(); - if (m_currentColors == newColors) - return; - - m_currentColors = newColors; - - updateViewport(); - update(); + setColors(newColors); } static bool contextMatcher(QObject *, Qt::ShortcutContext) @@ -336,74 +270,33 @@ void TerminalWidget::writeToPty(const QByteArray &data) m_process->writeRaw(data); } -void TerminalWidget::setupSurface() +void TerminalWidget::resizePty(QSize newSize) +{ + if (m_process && m_process->ptyData()) + m_process->ptyData()->resize(newSize); +} + +void TerminalWidget::surfaceChanged() { m_shellIntegration.reset(new ShellIntegration()); - m_surface = std::make_unique(QSize{80, 60}, m_shellIntegration.get()); - m_search = TerminalSearchPtr(new TerminalSearch(m_surface.get()), [this](TerminalSearch *p) { + setSurfaceIntegration(m_shellIntegration.get()); + + m_search = TerminalSearchPtr(new TerminalSearch(surface()), [this](TerminalSearch *p) { m_aggregate->remove(p); delete p; }); connect(m_search.get(), &TerminalSearch::hitsChanged, this, &TerminalWidget::updateViewport); connect(m_search.get(), &TerminalSearch::currentHitChanged, this, [this] { - SearchHit hit = m_search->currentHit(); + TerminalSolution::SearchHit hit = m_search->currentHit(); if (hit.start >= 0) { setSelection(Selection{hit.start, hit.end, true}, hit != m_lastSelectedHit); m_lastSelectedHit = hit; } }); - connect(m_surface.get(), - &Internal::TerminalSurface::writeToPty, - this, - &TerminalWidget::writeToPty); - connect(m_surface.get(), &Internal::TerminalSurface::fullSizeChanged, this, [this] { - updateScrollBars(); - }); - connect(m_surface.get(), - &Internal::TerminalSurface::invalidated, - this, - [this](const QRect &rect) { - setSelection(std::nullopt); - updateViewportRect(gridToViewport(rect)); - verticalScrollBar()->setValue(m_surface->fullSize().height()); - }); - connect(m_surface.get(), - &Internal::TerminalSurface::cursorChanged, - this, - [this](const Internal::Cursor &oldCursor, const Internal::Cursor &newCursor) { - int startX = oldCursor.position.x(); - int endX = newCursor.position.x(); - - if (startX > endX) - std::swap(startX, endX); - - int startY = oldCursor.position.y(); - int endY = newCursor.position.y(); - if (startY > endY) - std::swap(startY, endY); - - m_cursor = newCursor; - - updateViewportRect( - gridToViewport(QRect{QPoint{startX, startY}, QPoint{endX, endY}})); - configBlinkTimer(); - }); - connect(m_surface.get(), &Internal::TerminalSurface::altscreenChanged, this, [this] { - updateScrollBars(); - if (!setSelection(std::nullopt)) - updateViewport(); - }); - connect(m_surface.get(), &Internal::TerminalSurface::unscroll, this, [this] { - verticalScrollBar()->setValue(verticalScrollBar()->maximum()); - }); - connect(m_surface.get(), &Internal::TerminalSurface::bell, this, [] { - if (settings().audibleBell()) - QApplication::beep(); - }); - connect(m_surface.get(), - &Internal::TerminalSurface::titleChanged, + connect(m_shellIntegration.get(), + &ShellIntegration::titleChanged, this, [this](const QString &title) { const FilePath titleFile = FilePath::fromUserInput(title); @@ -415,46 +308,20 @@ void TerminalWidget::setupSurface() emit titleChanged(); }); - if (m_shellIntegration) { - connect(m_shellIntegration.get(), - &ShellIntegration::commandChanged, - this, - [this](const CommandLine &command) { - m_currentCommand = command; - emit commandChanged(m_currentCommand); - }); - connect(m_shellIntegration.get(), - &ShellIntegration::currentDirChanged, - this, - [this](const QString ¤tDir) { - m_cwd = FilePath::fromUserInput(currentDir); - emit cwdChanged(m_cwd); - }); - } -} - -void TerminalWidget::configBlinkTimer() -{ - bool shouldRun = m_cursor.visible && m_cursor.blink && hasFocus() - && settings().allowBlinkingCursor(); - if (shouldRun != m_cursorBlinkTimer.isActive()) { - if (shouldRun) - m_cursorBlinkTimer.start(); - else - m_cursorBlinkTimer.stop(); - } -} - -QColor TerminalWidget::toQColor(std::variant color) const -{ - if (std::holds_alternative(color)) { - int idx = std::get(color); - if (idx >= 0 && idx < 18) - return m_currentColors[idx]; - - return m_currentColors[ColorIndex::Background]; - } - return std::get(color); + connect(m_shellIntegration.get(), + &ShellIntegration::commandChanged, + this, + [this](const CommandLine &command) { + m_currentCommand = command; + emit commandChanged(m_currentCommand); + }); + connect(m_shellIntegration.get(), + &ShellIntegration::currentDirChanged, + this, + [this](const QString ¤tDir) { + m_cwd = FilePath::fromUserInput(currentDir); + emit cwdChanged(m_cwd); + }); } QString TerminalWidget::title() const @@ -473,194 +340,49 @@ void TerminalWidget::updateCopyState() if (!hasFocus()) return; - m_copy->setEnabled(m_selection.has_value()); + m_copy->setEnabled(selection().has_value()); } -void TerminalWidget::setFont(const QFont &font) +void TerminalWidget::setClipboard(const QString &text) { - m_font = font; - - QFontMetricsF qfm{m_font}; - const qreal w = [qfm]() -> qreal { - if (HostOsInfo::isMacHost()) - return qfm.maxWidth(); - return qfm.averageCharWidth(); - }(); - - qCInfo(terminalLog) << font.family() << font.pointSize() << w << viewport()->size(); - - m_cellSize = {w, (double) qCeil(qfm.height())}; - - QAbstractScrollArea::setFont(m_font); - - if (m_process) { - applySizeChange(); - } -} - -void TerminalWidget::copyToClipboard() -{ - QTC_ASSERT(m_selection.has_value(), return); - - QString text = textFromSelection(); - - qCDebug(selectionLog) << "Copied to clipboard: " << text; - setClipboardAndSelection(text); } -void TerminalWidget::pasteFromClipboard() +std::optional TerminalWidget::toLink(const QString &text) { - QClipboard *clipboard = QApplication::clipboard(); - const QString clipboardText = clipboard->text(QClipboard::Clipboard); + if (text.size() > 0) { + QString result = chopIfEndsWith(text, ':'); - if (clipboardText.isEmpty()) - return; + if (!result.isEmpty()) { + if (result.startsWith("~/")) + result = QDir::homePath() + result.mid(1); - m_surface->pasteFromClipboard(clipboardText); + Utils::Link link = Utils::Link::fromString(result, true); + + if (!link.targetFilePath.isEmpty() && !link.targetFilePath.isAbsolutePath()) + link.targetFilePath = m_cwd.pathAppended(link.targetFilePath.path()); + + if (link.hasValidTarget() + && (link.targetFilePath.scheme().toString().startsWith("http") + || link.targetFilePath.exists())) { + return Link{link.targetFilePath.toString(), link.targetLine, link.targetColumn}; + } + } + } + + return std::nullopt; } -void TerminalWidget::copyLinkToClipboard() +const QList &TerminalWidget::searchHits() const { - if (m_linkSelection) - setClipboardAndSelection(m_linkSelection->link.targetFilePath.toUserOutput()); -} - -void TerminalWidget::clearSelection() -{ - setSelection(std::nullopt); - m_surface->sendKey(Qt::Key_Escape); -} - -void TerminalWidget::zoomIn() -{ - m_font.setPointSize(m_font.pointSize() + 1); - setFont(m_font); -} - -void TerminalWidget::zoomOut() -{ - m_font.setPointSize(qMax(m_font.pointSize() - 1, 1)); - setFont(m_font); -} - -void TerminalWidget::moveCursorWordLeft() -{ - writeToPty("\x1b\x62"); -} - -void TerminalWidget::moveCursorWordRight() -{ - writeToPty("\x1b\x66"); -} - -void TerminalWidget::clearContents() -{ - m_surface->clearAll(); + return m_search->hits(); } void TerminalWidget::onReadyRead(bool forceFlush) { QByteArray data = m_process->readAllRawStandardOutput(); - m_surface->dataFromPty(data); - - flushVTerm(forceFlush); -} - -void TerminalWidget::flushVTerm(bool force) -{ - const std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); - const std::chrono::milliseconds timeSinceLastFlush - = std::chrono::duration_cast(now - m_lastFlush); - - const bool shouldFlushImmediately = timeSinceLastFlush > minRefreshInterval; - if (force || shouldFlushImmediately) { - if (m_flushDelayTimer.isActive()) - m_flushDelayTimer.stop(); - - m_lastFlush = now; - m_surface->flush(); - return; - } - - if (!m_flushDelayTimer.isActive()) { - const std::chrono::milliseconds timeToNextFlush = (minRefreshInterval - timeSinceLastFlush); - m_flushDelayTimer.start(timeToNextFlush.count()); - } -} - -QString TerminalWidget::textFromSelection() const -{ - if (!m_selection) - return {}; - - Internal::CellIterator it = m_surface->iteratorAt(m_selection->start); - Internal::CellIterator end = m_surface->iteratorAt(m_selection->end); - - QTC_ASSERT(it.position() < end.position(), return {}); - - std::u32string s; - bool previousWasZero = false; - for (; it != end; ++it) { - if (it.gridPos().x() == 0 && !s.empty() && previousWasZero) - s += U'\n'; - - if (*it != 0) { - previousWasZero = false; - s += *it; - } else { - previousWasZero = true; - } - } - - return QString::fromUcs4(s.data(), static_cast(s.size())); -} - -bool TerminalWidget::setSelection(const std::optional &selection, bool scroll) -{ - qCDebug(selectionLog) << "setSelection" << selection.has_value(); - if (selection.has_value()) - qCDebug(selectionLog) << "start:" << selection->start << "end:" << selection->end - << "final:" << selection->final; - - if (selectionLog().isDebugEnabled()) - updateViewport(); - - if (selection == m_selection) - return false; - - m_selection = selection; - - updateCopyState(); - - if (m_selection && m_selection->final) { - qCDebug(selectionLog) << "Copy enabled:" << selection.has_value(); - QString text = textFromSelection(); - - QClipboard *clipboard = QApplication::clipboard(); - if (clipboard->supportsSelection()) { - qCDebug(selectionLog) << "Selection set to clipboard: " << text; - clipboard->setText(text, QClipboard::Selection); - } - - if (scroll) { - QPoint start = m_surface->posToGrid(m_selection->start); - QPoint end = m_surface->posToGrid(m_selection->end); - QRect viewRect = gridToViewport(QRect{start, end}); - if (viewRect.y() >= viewport()->height() || viewRect.y() < 0) { - // Selection is outside of the viewport, scroll to it. - verticalScrollBar()->setValue(start.y()); - } - } - - m_search->setCurrentSelection(SearchHitWithText{{selection->start, selection->end}, text}); - } - - if (!selectionLog().isDebugEnabled()) - updateViewport(); - - return true; + writeToTerminal(data, forceFlush); } void TerminalWidget::setShellName(const QString &shellName) @@ -700,874 +422,65 @@ void TerminalWidget::restart(const OpenTerminalParameters &openParameters) { QTC_ASSERT(!m_process || !m_process->isRunning(), return); m_openParameters = openParameters; - m_process.reset(); - setupSurface(); + TerminalView::restart(); setupPty(); } -QPoint TerminalWidget::viewportToGlobal(QPoint p) const +void TerminalWidget::selectionChanged(const std::optional &newSelection) { - int y = p.y() - topMargin(); - const double offset = verticalScrollBar()->value() * m_cellSize.height(); - y += offset; + updateCopyState(); - return {p.x(), y}; -} + if (selection() && selection()->final) { + QString text = textFromSelection(); -QPoint TerminalWidget::globalToViewport(QPoint p) const -{ - int y = p.y() + topMargin(); - const double offset = verticalScrollBar()->value() * m_cellSize.height(); - y -= offset; + QClipboard *clipboard = QApplication::clipboard(); + if (clipboard->supportsSelection()) + clipboard->setText(text, QClipboard::Selection); - return {p.x(), y}; -} - -QPoint TerminalWidget::globalToGrid(QPointF p) const -{ - return QPoint(p.x() / m_cellSize.width(), p.y() / m_cellSize.height()); -} - -QPointF TerminalWidget::gridToGlobal(QPoint p, bool bottom, bool right) const -{ - QPointF result = QPointF(p.x() * m_cellSize.width(), p.y() * m_cellSize.height()); - if (bottom || right) - result += {right ? m_cellSize.width() : 0, bottom ? m_cellSize.height() : 0}; - return result; -} - -qreal TerminalWidget::topMargin() const -{ - return viewport()->size().height() - (m_surface->liveSize().height() * m_cellSize.height()); -} - -static QPixmap generateWavyPixmap(qreal maxRadius, const QPen &pen) -{ - const qreal radiusBase = qMax(qreal(1), maxRadius); - const qreal pWidth = pen.widthF(); - - const QString key = QLatin1String("WaveUnderline-") % pen.color().name() - % QString::number(int(radiusBase), 16) - % QString::number(int(pWidth), 16); - - QPixmap pixmap; - if (QPixmapCache::find(key, &pixmap)) - return pixmap; - - const qreal halfPeriod = qMax(qreal(2), qreal(radiusBase * 1.61803399)); // the golden ratio - const int width = qCeil(100 / (2 * halfPeriod)) * (2 * halfPeriod); - const qreal radius = qFloor(radiusBase * 2) / 2.; - - QPainterPath path; - - qreal xs = 0; - qreal ys = radius; - - while (xs < width) { - xs += halfPeriod; - ys = -ys; - path.quadTo(xs - halfPeriod / 2, ys, xs, 0); + m_search->setCurrentSelection( + SearchHitWithText{{newSelection->start, newSelection->end}, text}); } - - pixmap = QPixmap(width, radius * 2); - pixmap.fill(Qt::transparent); - { - QPen wavePen = pen; - wavePen.setCapStyle(Qt::SquareCap); - - // This is to protect against making the line too fat, as happens on macOS - // due to it having a rather thick width for the regular underline. - const qreal maxPenWidth = .8 * radius; - if (wavePen.widthF() > maxPenWidth) - wavePen.setWidthF(maxPenWidth); - - QPainter imgPainter(&pixmap); - imgPainter.setPen(wavePen); - imgPainter.setRenderHint(QPainter::Antialiasing); - imgPainter.translate(0, radius); - imgPainter.drawPath(path); - } - - QPixmapCache::insert(key, pixmap); - - return pixmap; } -// Copied from qpainter.cpp -static void drawTextItemDecoration(QPainter &painter, - const QPointF &pos, - QTextCharFormat::UnderlineStyle underlineStyle, - QTextItem::RenderFlags flags, - qreal width, - const QColor &underlineColor, - const QRawFont &font) +void TerminalWidget::linkActivated(const Link &link) { - if (underlineStyle == QTextCharFormat::NoUnderline - && !(flags & (QTextItem::StrikeOut | QTextItem::Overline))) + FilePath filePath = FilePath::fromUserInput(link.text); + + if (filePath.scheme().toString().startsWith("http")) { + QDesktopServices::openUrl(filePath.toUrl()); return; - - const QPen oldPen = painter.pen(); - const QBrush oldBrush = painter.brush(); - painter.setBrush(Qt::NoBrush); - QPen pen = oldPen; - pen.setStyle(Qt::SolidLine); - pen.setWidthF(font.lineThickness()); - pen.setCapStyle(Qt::FlatCap); - - QLineF line(qFloor(pos.x()), pos.y(), qFloor(pos.x() + width), pos.y()); - - const qreal underlineOffset = font.underlinePosition(); - - /*if (underlineStyle == QTextCharFormat::SpellCheckUnderline) { - QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme(); - if (theme) - underlineStyle = QTextCharFormat::UnderlineStyle( - theme->themeHint(QPlatformTheme::SpellCheckUnderlineStyle).toInt()); - if (underlineStyle == QTextCharFormat::SpellCheckUnderline) // still not resolved - underlineStyle = QTextCharFormat::WaveUnderline; - }*/ - - if (underlineStyle == QTextCharFormat::WaveUnderline) { - painter.save(); - painter.translate(0, pos.y() + 1); - qreal maxHeight = font.descent() - qreal(1); - - QColor uc = underlineColor; - if (uc.isValid()) - pen.setColor(uc); - - // Adapt wave to underlineOffset or pen width, whatever is larger, to make it work on all platforms - const QPixmap wave = generateWavyPixmap(qMin(qMax(underlineOffset, pen.widthF()), - maxHeight / qreal(2.)), - pen); - const int descent = qFloor(maxHeight); - - painter.setBrushOrigin(painter.brushOrigin().x(), 0); - painter.fillRect(pos.x(), 0, qCeil(width), qMin(wave.height(), descent), wave); - painter.restore(); - } else if (underlineStyle != QTextCharFormat::NoUnderline) { - // Deliberately ceil the offset to avoid the underline coming too close to - // the text above it, but limit it to stay within descent. - qreal adjustedUnderlineOffset = std::ceil(underlineOffset) + 0.5; - if (underlineOffset <= font.descent()) - adjustedUnderlineOffset = qMin(adjustedUnderlineOffset, font.descent() - qreal(0.5)); - const qreal underlinePos = pos.y() + adjustedUnderlineOffset; - QColor uc = underlineColor; - if (uc.isValid()) - pen.setColor(uc); - - pen.setStyle((Qt::PenStyle)(underlineStyle)); - painter.setPen(pen); - QLineF underline(line.x1(), underlinePos, line.x2(), underlinePos); - painter.drawLine(underline); } - pen.setStyle(Qt::SolidLine); - pen.setColor(oldPen.color()); - - if (flags & QTextItem::StrikeOut) { - QLineF strikeOutLine = line; - strikeOutLine.translate(0., -font.ascent() / 3.); - QColor uc = underlineColor; - if (uc.isValid()) - pen.setColor(uc); - painter.setPen(pen); - painter.drawLine(strikeOutLine); - } - - if (flags & QTextItem::Overline) { - QLineF overline = line; - overline.translate(0., -font.ascent()); - QColor uc = underlineColor; - if (uc.isValid()) - pen.setColor(uc); - painter.setPen(pen); - painter.drawLine(overline); - } - - painter.setPen(oldPen); - painter.setBrush(oldBrush); -} - -bool TerminalWidget::paintFindMatches(QPainter &p, - QList::const_iterator &it, - const QRectF &cellRect, - const QPoint gridPos) const -{ - if (it == m_search->hits().constEnd()) - return false; - - const int pos = m_surface->gridToPos(gridPos); - while (it != m_search->hits().constEnd()) { - if (pos < it->start) - return false; - - if (pos >= it->end) { - ++it; - continue; - } - break; - } - - if (it == m_search->hits().constEnd()) - return false; - - p.fillRect(cellRect, m_currentColors[ColorIndex::FindMatch]); - - return true; -} - -bool TerminalWidget::paintSelection(QPainter &p, const QRectF &cellRect, const QPoint gridPos) const -{ - bool isInSelection = false; - const int pos = m_surface->gridToPos(gridPos); - - if (m_selection) - isInSelection = pos >= m_selection->start && pos < m_selection->end; - - if (isInSelection) - p.fillRect(cellRect, m_currentColors[ColorIndex::Selection]); - - return isInSelection; -} - -int TerminalWidget::paintCell(QPainter &p, - const QRectF &cellRect, - QPoint gridPos, - const Internal::TerminalCell &cell, - QFont &f, - QList::const_iterator &searchIt) const -{ - bool paintBackground = !paintSelection(p, cellRect, gridPos) - && !paintFindMatches(p, searchIt, cellRect, gridPos); - - bool isDefaultBg = std::holds_alternative(cell.backgroundColor) - && std::get(cell.backgroundColor) == 17; - - if (paintBackground && !isDefaultBg) - p.fillRect(cellRect, toQColor(cell.backgroundColor)); - - p.setPen(toQColor(cell.foregroundColor)); - - f.setBold(cell.bold); - f.setItalic(cell.italic); - - if (!cell.text.isEmpty()) { - const auto r = Internal::GlyphCache::instance().get(f, cell.text); - - if (r) { - const auto brSize = r->boundingRect().size(); - QPointF brOffset; - if (brSize.width() > cellRect.size().width()) - brOffset.setX(-(brSize.width() - cellRect.size().width()) / 2.0); - if (brSize.height() > cellRect.size().height()) - brOffset.setY(-(brSize.height() - cellRect.size().height()) / 2.0); - - QPointF finalPos = cellRect.topLeft() + brOffset; - - p.drawGlyphRun(finalPos, *r); - - bool tempLink = false; - if (m_linkSelection) { - int chPos = m_surface->gridToPos(gridPos); - tempLink = chPos >= m_linkSelection->start && chPos < m_linkSelection->end; - } - if (cell.underlineStyle != QTextCharFormat::NoUnderline || cell.strikeOut || tempLink) { - QTextItem::RenderFlags flags; - //flags.setFlag(QTextItem::RenderFlag::Underline, cell.format.fontUnderline()); - flags.setFlag(QTextItem::StrikeOut, cell.strikeOut); - finalPos.setY(finalPos.y() + r->rawFont().ascent()); - drawTextItemDecoration(p, - finalPos, - tempLink ? QTextCharFormat::DashUnderline - : cell.underlineStyle, - flags, - cellRect.size().width(), - {}, - r->rawFont()); - } - } - } - - return cell.width; -} - -void TerminalWidget::paintCursor(QPainter &p) const -{ - if (!m_process || !m_process->isRunning()) - return; - - auto cursor = m_surface->cursor(); - - if (!m_preEditString.isEmpty()) - cursor.shape = Internal::Cursor::Shape::Underline; - - const bool blinkState = !cursor.blink || m_cursorBlinkState - || !settings().allowBlinkingCursor(); - - if (cursor.visible && blinkState) { - const int cursorCellWidth = m_surface->cellWidthAt(cursor.position.x(), cursor.position.y()); - - QRectF cursorRect = QRectF(gridToGlobal(cursor.position), - gridToGlobal({cursor.position.x() + cursorCellWidth, - cursor.position.y()}, - true)) - .toAlignedRect(); - - cursorRect.adjust(1, 1, -1, -1); - - QPen pen(Qt::white, 0, Qt::SolidLine); - p.setPen(pen); - - if (hasFocus()) { - QPainter::CompositionMode oldMode = p.compositionMode(); - p.setCompositionMode(QPainter::RasterOp_NotDestination); - switch (cursor.shape) { - case Internal::Cursor::Shape::Block: - p.fillRect(cursorRect, p.pen().brush()); - break; - case Internal::Cursor::Shape::Underline: - p.drawLine(cursorRect.bottomLeft(), cursorRect.bottomRight()); - break; - case Internal::Cursor::Shape::LeftBar: - p.drawLine(cursorRect.topLeft(), cursorRect.bottomLeft()); - break; - } - p.setCompositionMode(oldMode); - } else { - p.drawRect(cursorRect); - } - } -} - -void TerminalWidget::paintPreedit(QPainter &p) const -{ - auto cursor = m_surface->cursor(); - if (!m_preEditString.isEmpty()) { - QRectF rect = QRectF(gridToGlobal(cursor.position), - gridToGlobal({cursor.position.x(), cursor.position.y()}, true, true)); - - rect.setWidth(viewport()->width() - rect.x()); - - p.setPen(toQColor(ColorIndex::Foreground)); - QFont f = font(); - f.setUnderline(true); - p.setFont(f); - p.drawText(rect, Qt::TextDontClip | Qt::TextWrapAnywhere, m_preEditString); - } -} - -void TerminalWidget::paintCells(QPainter &p, QPaintEvent *event) const -{ - QFont f = m_font; - - const int scrollOffset = verticalScrollBar()->value(); - - const int maxRow = m_surface->fullSize().height(); - const int startRow = qFloor((qreal) event->rect().y() / m_cellSize.height()) + scrollOffset; - const int endRow = qMin(maxRow, - qCeil((event->rect().y() + event->rect().height()) / m_cellSize.height()) - + scrollOffset); - - QList::const_iterator searchIt - = std::lower_bound(m_search->hits().constBegin(), - m_search->hits().constEnd(), - startRow, - [this](const SearchHit &hit, int value) { - return m_surface->posToGrid(hit.start).y() < value; - }); - - for (int cellY = startRow; cellY < endRow; ++cellY) { - for (int cellX = 0; cellX < m_surface->liveSize().width();) { - const auto cell = m_surface->fetchCell(cellX, cellY); - - QRectF cellRect(gridToGlobal({cellX, cellY}), - QSizeF{m_cellSize.width() * cell.width, m_cellSize.height()}); - - int numCells = paintCell(p, cellRect, {cellX, cellY}, cell, f, searchIt); - - cellX += numCells; - } - } -} - -void TerminalWidget::paintDebugSelection(QPainter &p, const Selection &selection) const -{ - auto s = globalToViewport(gridToGlobal(m_surface->posToGrid(selection.start)).toPoint()); - const auto e = globalToViewport( - gridToGlobal(m_surface->posToGrid(selection.end), true).toPoint()); - - p.setPen(QPen(Qt::green, 1, Qt::DashLine)); - p.drawLine(s.x(), 0, s.x(), height()); - p.drawLine(0, s.y(), width(), s.y()); - - p.setPen(QPen(Qt::red, 1, Qt::DashLine)); - - p.drawLine(e.x(), 0, e.x(), height()); - p.drawLine(0, e.y(), width(), e.y()); -} - -void TerminalWidget::paintEvent(QPaintEvent *event) -{ - QElapsedTimer t; - t.start(); - event->accept(); - QPainter p(viewport()); - - p.save(); - - if (paintLog().isDebugEnabled()) - p.fillRect(event->rect(), QColor::fromRgb(rand() % 60, rand() % 60, rand() % 60)); + if (filePath.isDir()) + Core::FileUtils::showInFileSystemView(filePath); else - p.fillRect(event->rect(), m_currentColors[ColorIndex::Background]); - - int scrollOffset = verticalScrollBar()->value(); - int offset = -(scrollOffset * m_cellSize.height()); - - qreal margin = topMargin(); - - p.translate(QPointF{0.0, offset + margin}); - - paintCells(p, event); - paintCursor(p); - paintPreedit(p); - - p.restore(); - - p.fillRect(QRectF{{0, 0}, QSizeF{(qreal) width(), topMargin()}}, - m_currentColors[ColorIndex::Background]); - - if (selectionLog().isDebugEnabled()) { - if (m_selection) - paintDebugSelection(p, *m_selection); - if (m_linkSelection) - paintDebugSelection(p, *m_linkSelection); - } - - if (paintLog().isDebugEnabled()) { - QToolTip::showText(this->mapToGlobal(QPoint(width() - 200, 0)), - QString("Paint: %1ms").arg(t.elapsed())); - } + EditorManager::openEditorAt(Utils::Link{filePath, link.targetLine, link.targetColumn}); } -void TerminalWidget::keyPressEvent(QKeyEvent *event) +void TerminalWidget::focusInEvent(QFocusEvent *event) { - // Don't blink during typing - if (m_cursorBlinkTimer.isActive()) { - m_cursorBlinkTimer.start(); - m_cursorBlinkState = true; - } - - if (event->key() == Qt::Key_Escape) { - bool sendToTerminal = settings().sendEscapeToTerminal(); - bool send = false; - if (sendToTerminal && event->modifiers() == Qt::NoModifier) - send = true; - else if (!sendToTerminal && event->modifiers() == Qt::ShiftModifier) - send = true; - - if (send) { - event->setModifiers(Qt::NoModifier); - m_surface->sendKey(event); - return; - } - - if (m_selection) - m_clearSelection->trigger(); - else { - QAction *returnAction = ActionManager::command(Core::Constants::S_RETURNTOEDITOR) - ->actionForContext(Core::Constants::C_GLOBAL); - QTC_ASSERT(returnAction, return); - returnAction->trigger(); - } - return; - } - - if (event->key() == Qt::Key_Control) { - if (!m_linkSelection.has_value() && checkLinkAt(mapFromGlobal(QCursor::pos()))) { - setCursor(Qt::PointingHandCursor); - } - } - - event->accept(); - - m_surface->sendKey(event); -} - -void TerminalWidget::keyReleaseEvent(QKeyEvent *event) -{ - if (event->key() == Qt::Key_Control && m_linkSelection.has_value()) { - m_linkSelection.reset(); - updateCopyState(); - setCursor(Qt::IBeamCursor); - updateViewport(); - } -} - -void TerminalWidget::applySizeChange() -{ - QSize newLiveSize = { - qFloor((qreal) (viewport()->size().width()) / (qreal) m_cellSize.width()), - qFloor((qreal) (viewport()->size().height()) / m_cellSize.height()), - }; - - if (newLiveSize.height() <= 0) - newLiveSize.setHeight(1); - - if (newLiveSize.width() <= 0) - newLiveSize.setWidth(1); - - if (m_process && m_process->ptyData()) - m_process->ptyData()->resize(newLiveSize); - - m_surface->resize(newLiveSize); - flushVTerm(true); -} - -void TerminalWidget::updateScrollBars() -{ - int scrollSize = m_surface->fullSize().height() - m_surface->liveSize().height(); - verticalScrollBar()->setRange(0, scrollSize); - verticalScrollBar()->setValue(verticalScrollBar()->maximum()); - updateViewport(); -} - -void TerminalWidget::resizeEvent(QResizeEvent *event) -{ - event->accept(); - - // If increasing in size, we'll trigger libvterm to call sb_popline in - // order to pull lines out of the history. This will cause the scrollback - // to decrease in size which reduces the size of the verticalScrollBar. - // That will trigger a scroll offset increase which we want to ignore. - m_ignoreScroll = true; - - applySizeChange(); - - setSelection(std::nullopt); - m_ignoreScroll = false; -} - -QRect TerminalWidget::gridToViewport(QRect rect) const -{ - int offset = verticalScrollBar()->value(); - - int startRow = rect.y() - offset; - int numRows = rect.height(); - int numCols = rect.width(); - - QRectF r{rect.x() * m_cellSize.width(), - startRow * m_cellSize.height(), - numCols * m_cellSize.width(), - numRows * m_cellSize.height()}; - - r.translate(0, topMargin()); - - return r.toAlignedRect(); -} - -void TerminalWidget::updateViewport() -{ - viewport()->update(); -} - -void TerminalWidget::updateViewportRect(const QRect &rect) -{ - viewport()->update(rect); -} - -void TerminalWidget::wheelEvent(QWheelEvent *event) -{ - verticalScrollBar()->event(event); - - if (!settings().enableMouseTracking()) - return; - - if (event->angleDelta().ry() > 0) - m_surface->mouseButton(Qt::ExtraButton1, true, event->modifiers()); - else if (event->angleDelta().ry() < 0) - m_surface->mouseButton(Qt::ExtraButton2, true, event->modifiers()); -} - -void TerminalWidget::focusInEvent(QFocusEvent *) -{ - updateViewport(); - configBlinkTimer(); + TerminalView::focusInEvent(event); updateCopyState(); } -void TerminalWidget::focusOutEvent(QFocusEvent *) + +void TerminalWidget::contextMenuRequested(const QPoint &pos) { - updateViewport(); - configBlinkTimer(); -} - -void TerminalWidget::inputMethodEvent(QInputMethodEvent *event) -{ - m_preEditString = event->preeditString(); - - if (event->commitString().isEmpty()) { - updateViewport(); - return; - } - - m_surface->sendKey(event->commitString()); -} - -QPoint TerminalWidget::toGridPos(QMouseEvent *event) const -{ - return globalToGrid(event->pos().toPointF() + QPointF(0, -topMargin() + 0.5)); -} - -void TerminalWidget::mousePressEvent(QMouseEvent *event) -{ - if (settings().enableMouseTracking()) { - m_surface->mouseMove(toGridPos(event), event->modifiers()); - m_surface->mouseButton(event->button(), true, event->modifiers()); - } - - m_scrollDirection = 0; - - m_activeMouseSelect.start = viewportToGlobal(event->pos()); - - if (event->button() == Qt::LeftButton && event->modifiers() & Qt::ControlModifier) { - if (m_linkSelection) { - if (event->modifiers() & Qt::ShiftModifier) { - copyLinkToClipboard(); - return; - } - - if (m_linkSelection->link.targetFilePath.scheme().toString().startsWith("http")) { - QDesktopServices::openUrl(m_linkSelection->link.targetFilePath.toUrl()); - return; - } - - if (m_linkSelection->link.targetFilePath.isDir()) - Core::FileUtils::showInFileSystemView(m_linkSelection->link.targetFilePath); - else - EditorManager::openEditorAt(m_linkSelection->link); - } - return; - } - - if (event->button() == Qt::LeftButton) { - if (std::chrono::system_clock::now() - m_lastDoubleClick < 500ms) { - m_selectLineMode = true; - const Selection newSelection{m_surface->gridToPos( - {0, m_surface->posToGrid(m_selection->start).y()}), - m_surface->gridToPos( - {m_surface->liveSize().width(), - m_surface->posToGrid(m_selection->end).y()}), - false}; - setSelection(newSelection); - } else { - m_selectLineMode = false; - int pos = m_surface->gridToPos(globalToGrid(viewportToGlobal(event->pos()))); - setSelection(Selection{pos, pos, false}); - } - event->accept(); - updateViewport(); - } else if (event->button() == Qt::RightButton) { - if (event->modifiers() & Qt::ShiftModifier) { - QMenu *contextMenu = new QMenu(this); - QAction *configureAction = new QAction(contextMenu); - configureAction->setText(Tr::tr("Configure...")); - connect(configureAction, &QAction::triggered, this, [] { - ICore::showOptionsDialog("Terminal.General"); - }); - - contextMenu->addAction(ActionManager::command(Constants::COPY)->action()); - contextMenu->addAction(ActionManager::command(Constants::PASTE)->action()); - contextMenu->addSeparator(); - contextMenu->addAction(ActionManager::command(Constants::CLEAR_TERMINAL)->action()); - contextMenu->addSeparator(); - contextMenu->addAction(configureAction); - - contextMenu->popup(event->globalPosition().toPoint()); - } else if (m_selection) { - copyToClipboard(); - setSelection(std::nullopt); - } else { - pasteFromClipboard(); - } - } else if (event->button() == Qt::MiddleButton) { - QClipboard *clipboard = QApplication::clipboard(); - if (clipboard->supportsSelection()) { - const QString selectionText = clipboard->text(QClipboard::Selection); - if (!selectionText.isEmpty()) - m_surface->pasteFromClipboard(selectionText); - } else { - m_surface->pasteFromClipboard(textFromSelection()); - } - } -} - -void TerminalWidget::mouseMoveEvent(QMouseEvent *event) -{ - if (settings().enableMouseTracking()) - m_surface->mouseMove(toGridPos(event), event->modifiers()); - - if (m_selection && event->buttons() & Qt::LeftButton) { - Selection newSelection = *m_selection; - int scrollVelocity = 0; - if (event->pos().y() < 0) { - scrollVelocity = (event->pos().y()); - } else if (event->pos().y() > viewport()->height()) { - scrollVelocity = (event->pos().y() - viewport()->height()); - } - - if ((scrollVelocity != 0) != m_scrollTimer.isActive()) { - if (scrollVelocity != 0) - m_scrollTimer.start(); - else - m_scrollTimer.stop(); - } - - m_scrollDirection = scrollVelocity; - - if (m_scrollTimer.isActive() && scrollVelocity != 0) { - const std::chrono::milliseconds scrollInterval = 1000ms / qAbs(scrollVelocity); - if (m_scrollTimer.intervalAsDuration() != scrollInterval) - m_scrollTimer.setInterval(scrollInterval); - } - - QPoint posBoundedToViewport = event->pos(); - posBoundedToViewport.setX(qBound(0, posBoundedToViewport.x(), viewport()->width())); - - int start = m_surface->gridToPos(globalToGrid(m_activeMouseSelect.start)); - int newEnd = m_surface->gridToPos(globalToGrid(viewportToGlobal(posBoundedToViewport))); - - if (start > newEnd) { - std::swap(start, newEnd); - } - if (start < 0) - start = 0; - - if (m_selectLineMode) { - newSelection.start = m_surface->gridToPos({0, m_surface->posToGrid(start).y()}); - newSelection.end = m_surface->gridToPos( - {m_surface->liveSize().width(), m_surface->posToGrid(newEnd).y()}); - } else { - newSelection.start = start; - newSelection.end = newEnd; - } - - setSelection(newSelection); - } else if (event->modifiers() & Qt::ControlModifier) { - checkLinkAt(event->pos()); - } else if (m_linkSelection) { - m_linkSelection.reset(); - updateCopyState(); - updateViewport(); - } - - if (m_linkSelection) { - setCursor(Qt::PointingHandCursor); - } else { - setCursor(Qt::IBeamCursor); - } -} - -bool TerminalWidget::checkLinkAt(const QPoint &pos) -{ - const TextAndOffsets hit = textAt(pos); - - if (hit.text.size() > 0) { - QString t = QString::fromUcs4(hit.text.c_str(), hit.text.size()).trimmed(); - t = chopIfEndsWith(t, ':'); - - if (!t.isEmpty()) { - if (t.startsWith("~/")) - t = QDir::homePath() + t.mid(1); - - Link link = Link::fromString(t, true); - - if (!link.targetFilePath.isEmpty() && !link.targetFilePath.isAbsolutePath()) - link.targetFilePath = m_cwd.pathAppended(link.targetFilePath.path()); - - if (link.hasValidTarget() - && (link.targetFilePath.scheme().toString().startsWith("http") - || link.targetFilePath.exists())) { - const LinkSelection newSelection = LinkSelection{{hit.start, hit.end}, link}; - if (!m_linkSelection || *m_linkSelection != newSelection) { - m_linkSelection = newSelection; - updateViewport(); - updateCopyState(); - } - return true; - } - } - } - - if (m_linkSelection) { - m_linkSelection.reset(); - updateCopyState(); - updateViewport(); - } - return false; -} - -void TerminalWidget::mouseReleaseEvent(QMouseEvent *event) -{ - if (settings().enableMouseTracking()) { - m_surface->mouseMove(toGridPos(event), event->modifiers()); - m_surface->mouseButton(event->button(), false, event->modifiers()); - } - - m_scrollTimer.stop(); - - if (m_selection && event->button() == Qt::LeftButton) { - if (m_selection->end - m_selection->start == 0) - setSelection(std::nullopt); - else - setSelection(Selection{m_selection->start, m_selection->end, true}); - } -} - -TerminalWidget::TextAndOffsets TerminalWidget::textAt(const QPoint &pos) const -{ - auto it = m_surface->iteratorAt(globalToGrid(viewportToGlobal(pos))); - auto itRev = m_surface->rIteratorAt(globalToGrid(viewportToGlobal(pos))); - - std::u32string whiteSpaces = U" \t\x00a0"; - - const bool inverted = whiteSpaces.find(*it) != std::u32string::npos || *it == 0; - - auto predicate = [inverted, whiteSpaces](const std::u32string::value_type &ch) { - if (inverted) - return ch != 0 && whiteSpaces.find(ch) == std::u32string::npos; - else - return ch == 0 || whiteSpaces.find(ch) != std::u32string::npos; - }; - - auto itRight = std::find_if(it, m_surface->end(), predicate); - auto itLeft = std::find_if(itRev, m_surface->rend(), predicate); - - std::u32string text; - std::copy(itLeft.base(), it, std::back_inserter(text)); - std::copy(it, itRight, std::back_inserter(text)); - std::transform(text.begin(), text.end(), text.begin(), [](const char32_t &ch) { - return ch == 0 ? U' ' : ch; + QMenu *contextMenu = new QMenu(this); + QAction *configureAction = new QAction(contextMenu); + configureAction->setText(Tr::tr("Configure...")); + connect(configureAction, &QAction::triggered, this, [] { + ICore::showOptionsDialog("Terminal.General"); }); - return {(itLeft.base()).position(), itRight.position(), text}; -} + contextMenu->addAction(ActionManager::command(Constants::COPY)->action()); + contextMenu->addAction(ActionManager::command(Constants::PASTE)->action()); + contextMenu->addSeparator(); + contextMenu->addAction(ActionManager::command(Constants::CLEAR_TERMINAL)->action()); + contextMenu->addSeparator(); + contextMenu->addAction(configureAction); -void TerminalWidget::mouseDoubleClickEvent(QMouseEvent *event) -{ - if (settings().enableMouseTracking()) { - m_surface->mouseMove(toGridPos(event), event->modifiers()); - m_surface->mouseButton(event->button(), true, event->modifiers()); - m_surface->mouseButton(event->button(), false, event->modifiers()); - } - - const auto hit = textAt(event->pos()); - - setSelection(Selection{hit.start, hit.end, true}); - - m_lastDoubleClick = std::chrono::system_clock::now(); - - event->accept(); + contextMenu->popup(mapToGlobal(pos)); } void TerminalWidget::dragEnterEvent(QDragEnterEvent *event) @@ -1596,38 +509,65 @@ void TerminalWidget::showEvent(QShowEvent *event) if (!m_process) setupPty(); - QAbstractScrollArea::showEvent(event); + TerminalView::showEvent(event); +} + +void TerminalWidget::handleEscKey(QKeyEvent *event) +{ + bool sendToTerminal = settings().sendEscapeToTerminal(); + bool send = false; + if (sendToTerminal && event->modifiers() == Qt::NoModifier) + send = true; + else if (!sendToTerminal && event->modifiers() == Qt::ShiftModifier) + send = true; + + if (send) { + event->setModifiers(Qt::NoModifier); + TerminalView::keyPressEvent(event); + return; + } + + if (selection()) { + clearSelection(); + } else { + QAction *returnAction = ActionManager::command(Core::Constants::S_RETURNTOEDITOR) + ->actionForContext(Core::Constants::C_GLOBAL); + QTC_ASSERT(returnAction, return); + returnAction->trigger(); + } } bool TerminalWidget::event(QEvent *event) { - if (settings().lockKeyboard() && event->type() == QEvent::ShortcutOverride) { - event->accept(); - return true; - } + if (event->type() == QEvent::ShortcutOverride) { + auto keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape && keyEvent->modifiers() == Qt::NoModifier + && settings().sendEscapeToTerminal()) { + event->accept(); + return true; + } - if (event->type() == QEvent::Paint) { - QPainter p(this); - p.fillRect(QRect(QPoint(0, 0), size()), m_currentColors[ColorIndex::Background]); - return true; + if (settings().lockKeyboard()) { + event->accept(); + return true; + } } if (event->type() == QEvent::KeyPress) { auto k = static_cast(event); + if (k->key() == Qt::Key_Escape) { + handleEscKey(k); + return true; + } + if (settings().lockKeyboard() && m_shortcutMap.tryShortcut(k)) return true; keyPressEvent(k); return true; } - if (event->type() == QEvent::KeyRelease) { - auto k = static_cast(event); - keyReleaseEvent(k); - return true; - } - - return QAbstractScrollArea::event(event); + return TerminalView::event(event); } void TerminalWidget::initActions() diff --git a/src/plugins/terminal/terminalwidget.h b/src/plugins/terminal/terminalwidget.h index 2801ea4fc16..4b54562cc6c 100644 --- a/src/plugins/terminal/terminalwidget.h +++ b/src/plugins/terminal/terminalwidget.h @@ -3,83 +3,36 @@ #pragma once +#include "shellintegration.h" #include "shortcutmap.h" #include "terminalsearch.h" -#include "terminalsurface.h" + +#include #include -#include #include +#include #include #include #include -#include -#include -#include -#include - -#include -#include - namespace Terminal { using RegisteredAction = std::unique_ptr>; -class TerminalWidget : public QAbstractScrollArea +class TerminalWidget : public TerminalSolution::TerminalView { - friend class CellIterator; Q_OBJECT public: TerminalWidget(QWidget *parent = nullptr, const Utils::Terminal::OpenTerminalParameters &openParameters = {}); - void setFont(const QFont &font); - - void copyToClipboard(); - void pasteFromClipboard(); - void copyLinkToClipboard(); - - void clearSelection(); - - void zoomIn(); - void zoomOut(); - - void moveCursorWordLeft(); - void moveCursorWordRight(); - - void clearContents(); - void closeTerminal(); TerminalSearch *search() { return m_search.get(); } - struct Selection - { - int start; - int end; - bool final{false}; - - bool operator!=(const Selection &other) const - { - return start != other.start || end != other.end || final != other.final; - } - - bool operator==(const Selection &other) const { return !operator!=(other); } - }; - - struct LinkSelection : public Selection - { - Utils::Link link; - - bool operator!=(const LinkSelection &other) const - { - return link != other.link || Selection::operator!=(other); - } - }; - void setShellName(const QString &shellName); QString shellName() const; QString title() const; @@ -102,142 +55,52 @@ signals: void titleChanged(); protected: - void paintEvent(QPaintEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - void keyReleaseEvent(QKeyEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void wheelEvent(QWheelEvent *event) override; - void focusInEvent(QFocusEvent *event) override; - void focusOutEvent(QFocusEvent *event) override; - void inputMethodEvent(QInputMethodEvent *event) override; - - void mousePressEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void mouseDoubleClickEvent(QMouseEvent *event) override; - void dragEnterEvent(QDragEnterEvent *event) override; void dropEvent(QDropEvent *event) override; - void showEvent(QShowEvent *event) override; - + void focusInEvent(QFocusEvent *event) override; bool event(QEvent *event) override; -protected: void onReadyRead(bool forceFlush); - void setupSurface(); void setupFont(); void setupPty(); void setupColors(); void setupActions(); - void writeToPty(const QByteArray &data); + void handleEscKey(QKeyEvent *event); - int paintCell(QPainter &p, - const QRectF &cellRect, - QPoint gridPos, - const Internal::TerminalCell &cell, - QFont &f, - QList::const_iterator &searchIt) const; - void paintCells(QPainter &painter, QPaintEvent *event) const; - void paintCursor(QPainter &painter) const; - void paintPreedit(QPainter &painter) const; - bool paintFindMatches(QPainter &painter, - QList::const_iterator &searchIt, - const QRectF &cellRect, - const QPoint gridPos) const; - bool paintSelection(QPainter &painter, const QRectF &cellRect, const QPoint gridPos) const; - void paintDebugSelection(QPainter &painter, const Selection &selection) const; + void surfaceChanged() override; - qreal topMargin() const; + void selectionChanged(const std::optional &newSelection) override; + void linkActivated(const Link &link) override; + void contextMenuRequested(const QPoint &pos) override; - QPoint viewportToGlobal(QPoint p) const; - QPoint globalToViewport(QPoint p) const; - QPoint globalToGrid(QPointF p) const; - QPointF gridToGlobal(QPoint p, bool bottom = false, bool right = false) const; - QRect gridToViewport(QRect rect) const; - QPoint toGridPos(QMouseEvent *event) const; + void writeToPty(const QByteArray &data) override; + void resizePty(QSize newSize) override; + void setClipboard(const QString &text) override; + std::optional toLink(const QString &text) override; - void updateViewport(); - void updateViewportRect(const QRect &rect); - - int textLineFromPixel(int y) const; - std::optional textPosFromPoint(const QTextLayout &textLayout, QPoint p) const; - - std::optional selectionToFormatRange( - TerminalWidget::Selection selection, const QTextLayout &layout, int rowOffset) const; - - bool checkLinkAt(const QPoint &pos); - - struct TextAndOffsets - { - int start; - int end; - std::u32string text; - }; - - TextAndOffsets textAt(const QPoint &pos) const; - - void applySizeChange(); - void updateScrollBars(); - - void flushVTerm(bool force); - - bool setSelection(const std::optional &selection, bool scroll = true); - QString textFromSelection() const; - - void configBlinkTimer(); - - QColor toQColor(std::variant color) const; - - void updateCopyState(); + const QList &searchHits() const override; RegisteredAction registerAction(Utils::Id commandId, const Core::Context &context); void registerShortcut(Core::Command *command); + void updateCopyState(); + private: Core::Context m_context; std::unique_ptr m_process; - std::unique_ptr m_surface; std::unique_ptr m_shellIntegration; QString m_shellName; - Utils::Id m_identifier; - - QFont m_font; - QSizeF m_cellSize; - - bool m_ignoreScroll{false}; - - QString m_preEditString; QString m_title; - std::optional m_selection; - std::optional m_linkSelection; + TerminalSolution::SearchHit m_lastSelectedHit{}; - struct - { - QPoint start; - QPoint end; - } m_activeMouseSelect; - - QTimer m_flushDelayTimer; - - QTimer m_scrollTimer; - int m_scrollDirection{0}; - - std::array m_currentColors; + Utils::Id m_identifier; Utils::Terminal::OpenTerminalParameters m_openParameters; - std::chrono::system_clock::time_point m_lastFlush; - std::chrono::system_clock::time_point m_lastDoubleClick; - bool m_selectLineMode{false}; - - Internal::Cursor m_cursor; - QTimer m_cursorBlinkTimer; - bool m_cursorBlinkState{true}; - Utils::FilePath m_cwd; Utils::CommandLine m_currentCommand; @@ -245,7 +108,6 @@ private: TerminalSearchPtr m_search; Aggregation::Aggregate *m_aggregate{nullptr}; - SearchHit m_lastSelectedHit{}; RegisteredAction m_copy; RegisteredAction m_paste; diff --git a/src/plugins/terminal/tests/mouse b/src/plugins/terminal/tests/mouse new file mode 100755 index 00000000000..3134454bb38 --- /dev/null +++ b/src/plugins/terminal/tests/mouse @@ -0,0 +1,67 @@ +#!/bin/sh + + +function cleanup { + stty -echo + printf "\e[?1006;1000l" +} + +trap cleanup EXIT + +stty -echo + +# Enable SGR protocol and button press and release events +printf "\e[?1006;1000h" + +while read -n 1 line +do + printf -v ch "%d" \'$line + if [ "27" != "$ch" ]; then + continue + fi + read -n 1 line + if [ "[" != "$line" ]; then + continue + fi + read -n 1 line + if [ "<" != "$line" ]; then + continue + fi + # Read button state + modifier= + while read -n 1 line + do + if [ ";" = "$line" ]; then + # End + break + fi + printf -v modifier "$modifier$line" + done + # Read column + col= + while read -n 1 line + do + if [ ";" = "$line" ]; then + # End + break + fi + printf -v col "$col$line" + done + # Read row + row= + while read -n 1 line + do + if [ "M" = "$line" ] || [ "m" = "$line" ]; then + # End + btn=$line + break + fi + printf -v row "$row$line" + done + if [ "M" = "$btn" ]; then + echo "You pressed at $col x $row (mods: $modifier)" + else + echo "You released at $col x $row (mods: $modifier)" + fi +done < "${1:-/dev/stdin}" +