Internal
Public Access
QWidget's default handling intercepts Tab/Shift+Tab for focus-chain navigation before they ever reach keyPressEvent(), so pressing Tab in an RDP session moved focus to the OrbitHub UI (e.g. the Show Events button) instead of reaching the remote machine. KodoTerm (SSH) already overrides focusNextPrevChild() to opt out of this; RdpDisplayWidget and TerminalView did not. Both already handle Key_Tab in their own keyPressEvent, so this is enough to let it reach them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
537 lines
16 KiB
C++
537 lines
16 KiB
C++
#include "terminal_view.h"
|
|
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QColor>
|
|
#include <QFocusEvent>
|
|
#include <QFontMetrics>
|
|
#include <QKeyEvent>
|
|
#include <QResizeEvent>
|
|
#include <QTimer>
|
|
#include <QTextCursor>
|
|
|
|
#include <algorithm>
|
|
|
|
namespace {
|
|
QString normalizedThemeName(const QString& value)
|
|
{
|
|
return value.trimmed().toLower();
|
|
}
|
|
}
|
|
|
|
TerminalView::TerminalView(QWidget* parent)
|
|
: QTextEdit(parent),
|
|
m_bold(false),
|
|
m_hasFgColor(false),
|
|
m_hasBgColor(false)
|
|
{
|
|
setReadOnly(false);
|
|
setUndoRedoEnabled(false);
|
|
setAcceptRichText(false);
|
|
setLineWrapMode(QTextEdit::NoWrap);
|
|
setContextMenuPolicy(Qt::NoContextMenu);
|
|
setCursorWidth(2);
|
|
document()->setMaximumBlockCount(4000);
|
|
|
|
applyThemePalette(paletteByName(QStringLiteral("Dark")));
|
|
resetSgrState();
|
|
|
|
QTimer::singleShot(0, this, [this]() {
|
|
moveCursor(QTextCursor::End);
|
|
emitTerminalSize();
|
|
});
|
|
}
|
|
|
|
QStringList TerminalView::themeNames()
|
|
{
|
|
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
|
|
}
|
|
|
|
void TerminalView::setThemeName(const QString& themeName)
|
|
{
|
|
applyThemePalette(paletteByName(themeName));
|
|
}
|
|
|
|
void TerminalView::setFontPointSize(int pointSize)
|
|
{
|
|
QFont updatedFont = font();
|
|
updatedFont.setPointSize(pointSize);
|
|
setFont(updatedFont);
|
|
emitTerminalSize();
|
|
}
|
|
|
|
void TerminalView::appendTerminalData(const QString& data)
|
|
{
|
|
if (data.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
const QString merged = m_pendingEscape + data;
|
|
m_pendingEscape.clear();
|
|
|
|
QString plainBuffer;
|
|
|
|
for (int i = 0; i < merged.size();) {
|
|
const QChar ch = merged.at(i);
|
|
|
|
if (ch == QChar::fromLatin1('\x1b')) {
|
|
if (!plainBuffer.isEmpty()) {
|
|
appendTextChunk(plainBuffer);
|
|
plainBuffer.clear();
|
|
}
|
|
|
|
if (i + 1 >= merged.size()) {
|
|
m_pendingEscape = merged.mid(i);
|
|
break;
|
|
}
|
|
|
|
if (merged.at(i + 1) != QChar::fromLatin1('[')) {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
int end = i + 2;
|
|
while (end < merged.size()) {
|
|
const ushort c = merged.at(end).unicode();
|
|
if (c >= 0x40 && c <= 0x7e) {
|
|
break;
|
|
}
|
|
++end;
|
|
}
|
|
|
|
if (end >= merged.size()) {
|
|
m_pendingEscape = merged.mid(i);
|
|
break;
|
|
}
|
|
|
|
const QChar finalByte = merged.at(end);
|
|
const QString params = merged.mid(i + 2, end - (i + 2));
|
|
|
|
if (finalByte == QChar::fromLatin1('m')) {
|
|
handleSgrSequence(params);
|
|
} else if (finalByte == QChar::fromLatin1('J')) {
|
|
if (params.isEmpty() || params == QStringLiteral("2")) {
|
|
clear();
|
|
}
|
|
}
|
|
|
|
i = end + 1;
|
|
continue;
|
|
}
|
|
|
|
if (ch == QChar::fromLatin1('\r')) {
|
|
const bool hasLfAfter = (i + 1 < merged.size() && merged.at(i + 1) == QChar::fromLatin1('\n'));
|
|
if (!hasLfAfter) {
|
|
plainBuffer.append(QChar::fromLatin1('\n'));
|
|
}
|
|
++i;
|
|
continue;
|
|
}
|
|
|
|
plainBuffer.append(ch);
|
|
++i;
|
|
}
|
|
|
|
if (!plainBuffer.isEmpty()) {
|
|
appendTextChunk(plainBuffer);
|
|
}
|
|
}
|
|
|
|
void TerminalView::keyPressEvent(QKeyEvent* event)
|
|
{
|
|
if (event == nullptr) {
|
|
return;
|
|
}
|
|
|
|
moveCursor(QTextCursor::End);
|
|
|
|
const Qt::KeyboardModifiers modifiers = event->modifiers();
|
|
|
|
if (modifiers == (Qt::ControlModifier | Qt::ShiftModifier)
|
|
&& event->key() == Qt::Key_C) {
|
|
const QString selected = textCursor().selectedText();
|
|
if (!selected.isEmpty()) {
|
|
QApplication::clipboard()->setText(selected);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (modifiers == Qt::ControlModifier) {
|
|
switch (event->key()) {
|
|
case Qt::Key_C:
|
|
emit inputGenerated(QStringLiteral("\x03"));
|
|
return;
|
|
case Qt::Key_D:
|
|
emit inputGenerated(QStringLiteral("\x04"));
|
|
return;
|
|
case Qt::Key_L:
|
|
emit inputGenerated(QStringLiteral("\x0c"));
|
|
return;
|
|
case Qt::Key_V: {
|
|
const QString clipboardText = QApplication::clipboard()->text();
|
|
if (!clipboardText.isEmpty()) {
|
|
emit inputGenerated(clipboardText);
|
|
}
|
|
return;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (event->key()) {
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
emit inputGenerated(QStringLiteral("\n"));
|
|
return;
|
|
case Qt::Key_Backspace:
|
|
emit inputGenerated(QStringLiteral("\x7f"));
|
|
return;
|
|
case Qt::Key_Tab:
|
|
emit inputGenerated(QStringLiteral("\t"));
|
|
return;
|
|
case Qt::Key_Left:
|
|
emit inputGenerated(QStringLiteral("\x1b[D"));
|
|
return;
|
|
case Qt::Key_Right:
|
|
emit inputGenerated(QStringLiteral("\x1b[C"));
|
|
return;
|
|
case Qt::Key_Up:
|
|
emit inputGenerated(QStringLiteral("\x1b[A"));
|
|
return;
|
|
case Qt::Key_Down:
|
|
emit inputGenerated(QStringLiteral("\x1b[B"));
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
const QString text = event->text();
|
|
if (!text.isEmpty()) {
|
|
emit inputGenerated(text);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void TerminalView::focusInEvent(QFocusEvent* event)
|
|
{
|
|
QTextEdit::focusInEvent(event);
|
|
moveCursor(QTextCursor::End);
|
|
}
|
|
|
|
bool TerminalView::focusNextPrevChild(bool next)
|
|
{
|
|
Q_UNUSED(next);
|
|
// Tab/Shift+Tab must reach keyPressEvent() and be forwarded to the
|
|
// remote session instead of moving focus to the next local widget.
|
|
return false;
|
|
}
|
|
|
|
void TerminalView::resizeEvent(QResizeEvent* event)
|
|
{
|
|
QTextEdit::resizeEvent(event);
|
|
emitTerminalSize();
|
|
}
|
|
|
|
TerminalView::ThemePalette TerminalView::paletteByName(const QString& themeName)
|
|
{
|
|
const QString theme = normalizedThemeName(themeName);
|
|
|
|
if (theme == QStringLiteral("light")) {
|
|
return ThemePalette{QStringLiteral("Light"),
|
|
QColor(QStringLiteral("#ececec")),
|
|
QColor(QStringLiteral("#000000")),
|
|
{QColor(QStringLiteral("#000000")),
|
|
QColor(QStringLiteral("#aa0000")),
|
|
QColor(QStringLiteral("#008000")),
|
|
QColor(QStringLiteral("#7a5f00")),
|
|
QColor(QStringLiteral("#0033cc")),
|
|
QColor(QStringLiteral("#8a00a8")),
|
|
QColor(QStringLiteral("#005f87")),
|
|
QColor(QStringLiteral("#333333"))},
|
|
{QColor(QStringLiteral("#5c5c5c")),
|
|
QColor(QStringLiteral("#d30000")),
|
|
QColor(QStringLiteral("#00a000")),
|
|
QColor(QStringLiteral("#9a7700")),
|
|
QColor(QStringLiteral("#0055ff")),
|
|
QColor(QStringLiteral("#b300db")),
|
|
QColor(QStringLiteral("#007ea7")),
|
|
QColor(QStringLiteral("#111111"))}};
|
|
}
|
|
|
|
if (theme == QStringLiteral("solarized dark")) {
|
|
return ThemePalette{QStringLiteral("Solarized Dark"),
|
|
QColor(QStringLiteral("#002b36")),
|
|
QColor(QStringLiteral("#839496")),
|
|
{QColor(QStringLiteral("#073642")),
|
|
QColor(QStringLiteral("#dc322f")),
|
|
QColor(QStringLiteral("#859900")),
|
|
QColor(QStringLiteral("#b58900")),
|
|
QColor(QStringLiteral("#268bd2")),
|
|
QColor(QStringLiteral("#d33682")),
|
|
QColor(QStringLiteral("#2aa198")),
|
|
QColor(QStringLiteral("#eee8d5"))},
|
|
{QColor(QStringLiteral("#586e75")),
|
|
QColor(QStringLiteral("#cb4b16")),
|
|
QColor(QStringLiteral("#586e75")),
|
|
QColor(QStringLiteral("#657b83")),
|
|
QColor(QStringLiteral("#839496")),
|
|
QColor(QStringLiteral("#6c71c4")),
|
|
QColor(QStringLiteral("#93a1a1")),
|
|
QColor(QStringLiteral("#fdf6e3"))}};
|
|
}
|
|
|
|
return ThemePalette{QStringLiteral("Dark"),
|
|
QColor(QStringLiteral("#1e1e1e")),
|
|
QColor(QStringLiteral("#d4d4d4")),
|
|
{QColor(QStringLiteral("#000000")),
|
|
QColor(QStringLiteral("#cd3131")),
|
|
QColor(QStringLiteral("#0dbc79")),
|
|
QColor(QStringLiteral("#e5e510")),
|
|
QColor(QStringLiteral("#2472c8")),
|
|
QColor(QStringLiteral("#bc3fbc")),
|
|
QColor(QStringLiteral("#11a8cd")),
|
|
QColor(QStringLiteral("#e5e5e5"))},
|
|
{QColor(QStringLiteral("#666666")),
|
|
QColor(QStringLiteral("#f14c4c")),
|
|
QColor(QStringLiteral("#23d18b")),
|
|
QColor(QStringLiteral("#f5f543")),
|
|
QColor(QStringLiteral("#3b8eea")),
|
|
QColor(QStringLiteral("#d670d6")),
|
|
QColor(QStringLiteral("#29b8db")),
|
|
QColor(QStringLiteral("#ffffff"))}};
|
|
}
|
|
|
|
QColor TerminalView::colorFrom256Index(int index)
|
|
{
|
|
if (index < 0) {
|
|
index = 0;
|
|
}
|
|
if (index > 255) {
|
|
index = 255;
|
|
}
|
|
|
|
if (index < 16) {
|
|
static const std::array<QColor, 16> base = {
|
|
QColor(QStringLiteral("#000000")), QColor(QStringLiteral("#800000")),
|
|
QColor(QStringLiteral("#008000")), QColor(QStringLiteral("#808000")),
|
|
QColor(QStringLiteral("#000080")), QColor(QStringLiteral("#800080")),
|
|
QColor(QStringLiteral("#008080")), QColor(QStringLiteral("#c0c0c0")),
|
|
QColor(QStringLiteral("#808080")), QColor(QStringLiteral("#ff0000")),
|
|
QColor(QStringLiteral("#00ff00")), QColor(QStringLiteral("#ffff00")),
|
|
QColor(QStringLiteral("#0000ff")), QColor(QStringLiteral("#ff00ff")),
|
|
QColor(QStringLiteral("#00ffff")), QColor(QStringLiteral("#ffffff"))};
|
|
return base.at(static_cast<size_t>(index));
|
|
}
|
|
|
|
if (index >= 16 && index <= 231) {
|
|
const int c = index - 16;
|
|
const int r = c / 36;
|
|
const int g = (c / 6) % 6;
|
|
const int b = c % 6;
|
|
|
|
const auto channel = [](int v) { return v == 0 ? 0 : 55 + v * 40; };
|
|
return QColor(channel(r), channel(g), channel(b));
|
|
}
|
|
|
|
const int gray = 8 + (index - 232) * 10;
|
|
return QColor(gray, gray, gray);
|
|
}
|
|
|
|
void TerminalView::applyThemePalette(const ThemePalette& palette)
|
|
{
|
|
m_palette = palette;
|
|
|
|
const QString stylesheet = QStringLiteral("QTextEdit { background: %1; color: %2; }")
|
|
.arg(m_palette.background.name(), m_palette.foreground.name());
|
|
setStyleSheet(stylesheet);
|
|
|
|
if (!m_hasFgColor) {
|
|
m_fgColor = m_palette.foreground;
|
|
}
|
|
if (!m_hasBgColor) {
|
|
m_bgColor = m_palette.background;
|
|
}
|
|
applyCurrentFormat();
|
|
}
|
|
|
|
void TerminalView::applyCurrentFormat()
|
|
{
|
|
m_currentFormat = QTextCharFormat();
|
|
m_currentFormat.setForeground(m_hasFgColor ? m_fgColor : m_palette.foreground);
|
|
if (m_hasBgColor) {
|
|
m_currentFormat.setBackground(m_bgColor);
|
|
}
|
|
QFont font = currentFont();
|
|
font.setBold(m_bold);
|
|
m_currentFormat.setFont(font);
|
|
}
|
|
|
|
void TerminalView::resetSgrState()
|
|
{
|
|
m_bold = false;
|
|
m_hasFgColor = false;
|
|
m_hasBgColor = false;
|
|
m_fgColor = m_palette.foreground;
|
|
m_bgColor = m_palette.background;
|
|
applyCurrentFormat();
|
|
}
|
|
|
|
void TerminalView::handleSgrSequence(const QString& params)
|
|
{
|
|
QStringList parts = params.split(QChar::fromLatin1(';'), Qt::KeepEmptyParts);
|
|
if (parts.isEmpty()) {
|
|
parts.push_back(QStringLiteral("0"));
|
|
}
|
|
|
|
for (int i = 0; i < parts.size(); ++i) {
|
|
const QString part = parts.at(i).trimmed();
|
|
bool ok = false;
|
|
const int code = part.isEmpty() ? 0 : part.toInt(&ok);
|
|
if (!ok && !part.isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
if (code == 0) {
|
|
resetSgrState();
|
|
continue;
|
|
}
|
|
if (code == 1) {
|
|
m_bold = true;
|
|
continue;
|
|
}
|
|
if (code == 22) {
|
|
m_bold = false;
|
|
continue;
|
|
}
|
|
if (code == 39) {
|
|
m_hasFgColor = false;
|
|
continue;
|
|
}
|
|
if (code == 49) {
|
|
m_hasBgColor = false;
|
|
continue;
|
|
}
|
|
|
|
if (code >= 30 && code <= 37) {
|
|
m_fgColor = paletteColor(false, code - 30, false);
|
|
m_hasFgColor = true;
|
|
continue;
|
|
}
|
|
if (code >= 90 && code <= 97) {
|
|
m_fgColor = paletteColor(false, code - 90, true);
|
|
m_hasFgColor = true;
|
|
continue;
|
|
}
|
|
if (code >= 40 && code <= 47) {
|
|
m_bgColor = paletteColor(true, code - 40, false);
|
|
m_hasBgColor = true;
|
|
continue;
|
|
}
|
|
if (code >= 100 && code <= 107) {
|
|
m_bgColor = paletteColor(true, code - 100, true);
|
|
m_hasBgColor = true;
|
|
continue;
|
|
}
|
|
|
|
if (code == 38 || code == 48) {
|
|
const bool background = (code == 48);
|
|
if (i + 1 >= parts.size()) {
|
|
continue;
|
|
}
|
|
|
|
const int mode = parts.at(i + 1).toInt(&ok);
|
|
if (!ok) {
|
|
continue;
|
|
}
|
|
|
|
if (mode == 5 && i + 2 < parts.size()) {
|
|
const int index = parts.at(i + 2).toInt(&ok);
|
|
if (ok) {
|
|
const QColor color = colorFrom256Index(index);
|
|
if (background) {
|
|
m_bgColor = color;
|
|
m_hasBgColor = true;
|
|
} else {
|
|
m_fgColor = color;
|
|
m_hasFgColor = true;
|
|
}
|
|
}
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
if (mode == 2 && i + 4 < parts.size()) {
|
|
const int r = parts.at(i + 2).toInt(&ok);
|
|
if (!ok) {
|
|
i += 4;
|
|
continue;
|
|
}
|
|
const int g = parts.at(i + 3).toInt(&ok);
|
|
if (!ok) {
|
|
i += 4;
|
|
continue;
|
|
}
|
|
const int b = parts.at(i + 4).toInt(&ok);
|
|
if (!ok) {
|
|
i += 4;
|
|
continue;
|
|
}
|
|
|
|
const QColor color(r, g, b);
|
|
if (background) {
|
|
m_bgColor = color;
|
|
m_hasBgColor = true;
|
|
} else {
|
|
m_fgColor = color;
|
|
m_hasFgColor = true;
|
|
}
|
|
|
|
i += 4;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
applyCurrentFormat();
|
|
}
|
|
|
|
void TerminalView::appendTextChunk(const QString& text)
|
|
{
|
|
if (text.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QTextCursor cursor = textCursor();
|
|
cursor.movePosition(QTextCursor::End);
|
|
cursor.insertText(text, m_currentFormat);
|
|
setTextCursor(cursor);
|
|
ensureCursorVisible();
|
|
}
|
|
|
|
QColor TerminalView::paletteColor(bool, int index, bool bright) const
|
|
{
|
|
const int safeIndex = std::clamp(index, 0, 7);
|
|
return bright ? m_palette.bright.at(static_cast<size_t>(safeIndex))
|
|
: m_palette.normal.at(static_cast<size_t>(safeIndex));
|
|
}
|
|
|
|
int TerminalView::terminalColumns() const
|
|
{
|
|
const QFontMetrics metrics(font());
|
|
const int cellWidth = std::max(1, metrics.horizontalAdvance(QChar::fromLatin1('M')));
|
|
return std::max(1, viewport()->width() / cellWidth);
|
|
}
|
|
|
|
int TerminalView::terminalRows() const
|
|
{
|
|
const QFontMetrics metrics(font());
|
|
const int cellHeight = std::max(1, metrics.lineSpacing());
|
|
return std::max(1, viewport()->height() / cellHeight);
|
|
}
|
|
|
|
void TerminalView::emitTerminalSize()
|
|
{
|
|
emit terminalSizeChanged(terminalColumns(), terminalRows());
|
|
}
|