2023-02-23 12:47:39 +01:00
|
|
|
// 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 "terminalwidget.h"
|
2023-03-03 17:18:56 +01:00
|
|
|
#include "glyphcache.h"
|
2023-03-23 11:04:54 +01:00
|
|
|
#include "terminalcommands.h"
|
2023-02-23 12:47:39 +01:00
|
|
|
#include "terminalsettings.h"
|
2023-03-03 17:18:56 +01:00
|
|
|
#include "terminalsurface.h"
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
#include <aggregation/aggregate.h>
|
|
|
|
|
|
|
|
|
|
#include <coreplugin/actionmanager/actionmanager.h>
|
|
|
|
|
#include <coreplugin/coreconstants.h>
|
2023-03-03 17:18:56 +01:00
|
|
|
#include <coreplugin/editormanager/editormanager.h>
|
2023-03-23 15:04:02 +01:00
|
|
|
#include <coreplugin/fileutils.h>
|
2023-02-23 12:47:39 +01:00
|
|
|
#include <coreplugin/icore.h>
|
|
|
|
|
|
|
|
|
|
#include <utils/algorithm.h>
|
|
|
|
|
#include <utils/environment.h>
|
2023-03-10 13:55:17 +01:00
|
|
|
#include <utils/fileutils.h>
|
2023-02-23 12:47:39 +01:00
|
|
|
#include <utils/hostosinfo.h>
|
2023-03-02 14:03:54 +01:00
|
|
|
#include <utils/processinterface.h>
|
2023-02-23 12:47:39 +01:00
|
|
|
#include <utils/stringutils.h>
|
|
|
|
|
|
|
|
|
|
#include <vterm.h>
|
|
|
|
|
|
|
|
|
|
#include <QApplication>
|
2023-03-03 17:18:56 +01:00
|
|
|
#include <QCache>
|
2023-02-23 12:47:39 +01:00
|
|
|
#include <QClipboard>
|
2023-03-03 17:18:56 +01:00
|
|
|
#include <QDesktopServices>
|
2023-03-01 08:15:58 +01:00
|
|
|
#include <QElapsedTimer>
|
2023-02-23 12:47:39 +01:00
|
|
|
#include <QGlyphRun>
|
|
|
|
|
#include <QLoggingCategory>
|
2023-03-23 12:28:44 +01:00
|
|
|
#include <QMenu>
|
2023-02-23 12:47:39 +01:00
|
|
|
#include <QPaintEvent>
|
|
|
|
|
#include <QPainter>
|
2023-03-03 17:18:56 +01:00
|
|
|
#include <QPainterPath>
|
|
|
|
|
#include <QPixmapCache>
|
2023-02-23 12:47:39 +01:00
|
|
|
#include <QRawFont>
|
|
|
|
|
#include <QRegularExpression>
|
|
|
|
|
#include <QScrollBar>
|
2023-03-03 17:18:56 +01:00
|
|
|
#include <QTextItem>
|
2023-02-23 12:47:39 +01:00
|
|
|
#include <QTextLayout>
|
2023-03-03 17:18:56 +01:00
|
|
|
#include <QToolTip>
|
2023-02-23 12:47:39 +01:00
|
|
|
|
|
|
|
|
Q_LOGGING_CATEGORY(terminalLog, "qtc.terminal", QtWarningMsg)
|
2023-03-03 10:37:38 +01:00
|
|
|
Q_LOGGING_CATEGORY(selectionLog, "qtc.terminal.selection", QtWarningMsg)
|
2023-03-03 17:18:56 +01:00
|
|
|
Q_LOGGING_CATEGORY(paintLog, "qtc.terminal.paint", QtWarningMsg)
|
2023-02-23 12:47:39 +01:00
|
|
|
|
|
|
|
|
using namespace Utils;
|
|
|
|
|
using namespace Utils::Terminal;
|
|
|
|
|
|
|
|
|
|
namespace Terminal {
|
|
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
namespace ColorIndex {
|
|
|
|
|
enum Indices {
|
|
|
|
|
Foreground = Internal::ColorIndex::Foreground,
|
|
|
|
|
Background = Internal::ColorIndex::Background,
|
|
|
|
|
Selection,
|
|
|
|
|
FindMatch,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 10:48:05 +01:00
|
|
|
using namespace std::chrono_literals;
|
|
|
|
|
|
2023-03-01 16:03:18 +01:00
|
|
|
// Minimum time between two refreshes. (30fps)
|
2023-03-03 08:11:07 +01:00
|
|
|
static constexpr std::chrono::milliseconds minRefreshInterval = 1s / 30;
|
2023-03-01 10:48:05 +01:00
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
TerminalWidget::TerminalWidget(QWidget *parent, const OpenTerminalParameters &openParameters)
|
|
|
|
|
: QAbstractScrollArea(parent)
|
|
|
|
|
, m_openParameters(openParameters)
|
2023-03-03 08:11:07 +01:00
|
|
|
, m_lastFlush(std::chrono::system_clock::now())
|
|
|
|
|
, m_lastDoubleClick(std::chrono::system_clock::now())
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
setupSurface();
|
2023-02-23 12:47:39 +01:00
|
|
|
setupFont();
|
|
|
|
|
setupColors();
|
2023-03-02 15:21:50 +01:00
|
|
|
setupActions();
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-05 23:55:37 +01:00
|
|
|
m_cursorBlinkTimer.setInterval(750);
|
|
|
|
|
m_cursorBlinkTimer.setSingleShot(false);
|
|
|
|
|
|
|
|
|
|
connect(&m_cursorBlinkTimer, &QTimer::timeout, this, [this]() {
|
|
|
|
|
if (hasFocus())
|
|
|
|
|
m_cursorBlinkState = !m_cursorBlinkState;
|
|
|
|
|
else
|
|
|
|
|
m_cursorBlinkState = true;
|
2023-03-24 12:53:21 +01:00
|
|
|
updateViewportRect(gridToViewport(QRect{m_cursor.position, m_cursor.position}));
|
2023-03-05 23:55:37 +01:00
|
|
|
});
|
|
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
setAttribute(Qt::WA_InputMethodEnabled);
|
|
|
|
|
setAttribute(Qt::WA_MouseTracking);
|
|
|
|
|
|
|
|
|
|
setCursor(Qt::IBeamCursor);
|
|
|
|
|
|
2023-02-25 11:53:26 +01:00
|
|
|
setViewportMargins(1, 1, 1, 1);
|
|
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
setFocus();
|
|
|
|
|
setFocusPolicy(Qt::StrongFocus);
|
|
|
|
|
|
|
|
|
|
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
|
|
|
|
|
|
2023-03-01 10:48:05 +01:00
|
|
|
m_flushDelayTimer.setSingleShot(true);
|
|
|
|
|
m_flushDelayTimer.setInterval(minRefreshInterval);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-01 10:48:05 +01:00
|
|
|
connect(&m_flushDelayTimer, &QTimer::timeout, this, [this]() { flushVTerm(true); });
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-09 22:33:47 +01:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2023-03-01 08:15:58 +01:00
|
|
|
connect(&TerminalSettings::instance(), &AspectContainer::applied, this, [this] {
|
2023-02-23 12:47:39 +01:00
|
|
|
// Setup colors first, as setupFont will redraw the screen.
|
|
|
|
|
setupColors();
|
|
|
|
|
setupFont();
|
2023-03-05 23:55:37 +01:00
|
|
|
configBlinkTimer();
|
2023-02-23 12:47:39 +01:00
|
|
|
});
|
2023-03-24 12:53:21 +01:00
|
|
|
|
|
|
|
|
m_aggregate = new Aggregation::Aggregate(this);
|
|
|
|
|
m_aggregate->add(this);
|
|
|
|
|
m_aggregate->add(m_search.get());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TerminalWidget::~TerminalWidget()
|
|
|
|
|
{
|
|
|
|
|
// The Aggregate stuff tries to do clever deletion of the children, but we
|
|
|
|
|
// we don't want that.
|
|
|
|
|
m_aggregate->remove(this);
|
|
|
|
|
m_aggregate->remove(m_search.get());
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::setupPty()
|
|
|
|
|
{
|
2023-03-01 08:15:58 +01:00
|
|
|
m_process = std::make_unique<QtcProcess>();
|
2023-02-23 12:47:39 +01:00
|
|
|
|
|
|
|
|
Environment env = m_openParameters.environment.value_or(Environment::systemEnvironment());
|
|
|
|
|
|
|
|
|
|
CommandLine shellCommand = m_openParameters.shellCommand.value_or(
|
|
|
|
|
CommandLine{TerminalSettings::instance().shell.filePath(), {}});
|
|
|
|
|
|
2023-02-23 15:21:26 +01:00
|
|
|
// For git bash on Windows
|
|
|
|
|
env.prependOrSetPath(shellCommand.executable().parentDir());
|
2023-03-01 08:15:58 +01:00
|
|
|
if (env.hasKey("CLINK_NOAUTORUN"))
|
|
|
|
|
env.unset("CLINK_NOAUTORUN");
|
2023-02-23 15:21:26 +01:00
|
|
|
|
2023-03-01 08:15:58 +01:00
|
|
|
m_process->setProcessMode(ProcessMode::Writer);
|
2023-03-14 12:10:59 +01:00
|
|
|
m_process->setPtyData(Utils::Pty::Data());
|
2023-03-01 08:15:58 +01:00
|
|
|
m_process->setCommand(shellCommand);
|
2023-03-01 09:57:43 +01:00
|
|
|
if (m_openParameters.workingDirectory.has_value())
|
|
|
|
|
m_process->setWorkingDirectory(*m_openParameters.workingDirectory);
|
2023-03-01 08:15:58 +01:00
|
|
|
m_process->setEnvironment(env);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-10 13:55:17 +01:00
|
|
|
if (m_surface->shellIntegration()) {
|
|
|
|
|
m_surface->shellIntegration()->prepareProcess(*m_process.get());
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 10:48:05 +01:00
|
|
|
connect(m_process.get(), &QtcProcess::readyReadStandardOutput, this, [this]() {
|
|
|
|
|
onReadyRead(false);
|
2023-02-23 12:47:39 +01:00
|
|
|
});
|
|
|
|
|
|
2023-03-01 08:15:58 +01:00
|
|
|
connect(m_process.get(), &QtcProcess::done, this, [this] {
|
|
|
|
|
if (m_process) {
|
|
|
|
|
if (m_process->exitCode() != 0) {
|
2023-02-23 12:47:39 +01:00
|
|
|
QByteArray msg = QString("\r\n\033[31mProcess exited with code: %1")
|
2023-03-01 08:15:58 +01:00
|
|
|
.arg(m_process->exitCode())
|
2023-02-23 12:47:39 +01:00
|
|
|
.toUtf8();
|
|
|
|
|
|
2023-03-01 08:15:58 +01:00
|
|
|
if (!m_process->errorString().isEmpty())
|
|
|
|
|
msg += QString(" (%1)").arg(m_process->errorString()).toUtf8();
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
m_surface->dataFromPty(msg);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_openParameters.m_exitBehavior == ExitBehavior::Restart) {
|
2023-03-01 10:48:05 +01:00
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this] {
|
2023-03-01 08:15:58 +01:00
|
|
|
m_process.reset();
|
2023-03-07 17:55:38 +01:00
|
|
|
setupSurface();
|
2023-02-23 12:47:39 +01:00
|
|
|
setupPty();
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_openParameters.m_exitBehavior == ExitBehavior::Close)
|
|
|
|
|
deleteLater();
|
|
|
|
|
|
|
|
|
|
if (m_openParameters.m_exitBehavior == ExitBehavior::Keep) {
|
|
|
|
|
QByteArray msg = QString("\r\nProcess exited with code: %1")
|
2023-03-01 08:15:58 +01:00
|
|
|
.arg(m_process ? m_process->exitCode() : -1)
|
2023-02-23 12:47:39 +01:00
|
|
|
.toUtf8();
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
m_surface->dataFromPty(msg);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
});
|
2023-03-01 08:15:58 +01:00
|
|
|
|
|
|
|
|
connect(m_process.get(), &QtcProcess::started, this, [this] {
|
2023-03-07 17:55:38 +01:00
|
|
|
if (m_shellName.isEmpty())
|
|
|
|
|
m_shellName = m_process->commandLine().executable().fileName();
|
2023-03-02 16:59:03 +01:00
|
|
|
if (HostOsInfo::isWindowsHost() && m_shellName.endsWith(QTC_WIN_EXE_SUFFIX))
|
|
|
|
|
m_shellName.chop(QStringLiteral(QTC_WIN_EXE_SUFFIX).size());
|
|
|
|
|
|
2023-03-01 08:15:58 +01:00
|
|
|
applySizeChange();
|
|
|
|
|
emit started(m_process->processId());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
m_process->start();
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::setupFont()
|
|
|
|
|
{
|
|
|
|
|
QFont f;
|
|
|
|
|
f.setFixedPitch(true);
|
|
|
|
|
f.setFamily(TerminalSettings::instance().font.value());
|
|
|
|
|
f.setPointSize(TerminalSettings::instance().fontSize.value());
|
|
|
|
|
|
|
|
|
|
setFont(f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::setupColors()
|
|
|
|
|
{
|
|
|
|
|
// Check if the colors have changed.
|
2023-03-24 12:53:21 +01:00
|
|
|
std::array<QColor, 20> newColors;
|
2023-02-23 12:47:39 +01:00
|
|
|
for (int i = 0; i < 16; ++i) {
|
|
|
|
|
newColors[i] = TerminalSettings::instance().colors[i].value();
|
|
|
|
|
}
|
2023-03-24 12:53:21 +01:00
|
|
|
newColors[ColorIndex::Background] = TerminalSettings::instance().backgroundColor.value();
|
|
|
|
|
newColors[ColorIndex::Foreground] = TerminalSettings::instance().foregroundColor.value();
|
|
|
|
|
newColors[ColorIndex::Selection] = TerminalSettings::instance().selectionColor.value();
|
|
|
|
|
newColors[ColorIndex::FindMatch] = TerminalSettings::instance().findMatchColor.value();
|
2023-02-23 12:47:39 +01:00
|
|
|
|
|
|
|
|
if (m_currentColors == newColors)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_currentColors = newColors;
|
|
|
|
|
|
2023-03-22 15:39:55 +01:00
|
|
|
updateViewport();
|
|
|
|
|
update();
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-02 15:21:50 +01:00
|
|
|
void TerminalWidget::setupActions()
|
|
|
|
|
{
|
2023-03-23 11:04:54 +01:00
|
|
|
WidgetActions &a = TerminalCommands::widgetActions();
|
|
|
|
|
|
|
|
|
|
auto ifHasFocus = [this](void (TerminalWidget::*f)()) {
|
|
|
|
|
return [this, f] {
|
|
|
|
|
if (hasFocus())
|
|
|
|
|
(this->*f)();
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// clang-format off
|
|
|
|
|
connect(&a.copy, &QAction::triggered, this, ifHasFocus(&TerminalWidget::copyToClipboard));
|
|
|
|
|
connect(&a.paste, &QAction::triggered, this, ifHasFocus(&TerminalWidget::pasteFromClipboard));
|
|
|
|
|
connect(&a.clearSelection, &QAction::triggered, this, ifHasFocus(&TerminalWidget::clearSelection));
|
2023-03-23 12:28:44 +01:00
|
|
|
connect(&a.clearTerminal, &QAction::triggered, this, ifHasFocus(&TerminalWidget::clearContents));
|
2023-03-23 14:51:24 +01:00
|
|
|
connect(&a.moveCursorWordLeft, &QAction::triggered, this, ifHasFocus(&TerminalWidget::moveCursorWordLeft));
|
|
|
|
|
connect(&a.moveCursorWordRight, &QAction::triggered, this, ifHasFocus(&TerminalWidget::moveCursorWordRight));
|
2023-03-23 11:04:54 +01:00
|
|
|
// clang-format on
|
2023-03-02 15:21:50 +01:00
|
|
|
}
|
|
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
void TerminalWidget::writeToPty(const QByteArray &data)
|
|
|
|
|
{
|
2023-03-07 17:55:38 +01:00
|
|
|
if (m_process && m_process->isRunning())
|
2023-03-01 08:15:58 +01:00
|
|
|
m_process->writeRaw(data);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
void TerminalWidget::setupSurface()
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-10 13:55:17 +01:00
|
|
|
m_shellIntegration.reset(new ShellIntegration());
|
|
|
|
|
m_surface = std::make_unique<Internal::TerminalSurface>(QSize{80, 60}, m_shellIntegration.get());
|
2023-03-24 12:53:21 +01:00
|
|
|
m_search = std::make_unique<TerminalSearch>(m_surface.get());
|
|
|
|
|
|
|
|
|
|
connect(m_search.get(), &TerminalSearch::hitsChanged, this, &TerminalWidget::updateViewport);
|
|
|
|
|
connect(m_search.get(), &TerminalSearch::currentHitChanged, this, [this] {
|
|
|
|
|
SearchHit hit = m_search->currentHit();
|
|
|
|
|
if (hit.start >= 0) {
|
|
|
|
|
setSelection(Selection{hit.start, hit.end, true}, hit != m_lastSelectedHit);
|
|
|
|
|
m_lastSelectedHit = hit;
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-03-03 17:18:56 +01:00
|
|
|
|
|
|
|
|
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,
|
2023-03-06 18:37:45 +01:00
|
|
|
[this](const QRect &rect) {
|
2023-03-17 11:01:49 +01:00
|
|
|
setSelection(std::nullopt);
|
2023-03-24 12:53:21 +01:00
|
|
|
updateViewportRect(gridToViewport(rect));
|
|
|
|
|
verticalScrollBar()->setValue(m_surface->fullSize().height());
|
2023-03-06 18:37:45 +01:00
|
|
|
});
|
2023-03-03 17:18:56 +01:00
|
|
|
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);
|
|
|
|
|
|
2023-03-05 23:55:37 +01:00
|
|
|
m_cursor = newCursor;
|
|
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
updateViewportRect(
|
|
|
|
|
gridToViewport(QRect{QPoint{startX, startY}, QPoint{endX, endY}}));
|
2023-03-05 23:55:37 +01:00
|
|
|
configBlinkTimer();
|
2023-03-03 17:18:56 +01:00
|
|
|
});
|
|
|
|
|
connect(m_surface.get(), &Internal::TerminalSurface::altscreenChanged, this, [this] {
|
2023-03-06 18:16:28 +01:00
|
|
|
updateScrollBars();
|
2023-03-17 11:01:49 +01:00
|
|
|
if (!setSelection(std::nullopt))
|
|
|
|
|
updateViewport();
|
2023-03-03 17:18:56 +01:00
|
|
|
});
|
|
|
|
|
connect(m_surface.get(), &Internal::TerminalSurface::unscroll, this, [this] {
|
|
|
|
|
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
|
|
|
|
|
});
|
2023-03-24 22:14:18 +01:00
|
|
|
connect(m_surface.get(), &Internal::TerminalSurface::bell, this, [] {
|
|
|
|
|
if (TerminalSettings::instance().audibleBell.value())
|
|
|
|
|
QApplication::beep();
|
|
|
|
|
});
|
|
|
|
|
|
2023-03-10 13:55:17 +01:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-05 23:55:37 +01:00
|
|
|
void TerminalWidget::configBlinkTimer()
|
|
|
|
|
{
|
|
|
|
|
bool shouldRun = m_cursor.visible && m_cursor.blink && hasFocus()
|
|
|
|
|
&& TerminalSettings::instance().allowBlinkingCursor.value();
|
|
|
|
|
if (shouldRun != m_cursorBlinkTimer.isActive()) {
|
|
|
|
|
if (shouldRun)
|
|
|
|
|
m_cursorBlinkTimer.start();
|
|
|
|
|
else
|
|
|
|
|
m_cursorBlinkTimer.stop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-22 15:39:55 +01:00
|
|
|
QColor TerminalWidget::toQColor(std::variant<int, QColor> color) const
|
|
|
|
|
{
|
|
|
|
|
if (std::holds_alternative<int>(color)) {
|
|
|
|
|
int idx = std::get<int>(color);
|
|
|
|
|
if (idx >= 0 && idx < 18)
|
|
|
|
|
return m_currentColors[idx];
|
|
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
return m_currentColors[ColorIndex::Background];
|
2023-03-22 15:39:55 +01:00
|
|
|
}
|
|
|
|
|
return std::get<QColor>(color);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:04:54 +01:00
|
|
|
void TerminalWidget::updateCopyState()
|
|
|
|
|
{
|
|
|
|
|
if (!hasFocus())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
TerminalCommands::widgetActions().copy.setEnabled(m_selection.has_value());
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
void TerminalWidget::setFont(const QFont &font)
|
|
|
|
|
{
|
|
|
|
|
m_font = font;
|
|
|
|
|
|
|
|
|
|
QFontMetricsF qfm{m_font};
|
2023-03-03 17:18:56 +01:00
|
|
|
const qreal w = [qfm]() -> qreal {
|
2023-02-23 12:47:39 +01:00
|
|
|
if (HostOsInfo::isMacHost())
|
|
|
|
|
return qfm.maxWidth();
|
|
|
|
|
return qfm.averageCharWidth();
|
|
|
|
|
}();
|
|
|
|
|
|
2023-02-25 11:53:26 +01:00
|
|
|
qCInfo(terminalLog) << font.family() << font.pointSize() << w << viewport()->size();
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
m_cellSize = {w, (double) qCeil(qfm.height())};
|
2023-02-23 12:47:39 +01:00
|
|
|
|
|
|
|
|
QAbstractScrollArea::setFont(m_font);
|
|
|
|
|
|
2023-03-01 08:15:58 +01:00
|
|
|
if (m_process) {
|
2023-02-23 12:47:39 +01:00
|
|
|
applySizeChange();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:04:54 +01:00
|
|
|
void TerminalWidget::copyToClipboard()
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-23 11:04:54 +01:00
|
|
|
QTC_ASSERT(m_selection.has_value(), return);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-17 11:01:49 +01:00
|
|
|
QString text = textFromSelection();
|
2023-03-03 16:40:11 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
qCDebug(selectionLog) << "Copied to clipboard: " << text;
|
|
|
|
|
|
|
|
|
|
setClipboardAndSelection(text);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
2023-03-03 17:18:56 +01:00
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
void TerminalWidget::pasteFromClipboard()
|
|
|
|
|
{
|
|
|
|
|
QClipboard *clipboard = QApplication::clipboard();
|
|
|
|
|
const QString clipboardText = clipboard->text(QClipboard::Clipboard);
|
|
|
|
|
|
|
|
|
|
if (clipboardText.isEmpty())
|
|
|
|
|
return;
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
m_surface->pasteFromClipboard(clipboardText);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::clearSelection()
|
|
|
|
|
{
|
2023-03-02 15:21:50 +01:00
|
|
|
setSelection(std::nullopt);
|
2023-03-03 17:18:56 +01:00
|
|
|
m_surface->sendKey(Qt::Key_Escape);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
2023-03-03 17:18:56 +01:00
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
void TerminalWidget::zoomIn()
|
|
|
|
|
{
|
|
|
|
|
m_font.setPointSize(m_font.pointSize() + 1);
|
|
|
|
|
setFont(m_font);
|
|
|
|
|
}
|
2023-03-03 17:18:56 +01:00
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
void TerminalWidget::zoomOut()
|
|
|
|
|
{
|
|
|
|
|
m_font.setPointSize(qMax(m_font.pointSize() - 1, 1));
|
|
|
|
|
setFont(m_font);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 14:51:24 +01:00
|
|
|
void TerminalWidget::moveCursorWordLeft()
|
|
|
|
|
{
|
|
|
|
|
writeToPty("\x1b\x62");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::moveCursorWordRight()
|
|
|
|
|
{
|
|
|
|
|
writeToPty("\x1b\x66");
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
void TerminalWidget::clearContents()
|
|
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
m_surface->clearAll();
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-01 10:48:05 +01:00
|
|
|
void TerminalWidget::onReadyRead(bool forceFlush)
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-01 08:15:58 +01:00
|
|
|
QByteArray data = m_process->readAllRawStandardOutput();
|
2023-03-03 17:18:56 +01:00
|
|
|
|
|
|
|
|
m_surface->dataFromPty(data);
|
2023-03-01 10:48:05 +01:00
|
|
|
|
|
|
|
|
flushVTerm(forceFlush);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::flushVTerm(bool force)
|
|
|
|
|
{
|
2023-03-03 08:11:07 +01:00
|
|
|
const std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
|
|
|
|
|
const std::chrono::milliseconds timeSinceLastFlush
|
|
|
|
|
= std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastFlush);
|
|
|
|
|
|
|
|
|
|
const bool shouldFlushImmediately = timeSinceLastFlush > minRefreshInterval;
|
|
|
|
|
if (force || shouldFlushImmediately) {
|
|
|
|
|
if (m_flushDelayTimer.isActive())
|
|
|
|
|
m_flushDelayTimer.stop();
|
|
|
|
|
|
|
|
|
|
m_lastFlush = now;
|
2023-03-03 17:18:56 +01:00
|
|
|
m_surface->flush();
|
2023-03-01 10:48:05 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-03 08:11:07 +01:00
|
|
|
if (!m_flushDelayTimer.isActive()) {
|
|
|
|
|
const std::chrono::milliseconds timeToNextFlush = (minRefreshInterval - timeSinceLastFlush);
|
|
|
|
|
m_flushDelayTimer.start(timeToNextFlush.count());
|
|
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-17 11:01:49 +01:00
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
std::u32string s;
|
2023-03-24 12:53:21 +01:00
|
|
|
bool previousWasZero = false;
|
2023-03-17 11:01:49 +01:00
|
|
|
for (; it != end; ++it) {
|
2023-03-24 12:53:21 +01:00
|
|
|
if (it.gridPos().x() == 0 && !s.empty() && previousWasZero)
|
2023-03-17 11:01:49 +01:00
|
|
|
s += U'\n';
|
2023-03-24 12:53:21 +01:00
|
|
|
|
|
|
|
|
if (*it != 0) {
|
|
|
|
|
previousWasZero = false;
|
2023-03-17 11:01:49 +01:00
|
|
|
s += *it;
|
2023-03-24 12:53:21 +01:00
|
|
|
} else {
|
|
|
|
|
previousWasZero = true;
|
|
|
|
|
}
|
2023-03-17 11:01:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return QString::fromUcs4(s.data(), static_cast<int>(s.size()));
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
bool TerminalWidget::setSelection(const std::optional<Selection> &selection, bool scroll)
|
2023-03-02 15:21:50 +01:00
|
|
|
{
|
2023-03-24 12:53:21 +01:00
|
|
|
qCDebug(selectionLog) << "setSelection" << selection.has_value();
|
|
|
|
|
if (selection.has_value())
|
|
|
|
|
qCDebug(selectionLog) << "start:" << selection->start << "end:" << selection->end
|
|
|
|
|
<< "final:" << selection->final;
|
|
|
|
|
|
2023-03-17 11:01:49 +01:00
|
|
|
if (selectionLog().isDebugEnabled())
|
|
|
|
|
updateViewport();
|
|
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
if (selection == m_selection)
|
2023-03-17 11:01:49 +01:00
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
m_selection = selection;
|
|
|
|
|
|
2023-03-23 11:04:54 +01:00
|
|
|
updateCopyState();
|
|
|
|
|
|
2023-03-17 11:01:49 +01:00
|
|
|
if (m_selection && m_selection->final) {
|
2023-03-03 10:37:38 +01:00
|
|
|
qCDebug(selectionLog) << "Copy enabled:" << selection.has_value();
|
2023-03-24 12:53:21 +01:00
|
|
|
QString text = textFromSelection();
|
2023-03-06 18:37:45 +01:00
|
|
|
|
2023-03-17 11:01:49 +01:00
|
|
|
QClipboard *clipboard = QApplication::clipboard();
|
|
|
|
|
if (clipboard->supportsSelection()) {
|
|
|
|
|
qCDebug(selectionLog) << "Selection set to clipboard: " << text;
|
|
|
|
|
clipboard->setText(text, QClipboard::Selection);
|
|
|
|
|
}
|
2023-03-24 12:53:21 +01:00
|
|
|
|
|
|
|
|
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});
|
2023-03-06 18:37:45 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-17 11:01:49 +01:00
|
|
|
if (!selectionLog().isDebugEnabled())
|
|
|
|
|
updateViewport();
|
|
|
|
|
|
2023-03-06 18:37:45 +01:00
|
|
|
return true;
|
2023-03-02 15:21:50 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-07 17:55:38 +01:00
|
|
|
void TerminalWidget::setShellName(const QString &shellName)
|
|
|
|
|
{
|
|
|
|
|
m_shellName = shellName;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-02 16:59:03 +01:00
|
|
|
QString TerminalWidget::shellName() const
|
|
|
|
|
{
|
|
|
|
|
return m_shellName;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-10 13:55:17 +01:00
|
|
|
FilePath TerminalWidget::cwd() const
|
|
|
|
|
{
|
|
|
|
|
return m_cwd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CommandLine TerminalWidget::currentCommand() const
|
|
|
|
|
{
|
|
|
|
|
return m_currentCommand;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-07 17:55:38 +01:00
|
|
|
std::optional<Id> TerminalWidget::identifier() const
|
|
|
|
|
{
|
|
|
|
|
return m_openParameters.identifier;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QProcess::ProcessState TerminalWidget::processState() const
|
|
|
|
|
{
|
|
|
|
|
if (m_process)
|
|
|
|
|
return m_process->state();
|
|
|
|
|
|
|
|
|
|
return QProcess::NotRunning;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::restart(const OpenTerminalParameters &openParameters)
|
|
|
|
|
{
|
|
|
|
|
QTC_ASSERT(!m_process || !m_process->isRunning(), return);
|
|
|
|
|
m_openParameters = openParameters;
|
|
|
|
|
|
|
|
|
|
m_process.reset();
|
|
|
|
|
setupSurface();
|
|
|
|
|
setupPty();
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
QPoint TerminalWidget::viewportToGlobal(QPoint p) const
|
|
|
|
|
{
|
|
|
|
|
int y = p.y() - topMargin();
|
2023-03-03 17:18:56 +01:00
|
|
|
const double offset = verticalScrollBar()->value() * m_cellSize.height();
|
2023-02-23 12:47:39 +01:00
|
|
|
y += offset;
|
|
|
|
|
|
|
|
|
|
return {p.x(), y};
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-03 10:37:38 +01:00
|
|
|
QPoint TerminalWidget::globalToViewport(QPoint p) const
|
|
|
|
|
{
|
|
|
|
|
int y = p.y() + topMargin();
|
2023-03-03 17:18:56 +01:00
|
|
|
const double offset = verticalScrollBar()->value() * m_cellSize.height();
|
2023-03-03 10:37:38 +01:00
|
|
|
y -= offset;
|
|
|
|
|
|
|
|
|
|
return {p.x(), y};
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
QPoint TerminalWidget::globalToGrid(QPointF p) const
|
2023-03-02 12:07:53 +01:00
|
|
|
{
|
|
|
|
|
return QPoint(p.x() / m_cellSize.width(), p.y() / m_cellSize.height());
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
QPointF TerminalWidget::gridToGlobal(QPoint p, bool bottom, bool right) const
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
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();
|
|
|
|
|
|
2023-03-07 06:52:19 +01:00
|
|
|
QString key = QLatin1String("WaveUnderline-") % pen.color().name()
|
2023-03-03 17:18:56 +01:00
|
|
|
% QString::number(*(size_t *) &radiusBase, 16)
|
|
|
|
|
% QString::number(*(size_t *) &pWidth);
|
|
|
|
|
|
|
|
|
|
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);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
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);
|
|
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
QPixmapCache::insert(key, pixmap);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
return pixmap;
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
// 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)
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
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);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
bool TerminalWidget::paintFindMatches(QPainter &p,
|
|
|
|
|
QList<SearchHit>::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;
|
|
|
|
|
}
|
2023-03-24 22:02:23 +01:00
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
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
|
2023-03-06 18:37:45 +01:00
|
|
|
{
|
|
|
|
|
bool isInSelection = false;
|
2023-03-24 12:53:21 +01:00
|
|
|
const int pos = m_surface->gridToPos(gridPos);
|
2023-03-06 18:37:45 +01:00
|
|
|
|
2023-03-24 22:02:23 +01:00
|
|
|
if (m_selection)
|
2023-03-06 18:37:45 +01:00
|
|
|
isInSelection = pos >= m_selection->start && pos < m_selection->end;
|
|
|
|
|
|
2023-03-24 22:02:23 +01:00
|
|
|
if (isInSelection)
|
2023-03-24 12:53:21 +01:00
|
|
|
p.fillRect(cellRect, m_currentColors[ColorIndex::Selection]);
|
|
|
|
|
|
|
|
|
|
return isInSelection;
|
2023-03-06 18:37:45 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
int TerminalWidget::paintCell(QPainter &p,
|
|
|
|
|
const QRectF &cellRect,
|
|
|
|
|
QPoint gridPos,
|
|
|
|
|
const Internal::TerminalCell &cell,
|
2023-03-24 12:53:21 +01:00
|
|
|
QFont &f,
|
|
|
|
|
QList<SearchHit>::const_iterator &searchIt) const
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-24 12:53:21 +01:00
|
|
|
bool paintBackground = !paintSelection(p, cellRect, gridPos)
|
|
|
|
|
&& !paintFindMatches(p, searchIt, cellRect, gridPos);
|
|
|
|
|
|
|
|
|
|
bool isDefaultBg = std::holds_alternative<int>(cell.backgroundColor)
|
|
|
|
|
&& std::get<int>(cell.backgroundColor) == 17;
|
|
|
|
|
|
|
|
|
|
if (paintBackground && !isDefaultBg)
|
|
|
|
|
p.fillRect(cellRect, toQColor(cell.backgroundColor));
|
2023-03-03 17:18:56 +01:00
|
|
|
|
2023-03-22 15:39:55 +01:00
|
|
|
p.setPen(toQColor(cell.foregroundColor));
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
f.setBold(cell.bold);
|
|
|
|
|
f.setItalic(cell.italic);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
if (!cell.text.isEmpty()) {
|
|
|
|
|
const auto r = Internal::GlyphCache::instance().get(f, cell.text);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
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);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
QPointF finalPos = cellRect.topLeft() + brOffset;
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
return cell.width;
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
void TerminalWidget::paintCursor(QPainter &p) const
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-07 17:55:38 +01:00
|
|
|
if (!m_process || !m_process->isRunning())
|
|
|
|
|
return;
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
auto cursor = m_surface->cursor();
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-05 23:55:37 +01:00
|
|
|
const bool blinkState = !cursor.blink || m_cursorBlinkState
|
|
|
|
|
|| !TerminalSettings::instance().allowBlinkingCursor.value();
|
|
|
|
|
|
|
|
|
|
if (cursor.visible && blinkState) {
|
2023-03-03 17:18:56 +01:00
|
|
|
const int cursorCellWidth = m_surface->cellWidthAt(cursor.position.x(), cursor.position.y());
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
QRectF cursorRect = QRectF(gridToGlobal(cursor.position),
|
|
|
|
|
gridToGlobal({cursor.position.x() + cursorCellWidth,
|
|
|
|
|
cursor.position.y()},
|
|
|
|
|
true));
|
2023-03-05 23:55:37 +01:00
|
|
|
|
|
|
|
|
cursorRect.adjust(0, 0, 0, -1);
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
if (hasFocus()) {
|
|
|
|
|
QPainter::CompositionMode oldMode = p.compositionMode();
|
|
|
|
|
p.setCompositionMode(QPainter::RasterOp_NotDestination);
|
2023-03-05 23:55:37 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2023-03-03 17:18:56 +01:00
|
|
|
p.setCompositionMode(oldMode);
|
|
|
|
|
} else {
|
|
|
|
|
p.drawRect(cursorRect);
|
|
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
2023-03-03 17:18:56 +01:00
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
p.fillRect(rect, QColor::fromRgb(0, 0, 0));
|
|
|
|
|
p.setPen(Qt::white);
|
|
|
|
|
p.drawText(rect, m_preEditString);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
void TerminalWidget::paintCells(QPainter &p, QPaintEvent *event) const
|
|
|
|
|
{
|
|
|
|
|
QFont f = m_font;
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
const int scrollOffset = verticalScrollBar()->value();
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
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);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
QList<SearchHit>::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;
|
|
|
|
|
});
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
for (int cellY = startRow; cellY < endRow; ++cellY) {
|
|
|
|
|
for (int cellX = 0; cellX < m_surface->liveSize().width();) {
|
|
|
|
|
const auto cell = m_surface->fetchCell(cellX, cellY);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
QRectF cellRect(gridToGlobal({cellX, cellY}),
|
|
|
|
|
QSizeF{m_cellSize.width() * cell.width, m_cellSize.height()});
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
int numCells = paintCell(p, cellRect, {cellX, cellY}, cell, f, searchIt);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
cellX += numCells;
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
2023-03-03 17:18:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
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());
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
p.setPen(QPen(Qt::green, 1, Qt::DashLine));
|
|
|
|
|
p.drawLine(s.x(), 0, s.x(), height());
|
|
|
|
|
p.drawLine(0, s.y(), width(), s.y());
|
2023-03-03 10:37:38 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
p.setPen(QPen(Qt::red, 1, Qt::DashLine));
|
2023-03-03 10:37:38 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
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();
|
2023-03-03 10:37:38 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
if (paintLog().isDebugEnabled())
|
|
|
|
|
p.fillRect(event->rect(), QColor::fromRgb(rand() % 60, rand() % 60, rand() % 60));
|
|
|
|
|
else
|
2023-03-24 12:53:21 +01:00
|
|
|
p.fillRect(event->rect(), m_currentColors[ColorIndex::Background]);
|
2023-03-03 17:18:56 +01:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
2023-03-22 15:39:55 +01:00
|
|
|
p.fillRect(QRectF{{0, 0}, QSizeF{(qreal) width(), topMargin()}},
|
2023-03-24 12:53:21 +01:00
|
|
|
m_currentColors[ColorIndex::Background]);
|
2023-03-03 17:18:56 +01:00
|
|
|
|
|
|
|
|
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()));
|
2023-03-03 10:37:38 +01:00
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::keyPressEvent(QKeyEvent *event)
|
|
|
|
|
{
|
2023-03-05 23:55:37 +01:00
|
|
|
// Don't blink during typing
|
|
|
|
|
if (m_cursorBlinkTimer.isActive()) {
|
|
|
|
|
m_cursorBlinkTimer.start();
|
|
|
|
|
m_cursorBlinkState = true;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
if (event->key() == Qt::Key_Escape) {
|
2023-03-24 22:02:23 +01:00
|
|
|
bool sendToTerminal = TerminalSettings::instance().sendEscapeToTerminal.value();
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
if (m_selection)
|
|
|
|
|
TerminalCommands::widgetActions().clearSelection.trigger();
|
|
|
|
|
else {
|
|
|
|
|
QTC_ASSERT(Core::ActionManager::command(Core::Constants::S_RETURNTOEDITOR), return);
|
|
|
|
|
Core::ActionManager::command(Core::Constants::S_RETURNTOEDITOR)->action()->trigger();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto oldSelection = m_selection;
|
2023-03-23 11:04:54 +01:00
|
|
|
if (TerminalCommands::triggerAction(event)) {
|
2023-03-24 12:53:21 +01:00
|
|
|
if (oldSelection && oldSelection == m_selection)
|
|
|
|
|
setSelection(std::nullopt);
|
2023-02-23 12:47:39 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-02 15:21:50 +01:00
|
|
|
event->accept();
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
m_surface->sendKey(event);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::applySizeChange()
|
|
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
QSize newLiveSize = {
|
2023-02-25 11:53:26 +01:00
|
|
|
qFloor((qreal) (viewport()->size().width()) / (qreal) m_cellSize.width()),
|
2023-03-03 17:18:56 +01:00
|
|
|
qFloor((qreal) (viewport()->size().height()) / m_cellSize.height()),
|
2023-02-23 12:47:39 +01:00
|
|
|
};
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
if (newLiveSize.height() <= 0)
|
|
|
|
|
newLiveSize.setHeight(1);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
if (newLiveSize.width() <= 0)
|
|
|
|
|
newLiveSize.setWidth(1);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-14 12:10:59 +01:00
|
|
|
if (m_process && m_process->ptyData())
|
|
|
|
|
m_process->ptyData()->resize(newLiveSize);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
m_surface->resize(newLiveSize);
|
2023-03-01 10:48:05 +01:00
|
|
|
flushVTerm(true);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::updateScrollBars()
|
|
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
int scrollSize = m_surface->fullSize().height() - m_surface->liveSize().height();
|
|
|
|
|
verticalScrollBar()->setRange(0, scrollSize);
|
2023-02-23 12:47:39 +01:00
|
|
|
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
|
2023-03-03 17:18:56 +01:00
|
|
|
updateViewport();
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
2023-03-02 15:21:50 +01:00
|
|
|
setSelection(std::nullopt);
|
2023-02-23 12:47:39 +01:00
|
|
|
m_ignoreScroll = false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
QRect TerminalWidget::gridToViewport(QRect rect) const
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
int offset = verticalScrollBar()->value();
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
int startRow = rect.y() - offset;
|
|
|
|
|
int numRows = rect.height();
|
|
|
|
|
int numCols = rect.width();
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
QRect r{qFloor(rect.x() * m_cellSize.width()),
|
|
|
|
|
qFloor(startRow * m_cellSize.height()),
|
|
|
|
|
qCeil(numCols * m_cellSize.width()),
|
|
|
|
|
qCeil(numRows * m_cellSize.height())};
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
r.translate(0, topMargin());
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
return r;
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
void TerminalWidget::updateViewport()
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
viewport()->update();
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-24 12:53:21 +01:00
|
|
|
void TerminalWidget::updateViewportRect(const QRect &rect)
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
viewport()->update(rect);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::wheelEvent(QWheelEvent *event)
|
|
|
|
|
{
|
2023-02-27 13:15:15 +01:00
|
|
|
verticalScrollBar()->event(event);
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::focusInEvent(QFocusEvent *)
|
|
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
updateViewport();
|
2023-03-05 23:55:37 +01:00
|
|
|
configBlinkTimer();
|
2023-03-23 11:04:54 +01:00
|
|
|
updateCopyState();
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
void TerminalWidget::focusOutEvent(QFocusEvent *)
|
|
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
updateViewport();
|
2023-03-05 23:55:37 +01:00
|
|
|
configBlinkTimer();
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::inputMethodEvent(QInputMethodEvent *event)
|
|
|
|
|
{
|
|
|
|
|
m_preEditString = event->preeditString();
|
|
|
|
|
|
|
|
|
|
if (event->commitString().isEmpty()) {
|
2023-03-03 17:18:56 +01:00
|
|
|
updateViewport();
|
2023-02-23 12:47:39 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
m_surface->sendKey(event->commitString());
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::mousePressEvent(QMouseEvent *event)
|
|
|
|
|
{
|
2023-03-09 22:33:47 +01:00
|
|
|
m_scrollDirection = 0;
|
|
|
|
|
|
|
|
|
|
m_activeMouseSelect.start = viewportToGlobal(event->pos());
|
2023-03-03 17:18:56 +01:00
|
|
|
|
|
|
|
|
if (event->button() == Qt::LeftButton && event->modifiers() == Qt::ControlModifier) {
|
|
|
|
|
if (m_linkSelection) {
|
2023-03-06 12:25:26 +01:00
|
|
|
if (m_linkSelection->link.targetFilePath.scheme().toString().startsWith("http")) {
|
|
|
|
|
QDesktopServices::openUrl(m_linkSelection->link.targetFilePath.toUrl());
|
2023-03-03 17:18:56 +01:00
|
|
|
return;
|
|
|
|
|
}
|
2023-03-06 12:25:26 +01:00
|
|
|
|
2023-03-23 15:04:02 +01:00
|
|
|
if (m_linkSelection->link.targetFilePath.isDir())
|
|
|
|
|
Core::FileUtils::showInFileSystemView(m_linkSelection->link.targetFilePath);
|
|
|
|
|
else
|
|
|
|
|
Core::EditorManager::openEditorAt(m_linkSelection->link);
|
2023-03-03 17:18:56 +01:00
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-03-01 16:03:18 +01:00
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
if (event->button() == Qt::LeftButton) {
|
2023-03-03 08:11:07 +01:00
|
|
|
if (std::chrono::system_clock::now() - m_lastDoubleClick < 500ms) {
|
2023-03-01 16:03:18 +01:00
|
|
|
m_selectLineMode = true;
|
2023-03-17 11:01:49 +01:00
|
|
|
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);
|
2023-03-01 16:03:18 +01:00
|
|
|
} else {
|
|
|
|
|
m_selectLineMode = false;
|
2023-03-03 17:18:56 +01:00
|
|
|
int pos = m_surface->gridToPos(globalToGrid(viewportToGlobal(event->pos())));
|
2023-03-17 11:01:49 +01:00
|
|
|
setSelection(Selection{pos, pos, false});
|
2023-03-01 16:03:18 +01:00
|
|
|
}
|
2023-03-03 10:37:38 +01:00
|
|
|
event->accept();
|
2023-03-03 17:18:56 +01:00
|
|
|
updateViewport();
|
2023-03-02 15:21:50 +01:00
|
|
|
} else if (event->button() == Qt::RightButton) {
|
2023-03-23 12:28:44 +01:00
|
|
|
if (event->modifiers() == Qt::ShiftModifier) {
|
|
|
|
|
QMenu *contextMenu = new QMenu(this);
|
|
|
|
|
contextMenu->addAction(&TerminalCommands::widgetActions().copy);
|
|
|
|
|
contextMenu->addAction(&TerminalCommands::widgetActions().paste);
|
|
|
|
|
contextMenu->addSeparator();
|
|
|
|
|
contextMenu->addAction(&TerminalCommands::widgetActions().clearTerminal);
|
|
|
|
|
contextMenu->addSeparator();
|
|
|
|
|
contextMenu->addAction(TerminalCommands::openSettingsAction());
|
|
|
|
|
|
|
|
|
|
contextMenu->popup(event->globalPos());
|
|
|
|
|
} else if (m_selection) {
|
2023-03-23 11:04:54 +01:00
|
|
|
copyToClipboard();
|
2023-03-02 15:21:50 +01:00
|
|
|
setSelection(std::nullopt);
|
|
|
|
|
} else {
|
2023-03-23 11:04:54 +01:00
|
|
|
pasteFromClipboard();
|
2023-03-02 15:21:50 +01:00
|
|
|
}
|
2023-03-23 13:31:21 +01:00
|
|
|
} 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());
|
|
|
|
|
}
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
void TerminalWidget::mouseMoveEvent(QMouseEvent *event)
|
|
|
|
|
{
|
|
|
|
|
if (m_selection && event->buttons() & Qt::LeftButton) {
|
2023-03-17 11:01:49 +01:00
|
|
|
Selection newSelection = *m_selection;
|
2023-03-09 22:33:47 +01:00
|
|
|
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();
|
|
|
|
|
}
|
2023-03-02 12:07:53 +01:00
|
|
|
|
2023-03-09 22:33:47 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int start = m_surface->gridToPos(globalToGrid(m_activeMouseSelect.start));
|
2023-03-03 17:18:56 +01:00
|
|
|
int newEnd = m_surface->gridToPos(globalToGrid(viewportToGlobal(event->pos())));
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
if (start > newEnd) {
|
2023-02-23 12:47:39 +01:00
|
|
|
std::swap(start, newEnd);
|
2023-03-03 10:37:38 +01:00
|
|
|
}
|
2023-03-09 18:52:02 +01:00
|
|
|
if (start < 0)
|
|
|
|
|
start = 0;
|
2023-03-03 10:37:38 +01:00
|
|
|
|
2023-03-09 21:38:01 +01:00
|
|
|
if (m_selectLineMode) {
|
2023-03-17 11:01:49 +01:00
|
|
|
newSelection.start = m_surface->gridToPos({0, m_surface->posToGrid(start).y()});
|
|
|
|
|
newSelection.end = m_surface->gridToPos(
|
2023-03-09 21:38:01 +01:00
|
|
|
{m_surface->liveSize().width(), m_surface->posToGrid(newEnd).y()});
|
|
|
|
|
} else {
|
2023-03-17 11:01:49 +01:00
|
|
|
newSelection.start = start;
|
|
|
|
|
newSelection.end = newEnd;
|
2023-03-09 21:38:01 +01:00
|
|
|
}
|
2023-03-03 17:18:56 +01:00
|
|
|
|
2023-03-17 11:01:49 +01:00
|
|
|
setSelection(newSelection);
|
2023-03-03 17:18:56 +01:00
|
|
|
} else if (event->modifiers() == Qt::ControlModifier) {
|
|
|
|
|
checkLinkAt(event->pos());
|
|
|
|
|
} else if (m_linkSelection) {
|
|
|
|
|
m_linkSelection.reset();
|
|
|
|
|
updateViewport();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_linkSelection) {
|
|
|
|
|
setCursor(Qt::PointingHandCursor);
|
|
|
|
|
} else {
|
|
|
|
|
setCursor(Qt::IBeamCursor);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::checkLinkAt(const QPoint &pos)
|
|
|
|
|
{
|
|
|
|
|
const TextAndOffsets hit = textAt(pos);
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
if (hit.text.size() > 0) {
|
2023-03-24 22:42:10 +01:00
|
|
|
QString t = QString::fromUcs4(hit.text.c_str(), hit.text.size()).trimmed();
|
|
|
|
|
t = chopIfEndsWith(t, ':');
|
|
|
|
|
|
|
|
|
|
if (t.isEmpty())
|
|
|
|
|
return;
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
if (t.startsWith("~/")) {
|
|
|
|
|
t = QDir::homePath() + t.mid(1);
|
2023-03-01 16:03:18 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-23 15:04:28 +01:00
|
|
|
Link link = Link::fromString(t, true);
|
|
|
|
|
|
|
|
|
|
if (!link.targetFilePath.isAbsolutePath())
|
|
|
|
|
link.targetFilePath = m_cwd.pathAppended(link.targetFilePath.path());
|
2023-02-23 12:47:39 +01:00
|
|
|
|
2023-03-06 12:25:26 +01:00
|
|
|
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) {
|
2023-03-03 17:18:56 +01:00
|
|
|
m_linkSelection = newSelection;
|
|
|
|
|
updateViewport();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-03 10:37:38 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
if (m_linkSelection) {
|
|
|
|
|
m_linkSelection.reset();
|
|
|
|
|
updateViewport();
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::mouseReleaseEvent(QMouseEvent *event)
|
|
|
|
|
{
|
2023-03-09 22:33:47 +01:00
|
|
|
m_scrollTimer.stop();
|
|
|
|
|
|
2023-03-03 10:37:38 +01:00
|
|
|
if (m_selection && event->button() == Qt::LeftButton) {
|
2023-03-17 11:01:49 +01:00
|
|
|
if (m_selection->end - m_selection->start == 0)
|
2023-03-02 15:21:50 +01:00
|
|
|
setSelection(std::nullopt);
|
2023-03-17 11:01:49 +01:00
|
|
|
else
|
|
|
|
|
setSelection(Selection{m_selection->start, m_selection->end, true});
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
TerminalWidget::TextAndOffsets TerminalWidget::textAt(const QPoint &pos) const
|
2023-02-23 12:47:39 +01:00
|
|
|
{
|
2023-03-03 17:18:56 +01:00
|
|
|
auto it = m_surface->iteratorAt(globalToGrid(viewportToGlobal(pos)));
|
|
|
|
|
auto itRev = m_surface->rIteratorAt(globalToGrid(viewportToGlobal(pos)));
|
2023-03-01 16:03:18 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
std::u32string whiteSpaces = U" \t\x00a0";
|
2023-03-01 16:03:18 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
const bool inverted = whiteSpaces.find(*it) != std::u32string::npos || *it == 0;
|
2023-03-01 16:03:18 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
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;
|
|
|
|
|
};
|
2023-03-01 16:03:18 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
auto itRight = std::find_if(it, m_surface->end(), predicate);
|
|
|
|
|
auto itLeft = std::find_if(itRev, m_surface->rend(), predicate);
|
2023-03-01 16:03:18 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
std::u32string text;
|
|
|
|
|
std::copy(itLeft.base(), it, std::back_inserter(text));
|
|
|
|
|
std::copy(it, itRight, std::back_inserter(text));
|
2023-03-24 22:42:10 +01:00
|
|
|
std::transform(text.begin(), text.end(), text.begin(), [](const char32_t &ch) {
|
|
|
|
|
return ch == 0 ? U' ' : ch;
|
|
|
|
|
});
|
2023-03-01 16:03:18 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
return {(itLeft.base()).position(), itRight.position(), text};
|
|
|
|
|
}
|
2023-03-01 16:03:18 +01:00
|
|
|
|
2023-03-03 17:18:56 +01:00
|
|
|
void TerminalWidget::mouseDoubleClickEvent(QMouseEvent *event)
|
|
|
|
|
{
|
|
|
|
|
const auto hit = textAt(event->pos());
|
2023-03-01 16:03:18 +01:00
|
|
|
|
2023-03-17 11:01:49 +01:00
|
|
|
setSelection(Selection{hit.start, hit.end, true});
|
2023-03-01 16:03:18 +01:00
|
|
|
|
2023-03-03 08:11:07 +01:00
|
|
|
m_lastDoubleClick = std::chrono::system_clock::now();
|
2023-03-01 16:03:18 +01:00
|
|
|
|
|
|
|
|
event->accept();
|
2023-02-23 12:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalWidget::showEvent(QShowEvent *event)
|
|
|
|
|
{
|
|
|
|
|
Q_UNUSED(event);
|
|
|
|
|
|
2023-03-01 08:15:58 +01:00
|
|
|
if (!m_process)
|
2023-02-23 12:47:39 +01:00
|
|
|
setupPty();
|
|
|
|
|
|
|
|
|
|
QAbstractScrollArea::showEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TerminalWidget::event(QEvent *event)
|
|
|
|
|
{
|
|
|
|
|
if (event->type() == QEvent::ShortcutOverride) {
|
|
|
|
|
if (hasFocus()) {
|
|
|
|
|
event->accept();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event->type() == QEvent::KeyPress) {
|
|
|
|
|
QKeyEvent *k = (QKeyEvent *) event;
|
|
|
|
|
keyPressEvent(k);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (event->type() == QEvent::KeyRelease) {
|
|
|
|
|
QKeyEvent *k = (QKeyEvent *) event;
|
|
|
|
|
keyReleaseEvent(k);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-25 11:53:26 +01:00
|
|
|
if (event->type() == QEvent::Paint) {
|
|
|
|
|
QPainter p(this);
|
2023-03-24 12:53:21 +01:00
|
|
|
p.fillRect(QRect(QPoint(0, 0), size()), m_currentColors[ColorIndex::Background]);
|
2023-02-25 11:53:26 +01:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-23 12:47:39 +01:00
|
|
|
return QAbstractScrollArea::event(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace Terminal
|