mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
WinDbg plugin: load dbgeng.dll dynamically from Debugging Tools directory instead of static linking (system dbgeng.dll lacks remote DebugConnect). Copy tools dbghelp.dll next to exe so it loads before System32 version. Add COM init on DbgEng thread, browse for tools dir, styled dialog. Editor: derive darker background via theme.background.darker(115) for visual depth between chrome and editor surfaces. UI: global scrollbar styling, workspace accent bar 1px, pane tab font from editor settings, workspace dock default width 128px.
3632 lines
154 KiB
C++
3632 lines
154 KiB
C++
#include "editor.h"
|
|
#include "disasm.h"
|
|
#include "providerregistry.h"
|
|
#include <QDebug>
|
|
#include <Qsci/qsciscintilla.h>
|
|
#include <Qsci/qsciscintillabase.h>
|
|
#include <Qsci/qscilexercpp.h>
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QFont>
|
|
#include <QColor>
|
|
#include <QKeyEvent>
|
|
#include <QMouseEvent>
|
|
#include <QFocusEvent>
|
|
#include <QTimer>
|
|
#include <QCursor>
|
|
#include <QMenu>
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QLabel>
|
|
#include <QToolButton>
|
|
#include <QLineEdit>
|
|
#include <QScreen>
|
|
#include <QScrollBar>
|
|
#include <QDateTime>
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include "themes/thememanager.h"
|
|
|
|
namespace rcx {
|
|
|
|
// Forward declaration (defined below, after RcxEditor constructor)
|
|
static QString getLineText(QsciScintilla* sci, int line);
|
|
|
|
// ── Value history popup (styled like TypeSelectorPopup) ──
|
|
|
|
class ValueHistoryPopup : public QFrame {
|
|
uint64_t m_nodeId = 0;
|
|
bool m_hasButtons = false;
|
|
QStringList m_values;
|
|
QVector<QLabel*> m_labels;
|
|
std::function<void(const QString&)> m_onSet;
|
|
std::function<void(QMouseEvent*)> m_onMouseMove;
|
|
public:
|
|
explicit ValueHistoryPopup(QWidget* parent)
|
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
|
{
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
|
setMouseTracking(true);
|
|
setFrameShape(QFrame::NoFrame);
|
|
setAutoFillBackground(true);
|
|
}
|
|
|
|
uint64_t nodeId() const { return m_nodeId; }
|
|
bool hasButtons() const { return m_hasButtons; }
|
|
void setOnSet(std::function<void(const QString&)> fn) { m_onSet = std::move(fn); }
|
|
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
|
|
protected:
|
|
void mouseMoveEvent(QMouseEvent* e) override {
|
|
if (!m_hasButtons && m_onMouseMove)
|
|
m_onMouseMove(e);
|
|
else
|
|
QFrame::mouseMoveEvent(e);
|
|
}
|
|
public:
|
|
|
|
void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font,
|
|
bool showButtons = false) {
|
|
QStringList vals;
|
|
hist.forEach([&](const QString& v) { vals.append(v); });
|
|
|
|
if (nodeId == m_nodeId && vals == m_values
|
|
&& showButtons == m_hasButtons && isVisible())
|
|
return;
|
|
|
|
// In-place label update when structure unchanged (avoids flicker)
|
|
if (nodeId == m_nodeId && vals.size() == m_values.size()
|
|
&& vals.size() == m_labels.size()
|
|
&& showButtons == m_hasButtons && isVisible()) {
|
|
for (int i = 0; i < vals.size(); i++)
|
|
m_labels[i]->setText(vals[i]);
|
|
m_values = vals;
|
|
return;
|
|
}
|
|
|
|
m_nodeId = nodeId;
|
|
m_values = vals;
|
|
m_hasButtons = showButtons;
|
|
m_labels.clear();
|
|
|
|
delete layout();
|
|
qDeleteAll(findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly));
|
|
|
|
const auto& theme = ThemeManager::instance().current();
|
|
QPalette pal;
|
|
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
|
pal.setColor(QPalette::WindowText, theme.text);
|
|
setPalette(pal);
|
|
|
|
auto* vbox = new QVBoxLayout(this);
|
|
vbox->setContentsMargins(8, 6, 8, 6);
|
|
vbox->setSpacing(2);
|
|
|
|
auto* title = new QLabel(QStringLiteral("Previous Values"));
|
|
QFont bold = font;
|
|
bold.setBold(true);
|
|
title->setFont(bold);
|
|
title->setStyleSheet(QStringLiteral("color: %1;").arg(theme.text.name()));
|
|
vbox->addWidget(title);
|
|
|
|
auto* sep = new QFrame;
|
|
sep->setFrameShape(QFrame::HLine);
|
|
sep->setFrameShadow(QFrame::Plain);
|
|
sep->setFixedHeight(1);
|
|
QPalette sp; sp.setColor(QPalette::WindowText, theme.border);
|
|
sep->setPalette(sp);
|
|
vbox->addWidget(sep);
|
|
|
|
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
|
hist.forEachWithTime([&](const QString& v, qint64 msec) {
|
|
auto* row = new QHBoxLayout;
|
|
row->setContentsMargins(0, 1, 0, 1);
|
|
row->setSpacing(8);
|
|
|
|
auto* label = new QLabel(v);
|
|
label->setFont(font);
|
|
label->setStyleSheet(QStringLiteral("color: %1;").arg(theme.syntaxNumber.name()));
|
|
row->addWidget(label, 1);
|
|
m_labels.append(label);
|
|
|
|
if (msec > 0) {
|
|
qint64 elapsed = now - msec;
|
|
QString timeStr;
|
|
if (elapsed < 1000)
|
|
timeStr = QStringLiteral("now");
|
|
else if (elapsed < 60000)
|
|
timeStr = QStringLiteral("%1s ago").arg(elapsed / 1000);
|
|
else if (elapsed < 3600000)
|
|
timeStr = QStringLiteral("%1m ago").arg(elapsed / 60000);
|
|
else
|
|
timeStr = QStringLiteral("%1h ago").arg(elapsed / 3600000);
|
|
|
|
auto* timeLabel = new QLabel(timeStr);
|
|
timeLabel->setFont(font);
|
|
timeLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name()));
|
|
row->addWidget(timeLabel);
|
|
}
|
|
|
|
if (showButtons) {
|
|
auto* setBtn = new QToolButton;
|
|
setBtn->setText(QStringLiteral("Set"));
|
|
setBtn->setAutoRaise(true);
|
|
setBtn->setCursor(Qt::PointingHandCursor);
|
|
setBtn->setFont(font);
|
|
setBtn->setStyleSheet(QStringLiteral(
|
|
"QToolButton { color: %1; border: none; padding: 1px 4px; }"
|
|
"QToolButton:hover { color: %2; background: %3; }")
|
|
.arg(theme.textDim.name(), theme.text.name(), theme.hover.name()));
|
|
QString val = v;
|
|
QObject::connect(setBtn, &QToolButton::clicked, [this, val]() {
|
|
if (m_onSet) m_onSet(val);
|
|
});
|
|
row->addWidget(setBtn);
|
|
}
|
|
vbox->addLayout(row);
|
|
});
|
|
|
|
adjustSize();
|
|
}
|
|
|
|
void showAt(const QPoint& globalPos, int lineHeight = 0) {
|
|
QSize sz = sizeHint();
|
|
QRect screen = QApplication::screenAt(globalPos)
|
|
? QApplication::screenAt(globalPos)->availableGeometry()
|
|
: QRect(0, 0, 1920, 1080);
|
|
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
|
int y = globalPos.y();
|
|
if (y + sz.height() > screen.bottom())
|
|
y = globalPos.y() - sz.height() - lineHeight - 4;
|
|
move(x, y);
|
|
if (!isVisible()) show();
|
|
}
|
|
|
|
void dismiss() {
|
|
if (isVisible()) hide();
|
|
m_nodeId = 0;
|
|
m_values.clear();
|
|
m_labels.clear();
|
|
}
|
|
};
|
|
|
|
// ── Disassembly / hex-dump hover popup ──
|
|
|
|
class DisasmPopup : public QFrame {
|
|
uint64_t m_nodeId = 0;
|
|
QString m_body;
|
|
QLabel* m_titleLabel = nullptr;
|
|
QLabel* m_bodyLabel = nullptr;
|
|
std::function<void(QMouseEvent*)> m_onMouseMove;
|
|
public:
|
|
explicit DisasmPopup(QWidget* parent)
|
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
|
{
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
|
setMouseTracking(true);
|
|
setFrameShape(QFrame::NoFrame);
|
|
setAutoFillBackground(true);
|
|
|
|
auto* vbox = new QVBoxLayout(this);
|
|
vbox->setContentsMargins(8, 6, 8, 6);
|
|
vbox->setSpacing(2);
|
|
|
|
m_titleLabel = new QLabel;
|
|
QFont bold = m_titleLabel->font();
|
|
bold.setBold(true);
|
|
m_titleLabel->setFont(bold);
|
|
vbox->addWidget(m_titleLabel);
|
|
|
|
auto* sep = new QFrame;
|
|
sep->setFrameShape(QFrame::HLine);
|
|
sep->setFrameShadow(QFrame::Plain);
|
|
sep->setFixedHeight(1);
|
|
vbox->addWidget(sep);
|
|
|
|
m_bodyLabel = new QLabel;
|
|
m_bodyLabel->setTextFormat(Qt::PlainText);
|
|
m_bodyLabel->setWordWrap(false);
|
|
vbox->addWidget(m_bodyLabel);
|
|
}
|
|
|
|
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
|
|
uint64_t nodeId() const { return m_nodeId; }
|
|
protected:
|
|
void mouseMoveEvent(QMouseEvent* e) override {
|
|
if (m_onMouseMove) m_onMouseMove(e);
|
|
else QFrame::mouseMoveEvent(e);
|
|
}
|
|
public:
|
|
void populate(uint64_t nodeId, const QString& title, const QString& body,
|
|
const QFont& font) {
|
|
if (nodeId == m_nodeId && body == m_body && isVisible())
|
|
return;
|
|
|
|
m_nodeId = nodeId;
|
|
m_body = body;
|
|
|
|
const auto& theme = ThemeManager::instance().current();
|
|
QPalette pal;
|
|
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
|
pal.setColor(QPalette::WindowText, theme.text);
|
|
setPalette(pal);
|
|
|
|
QFont bold = font;
|
|
bold.setBold(true);
|
|
m_titleLabel->setFont(bold);
|
|
m_titleLabel->setText(title);
|
|
m_titleLabel->setStyleSheet(
|
|
QStringLiteral("color: %1;").arg(theme.text.name()));
|
|
|
|
// Find and style the separator
|
|
for (auto* child : findChildren<QFrame*>()) {
|
|
if (child->frameShape() == QFrame::HLine) {
|
|
QPalette sp;
|
|
sp.setColor(QPalette::WindowText, theme.border);
|
|
child->setPalette(sp);
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_bodyLabel->setFont(font);
|
|
m_bodyLabel->setText(body);
|
|
m_bodyLabel->setStyleSheet(
|
|
QStringLiteral("color: %1;").arg(theme.syntaxNumber.name()));
|
|
|
|
setMaximumWidth(600);
|
|
adjustSize();
|
|
}
|
|
|
|
void showAt(const QPoint& globalPos, int lineHeight = 0) {
|
|
QSize sz = sizeHint();
|
|
QRect screen = QApplication::screenAt(globalPos)
|
|
? QApplication::screenAt(globalPos)->availableGeometry()
|
|
: QRect(0, 0, 1920, 1080);
|
|
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
|
int y = globalPos.y();
|
|
if (y + sz.height() > screen.bottom())
|
|
y = globalPos.y() - sz.height() - lineHeight - 4;
|
|
move(x, y);
|
|
if (!isVisible()) show();
|
|
}
|
|
|
|
void dismiss() {
|
|
if (isVisible()) hide();
|
|
m_nodeId = 0;
|
|
m_body.clear();
|
|
}
|
|
};
|
|
|
|
class StructPreviewPopup : public QFrame {
|
|
uint64_t m_nodeId = 0;
|
|
QString m_body;
|
|
QLabel* m_titleLabel = nullptr;
|
|
QLabel* m_bodyLabel = nullptr;
|
|
std::function<void(QMouseEvent*)> m_onMouseMove;
|
|
public:
|
|
explicit StructPreviewPopup(QWidget* parent)
|
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
|
{
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
|
setMouseTracking(true);
|
|
setFrameShape(QFrame::NoFrame);
|
|
setAutoFillBackground(true);
|
|
|
|
auto* vbox = new QVBoxLayout(this);
|
|
vbox->setContentsMargins(8, 6, 8, 6);
|
|
vbox->setSpacing(2);
|
|
|
|
m_titleLabel = new QLabel;
|
|
QFont bold = m_titleLabel->font();
|
|
bold.setBold(true);
|
|
m_titleLabel->setFont(bold);
|
|
vbox->addWidget(m_titleLabel);
|
|
|
|
auto* sep = new QFrame;
|
|
sep->setFrameShape(QFrame::HLine);
|
|
sep->setFrameShadow(QFrame::Plain);
|
|
sep->setFixedHeight(1);
|
|
vbox->addWidget(sep);
|
|
|
|
m_bodyLabel = new QLabel;
|
|
m_bodyLabel->setTextFormat(Qt::PlainText);
|
|
m_bodyLabel->setWordWrap(false);
|
|
vbox->addWidget(m_bodyLabel);
|
|
}
|
|
|
|
uint64_t nodeId() const { return m_nodeId; }
|
|
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
|
|
protected:
|
|
void mouseMoveEvent(QMouseEvent* e) override {
|
|
if (m_onMouseMove) m_onMouseMove(e);
|
|
else QFrame::mouseMoveEvent(e);
|
|
}
|
|
public:
|
|
void populate(uint64_t nodeId, const QString& title, const QString& body,
|
|
const QFont& font) {
|
|
if (nodeId == m_nodeId && body == m_body && isVisible())
|
|
return;
|
|
|
|
m_nodeId = nodeId;
|
|
m_body = body;
|
|
|
|
const auto& theme = ThemeManager::instance().current();
|
|
QPalette pal;
|
|
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
|
pal.setColor(QPalette::WindowText, theme.text);
|
|
setPalette(pal);
|
|
|
|
QFont bold = font;
|
|
bold.setBold(true);
|
|
m_titleLabel->setFont(bold);
|
|
m_titleLabel->setText(title);
|
|
m_titleLabel->setStyleSheet(
|
|
QStringLiteral("color: %1;").arg(theme.text.name()));
|
|
|
|
for (auto* child : findChildren<QFrame*>()) {
|
|
if (child->frameShape() == QFrame::HLine) {
|
|
QPalette sp;
|
|
sp.setColor(QPalette::WindowText, theme.border);
|
|
child->setPalette(sp);
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_bodyLabel->setFont(font);
|
|
m_bodyLabel->setText(body);
|
|
m_bodyLabel->setStyleSheet(
|
|
QStringLiteral("color: %1;").arg(theme.text.name()));
|
|
|
|
setMaximumWidth(600);
|
|
adjustSize();
|
|
}
|
|
|
|
void showAt(const QPoint& globalPos, int lineHeight = 0) {
|
|
QSize sz = sizeHint();
|
|
QRect screen = QApplication::screenAt(globalPos)
|
|
? QApplication::screenAt(globalPos)->availableGeometry()
|
|
: QRect(0, 0, 1920, 1080);
|
|
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
|
int y = globalPos.y();
|
|
if (y + sz.height() > screen.bottom())
|
|
y = globalPos.y() - sz.height() - lineHeight - 4;
|
|
move(x, y);
|
|
if (!isVisible()) show();
|
|
}
|
|
|
|
void dismiss() {
|
|
if (isVisible()) hide();
|
|
m_nodeId = 0;
|
|
m_body.clear();
|
|
}
|
|
};
|
|
|
|
static constexpr int IND_EDITABLE = 8;
|
|
static constexpr int IND_HEX_DIM = 9;
|
|
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
|
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
|
static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans
|
|
static constexpr int IND_HEAT_COLD = 13; // Heatmap level 1 (changed once)
|
|
static constexpr int IND_CLASS_NAME = 14; // Teal text for root class name
|
|
static constexpr int IND_HINT_GREEN = 15; // Green text for hint/comment text
|
|
static constexpr int IND_LOCAL_OFF = 16; // Dim text for inline local offset in relative mode
|
|
static constexpr int IND_HEAT_WARM = 17; // Heatmap level 2 (moderate changes)
|
|
static constexpr int IND_HEAT_HOT = 18; // Heatmap level 3 (frequent changes)
|
|
static constexpr int IND_FIND = 19; // Search match highlight
|
|
|
|
static QString g_fontName = "JetBrains Mono";
|
|
|
|
static QFont editorFont() {
|
|
QFont f(g_fontName, 12);
|
|
f.setFixedPitch(true);
|
|
return f;
|
|
}
|
|
|
|
RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
|
auto* layout = new QVBoxLayout(this);
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
m_sci = new QsciScintilla(this);
|
|
layout->addWidget(m_sci);
|
|
|
|
// Find bar (hidden by default, shown with Ctrl+F)
|
|
m_findBarContainer = new QWidget(this);
|
|
auto* fbLayout = new QHBoxLayout(m_findBarContainer);
|
|
fbLayout->setContentsMargins(4, 1, 4, 1);
|
|
fbLayout->setSpacing(2);
|
|
auto* findPrevBtn = new QToolButton(m_findBarContainer);
|
|
findPrevBtn->setText(QStringLiteral("\u25C0"));
|
|
findPrevBtn->setFixedSize(24, 24);
|
|
auto* findNextBtn = new QToolButton(m_findBarContainer);
|
|
findNextBtn->setText(QStringLiteral("\u25B6"));
|
|
findNextBtn->setFixedSize(24, 24);
|
|
auto* findCloseBtn = new QToolButton(m_findBarContainer);
|
|
findCloseBtn->setText(QStringLiteral("\u2715"));
|
|
findCloseBtn->setFixedSize(24, 24);
|
|
m_findBar = new QLineEdit(m_findBarContainer);
|
|
m_findBar->setPlaceholderText(QStringLiteral("Find..."));
|
|
m_findBar->setFixedHeight(24);
|
|
fbLayout->addWidget(findPrevBtn);
|
|
fbLayout->addWidget(findNextBtn);
|
|
fbLayout->addWidget(findCloseBtn);
|
|
fbLayout->addWidget(m_findBar);
|
|
m_findBarContainer->setVisible(false);
|
|
layout->addWidget(m_findBarContainer);
|
|
|
|
setupScintilla();
|
|
setupLexer();
|
|
setupMargins();
|
|
setupFolding();
|
|
setupMarkers();
|
|
allocateMarginStyles();
|
|
|
|
applyTheme(ThemeManager::instance().current());
|
|
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
|
|
this, &RcxEditor::applyTheme);
|
|
|
|
m_sci->installEventFilter(this);
|
|
m_sci->viewport()->installEventFilter(this);
|
|
m_sci->viewport()->setMouseTracking(true);
|
|
|
|
// Find bar: indicator-based search (selection is disabled in our Scintilla)
|
|
auto doFind = [this](bool forward) {
|
|
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (long)0, docLen);
|
|
|
|
QString text = m_findBar->text();
|
|
if (text.isEmpty()) return;
|
|
QByteArray needle = text.toUtf8();
|
|
|
|
long startPos = forward ? m_findPos : (m_findPos > 0 ? m_findPos - 1 : docLen);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, startPos);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND,
|
|
forward ? docLen : (long)0);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEARCHFLAGS, (long)0);
|
|
|
|
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_SEARCHINTARGET,
|
|
(uintptr_t)needle.size(), needle.constData());
|
|
if (pos == -1) { // wrap
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART,
|
|
forward ? (long)0 : docLen);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND,
|
|
forward ? startPos : (long)0);
|
|
pos = m_sci->SendScintilla(QsciScintillaBase::SCI_SEARCHINTARGET,
|
|
(uintptr_t)needle.size(), needle.constData());
|
|
}
|
|
if (pos >= 0) {
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, (long)needle.size());
|
|
int line = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_LINEFROMPOSITION, pos);
|
|
m_sci->ensureLineVisible(line);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, pos);
|
|
m_findPos = pos + (forward ? needle.size() : 0);
|
|
}
|
|
};
|
|
connect(m_findBar, &QLineEdit::textChanged, this, [doFind]() { doFind(true); });
|
|
connect(m_findBar, &QLineEdit::returnPressed, this, [doFind]() { doFind(true); });
|
|
connect(findNextBtn, &QToolButton::clicked, this, [doFind]() { doFind(true); });
|
|
connect(findPrevBtn, &QToolButton::clicked, this, [doFind]() { doFind(false); });
|
|
connect(findCloseBtn, &QToolButton::clicked, this, [this]() { hideFindBar(); });
|
|
// Escape hides find bar
|
|
{
|
|
auto* escAction = new QAction(m_findBar);
|
|
escAction->setShortcut(QKeySequence(Qt::Key_Escape));
|
|
escAction->setShortcutContext(Qt::WidgetShortcut);
|
|
m_findBar->addAction(escAction);
|
|
connect(escAction, &QAction::triggered, this, [this]() { hideFindBar(); });
|
|
}
|
|
|
|
// Recalculate hover when the viewport scrolls (scrollbar drag, wheel
|
|
// deceleration, etc.) so the highlight tracks whatever is under the cursor.
|
|
connect(m_sci->verticalScrollBar(), &QScrollBar::valueChanged,
|
|
this, [this]() {
|
|
if (m_editState.active || !m_hoverInside) return;
|
|
m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos());
|
|
m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos);
|
|
auto h = hitTest(m_lastHoverPos);
|
|
uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0;
|
|
int newHoverLine = (m_hoverInside && h.line >= 0) ? h.line : -1;
|
|
if (newHoverId != m_hoveredNodeId || newHoverLine != m_hoveredLine) {
|
|
m_hoveredNodeId = newHoverId;
|
|
m_hoveredLine = newHoverLine;
|
|
applyHoverHighlight();
|
|
}
|
|
applyHoverCursor();
|
|
});
|
|
|
|
// Hover cursor is applied synchronously in eventFilter (no timer).
|
|
|
|
connect(m_sci, &QsciScintilla::marginClicked,
|
|
this, [this](int margin, int line, Qt::KeyboardModifiers mods) {
|
|
emit marginClicked(margin, line, mods);
|
|
});
|
|
|
|
m_sci->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(m_sci, &QWidget::customContextMenuRequested,
|
|
this, [this](const QPoint& pos) {
|
|
// Right-click on offset margin → show margin mode menu
|
|
int margin0Width = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_GETMARGINWIDTHN, 0UL, 0L);
|
|
if (pos.x() < margin0Width) {
|
|
QMenu menu;
|
|
auto* actRel = menu.addAction("Relative Offsets (+0x)");
|
|
auto* actAbs = menu.addAction("Absolute Addresses");
|
|
actRel->setCheckable(true);
|
|
actAbs->setCheckable(true);
|
|
actRel->setChecked(m_relativeOffsets);
|
|
actAbs->setChecked(!m_relativeOffsets);
|
|
QAction* chosen = menu.exec(m_sci->mapToGlobal(pos));
|
|
if (chosen == actRel && !m_relativeOffsets) {
|
|
m_relativeOffsets = true;
|
|
reformatMargins();
|
|
} else if (chosen == actAbs && m_relativeOffsets) {
|
|
m_relativeOffsets = false;
|
|
reformatMargins();
|
|
}
|
|
return;
|
|
}
|
|
HitInfo hi = hitTest(pos);
|
|
int line = hi.line;
|
|
|
|
// Right-click on command row keyword → show conversion menu
|
|
if (line == 0 && hi.col >= 0 && !m_meta.isEmpty()
|
|
&& m_meta[0].lineKind == LineKind::CommandRow) {
|
|
QString lineText = getLineText(m_sci, 0);
|
|
ColumnSpan rts = commandRowRootTypeSpan(lineText);
|
|
if (rts.valid && hi.col >= rts.start && hi.col < rts.end) {
|
|
// Extract current keyword from span text
|
|
QString kw = lineText.mid(rts.start, rts.end - rts.start).trimmed();
|
|
QMenu menu;
|
|
if (kw == QStringLiteral("class"))
|
|
menu.addAction("Convert to Struct");
|
|
else if (kw == QStringLiteral("struct"))
|
|
menu.addAction("Convert to Class");
|
|
// enum: no conversion options
|
|
if (!menu.isEmpty()) {
|
|
QAction* chosen = menu.exec(m_sci->mapToGlobal(pos));
|
|
if (chosen) {
|
|
QString newKw = chosen->text().contains("Class")
|
|
? QStringLiteral("class") : QStringLiteral("struct");
|
|
emit keywordConvertRequested(newKw);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
int nodeIdx = -1;
|
|
int subLine = 0;
|
|
if (line >= 0 && line < m_meta.size()) {
|
|
nodeIdx = m_meta[line].nodeIdx;
|
|
subLine = m_meta[line].subLine;
|
|
}
|
|
emit contextMenuRequested(line, nodeIdx, subLine, m_sci->mapToGlobal(pos));
|
|
});
|
|
|
|
connect(m_sci, &QsciScintilla::userListActivated,
|
|
this, [this](int id, const QString& text) {
|
|
if (!m_editState.active) return;
|
|
if (id == 1 && (m_editState.target == EditTarget::Type
|
|
|| m_editState.target == EditTarget::ArrayElementType
|
|
|| m_editState.target == EditTarget::PointerTarget)) {
|
|
const LineMeta* lm = metaForLine(m_editState.line);
|
|
uint64_t addr = lm ? lm->offsetAddr : 0;
|
|
auto info = endInlineEdit();
|
|
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
|
|
}
|
|
});
|
|
|
|
connect(m_sci, &QsciScintilla::cursorPositionChanged,
|
|
this, [this](int line, int /*col*/) { updateEditableIndicators(line); });
|
|
|
|
connect(m_sci, &QsciScintilla::textChanged, this, [this]() {
|
|
if (!m_editState.active) return;
|
|
if (m_updatingComment) return; // Skip queuing during comment update
|
|
if (m_editState.target == EditTarget::Value)
|
|
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
|
|
|
|
// Autocomplete for static field expressions — show field names as user types
|
|
if (m_editState.target == EditTarget::StaticExpr && !m_staticCompletions.isEmpty()) {
|
|
// Get word at cursor
|
|
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
|
|
long wordStart = m_sci->SendScintilla(QsciScintillaBase::SCI_WORDSTARTPOSITION, pos, (long)1);
|
|
int wordLen = (int)(pos - wordStart);
|
|
if (wordLen >= 1) {
|
|
QByteArray list = m_staticCompletions.join(' ').toUtf8();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' ');
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSHOW, (uintptr_t)wordLen, list.constData());
|
|
}
|
|
}
|
|
});
|
|
|
|
connect(m_sci, &QsciScintilla::selectionChanged,
|
|
this, &RcxEditor::clampEditSelection);
|
|
}
|
|
|
|
RcxEditor::~RcxEditor() {
|
|
}
|
|
|
|
void RcxEditor::setupScintilla() {
|
|
m_sci->setFont(editorFont());
|
|
|
|
m_sci->setReadOnly(true);
|
|
m_sci->setWrapMode(QsciScintilla::WrapNone);
|
|
m_sci->setCaretLineVisible(false);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 0);
|
|
|
|
// Arrow cursor by default — not the I-beam (this is a structured viewer, not a text editor)
|
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
|
|
|
m_sci->setTabWidth(2);
|
|
m_sci->setIndentationsUseTabs(false);
|
|
|
|
// Line spacing for readability
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2);
|
|
|
|
// Disable native selection rendering — we use markers for selection
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0);
|
|
|
|
// Horizontal scrollbar: sized explicitly in applyDocument() to match content
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTHTRACKING, 0);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH, 1);
|
|
|
|
// Vertical scrollbar: don't allow scrolling past the last line
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETENDATLASTLINE, 1);
|
|
|
|
// Editable-field indicator - HIDDEN (no visual)
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_EDITABLE, 5 /*INDIC_HIDDEN*/);
|
|
|
|
// Hex node dim indicator — overrides text color
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/);
|
|
|
|
// Base address indicator — text color override on command row
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_BASE_ADDR, 17 /*INDIC_TEXTFORE*/);
|
|
|
|
// Hover span indicator — link-like text
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_HOVER_SPAN, 17 /*INDIC_TEXTFORE*/);
|
|
|
|
// Command-row pill background
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_CMD_PILL, 8 /*INDIC_STRAIGHTBOX*/);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETALPHA,
|
|
IND_CMD_PILL, (long)100);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
|
IND_CMD_PILL, (long)1);
|
|
|
|
// Heatmap indicators (cold / warm / hot)
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_HEAT_COLD, 17 /*INDIC_TEXTFORE*/);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_HEAT_WARM, 17 /*INDIC_TEXTFORE*/);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_HEAT_HOT, 17 /*INDIC_TEXTFORE*/);
|
|
|
|
// Root class name — type color
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_CLASS_NAME, 17 /*INDIC_TEXTFORE*/);
|
|
|
|
// Green text for hint/comment annotations
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_HINT_GREEN, 17 /*INDIC_TEXTFORE*/);
|
|
|
|
// Local offset text color (dim, like margin text)
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_LOCAL_OFF, 17 /*INDIC_TEXTFORE*/);
|
|
|
|
// Find match highlight — thick underline (avoids box rendering artifacts)
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
IND_FIND, 14 /*INDIC_COMPOSITIONTHICK*/);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
|
IND_FIND, (long)1);
|
|
|
|
}
|
|
|
|
void RcxEditor::setupLexer() {
|
|
m_lexer = new QsciLexerCPP(m_sci);
|
|
QFont font = editorFont();
|
|
m_lexer->setFont(font);
|
|
for (int i = 0; i <= 127; i++)
|
|
m_lexer->setFont(font, i);
|
|
|
|
m_sci->setLexer(m_lexer);
|
|
m_sci->setBraceMatching(QsciScintilla::NoBraceMatch); // Disable - this is a structured viewer
|
|
|
|
// Add built-in type names to keyword set 1 → blue coloring
|
|
QByteArray kw2 = allTypeNamesForUI(/*stripBrackets=*/true).join(' ').toLatin1();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETKEYWORDS,
|
|
(uintptr_t)1, kw2.constData());
|
|
}
|
|
|
|
void RcxEditor::setCustomTypeNames(const QStringList& names) {
|
|
m_customTypeNames = names;
|
|
QByteArray kw = names.join(' ').toLatin1();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETKEYWORDS,
|
|
(uintptr_t)3, kw.constData());
|
|
}
|
|
|
|
void RcxEditor::setupMargins() {
|
|
m_sci->setMarginsFont(editorFont());
|
|
|
|
// Margin 0: Offset text
|
|
m_sci->setMarginType(0, QsciScintilla::TextMarginRightJustified);
|
|
m_sci->setMarginWidth(0, " 00000000 "); // default 8-digit; resized dynamically in applyDocument()
|
|
m_sci->setMarginSensitivity(0, true);
|
|
|
|
// Margin 1: 2px accent bar (selection indicator)
|
|
m_sci->setMarginType(1, QsciScintilla::SymbolMargin);
|
|
m_sci->setMarginWidth(1, 2);
|
|
m_sci->setMarginSensitivity(1, false);
|
|
m_sci->setMarginMarkerMask(1, 1 << M_ACCENT);
|
|
}
|
|
|
|
void RcxEditor::setupFolding() {
|
|
// Hide fold margin (fold indicators are text-based now)
|
|
m_sci->setMarginWidth(2, 0);
|
|
|
|
// Fold indicators are now text in the line content (kFoldCol prefix),
|
|
// so no Scintilla markers needed for fold state.
|
|
|
|
// Keep Scintilla fold markers invisible (fold levels still used for click detection)
|
|
for (int i = 25; i <= 31; i++)
|
|
m_sci->markerDefine(QsciScintilla::Invisible, i);
|
|
|
|
// Disable automatic fold toggle — we handle collapse at model level
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETAUTOMATICFOLD,
|
|
(unsigned long)0);
|
|
|
|
// Disable lexer-driven folding — we set fold levels manually
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETPROPERTY,
|
|
(const char*)"fold", (const char*)"0");
|
|
}
|
|
|
|
void RcxEditor::setupMarkers() {
|
|
// M_CONT (0): continuation line (metadata only, no visual)
|
|
m_sci->markerDefine(QsciScintilla::Invisible, M_CONT);
|
|
|
|
// M_PTR0 (2): right triangle
|
|
m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0);
|
|
|
|
// M_CYCLE (3): arrows
|
|
m_sci->markerDefine(QsciScintilla::ThreeRightArrows, M_CYCLE);
|
|
|
|
// M_ERR (4): background
|
|
m_sci->markerDefine(QsciScintilla::Background, M_ERR);
|
|
|
|
// M_STRUCT_BG (5): struct header/footer
|
|
m_sci->markerDefine(QsciScintilla::Background, M_STRUCT_BG);
|
|
|
|
// M_HOVER (6): full-row hover highlight
|
|
m_sci->markerDefine(QsciScintilla::Background, M_HOVER);
|
|
|
|
// M_SELECTED (7): full-row selection highlight
|
|
m_sci->markerDefine(QsciScintilla::Background, M_SELECTED);
|
|
|
|
// M_CMD_ROW (8): distinct background for CommandRow bar
|
|
m_sci->markerDefine(QsciScintilla::Background, M_CMD_ROW);
|
|
|
|
// M_ACCENT (9): 2px accent bar in margin 1 (selection indicator)
|
|
m_sci->markerDefine(QsciScintilla::FullRectangle, M_ACCENT);
|
|
}
|
|
|
|
void RcxEditor::allocateMarginStyles() {
|
|
static constexpr int MSTYLE_NORMAL = 0;
|
|
static constexpr int MSTYLE_CONT = 1;
|
|
|
|
long base = m_sci->SendScintilla(QsciScintillaBase::SCI_ALLOCATEEXTENDEDSTYLES, (long)2);
|
|
m_marginStyleBase = (int)base;
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLEOFFSET, base);
|
|
|
|
QByteArray fontName = editorFont().family().toUtf8();
|
|
int fontSize = editorFont().pointSize();
|
|
|
|
for (int s = MSTYLE_NORMAL; s <= MSTYLE_CONT; s++) {
|
|
unsigned long abs = (unsigned long)(base + s);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFONT,
|
|
(uintptr_t)abs, fontName.constData());
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETSIZE, abs, (long)fontSize);
|
|
}
|
|
}
|
|
|
|
void RcxEditor::applyTheme(const Theme& theme) {
|
|
// Editor uses a slightly darker background than chrome for visual depth
|
|
const QColor editorBg = theme.background.darker(115);
|
|
|
|
// Paper and text
|
|
m_sci->setPaper(editorBg);
|
|
m_sci->setColor(theme.text);
|
|
m_sci->setCaretForegroundColor(theme.text);
|
|
|
|
// Indicator colors
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_HEX_DIM, theme.textFaint);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_BASE_ADDR, theme.text);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_HOVER_SPAN, theme.indHoverSpan);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_CMD_PILL, theme.indCmdPill);
|
|
// Heatmap colors
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_HEAT_COLD, theme.indHeatCold);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_HEAT_WARM, theme.indHeatWarm);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_HEAT_HOT, theme.indHeatHot);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_CLASS_NAME, theme.syntaxType);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_HINT_GREEN, theme.indHintGreen);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_LOCAL_OFF, theme.textFaint);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
IND_FIND, theme.borderFocused);
|
|
|
|
// Lexer colors
|
|
m_lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword);
|
|
m_lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::KeywordSet2);
|
|
m_lexer->setColor(theme.syntaxNumber, QsciLexerCPP::Number);
|
|
m_lexer->setColor(theme.syntaxString, QsciLexerCPP::DoubleQuotedString);
|
|
m_lexer->setColor(theme.syntaxString, QsciLexerCPP::SingleQuotedString);
|
|
m_lexer->setColor(theme.syntaxComment, QsciLexerCPP::Comment);
|
|
m_lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentLine);
|
|
m_lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentDoc);
|
|
m_lexer->setColor(theme.text, QsciLexerCPP::Default);
|
|
m_lexer->setColor(theme.text, QsciLexerCPP::Identifier);
|
|
m_lexer->setColor(theme.syntaxPreproc, QsciLexerCPP::PreProcessor);
|
|
m_lexer->setColor(theme.text, QsciLexerCPP::Operator);
|
|
m_lexer->setColor(theme.syntaxType, QsciLexerCPP::GlobalClass);
|
|
for (int i = 0; i <= 127; i++)
|
|
m_lexer->setPaper(editorBg, i);
|
|
|
|
// Margins
|
|
m_sci->setMarginsBackgroundColor(editorBg);
|
|
m_sci->setMarginsForegroundColor(theme.textFaint);
|
|
m_sci->setFoldMarginColors(editorBg, editorBg);
|
|
|
|
// Markers
|
|
m_sci->setMarkerBackgroundColor(theme.markerPtr, M_PTR0);
|
|
m_sci->setMarkerForegroundColor(theme.markerPtr, M_PTR0);
|
|
m_sci->setMarkerBackgroundColor(editorBg, M_CYCLE);
|
|
m_sci->setMarkerForegroundColor(editorBg, M_CYCLE);
|
|
m_sci->setMarkerBackgroundColor(theme.markerError, M_ERR);
|
|
m_sci->setMarkerForegroundColor(theme.text, M_ERR);
|
|
m_sci->setMarkerBackgroundColor(editorBg, M_STRUCT_BG);
|
|
m_sci->setMarkerForegroundColor(theme.text, M_STRUCT_BG);
|
|
m_sci->setMarkerBackgroundColor(theme.hover, M_HOVER);
|
|
m_sci->setMarkerBackgroundColor(theme.selected, M_SELECTED);
|
|
m_sci->setMarkerBackgroundColor(editorBg, M_CMD_ROW);
|
|
m_sci->setMarkerBackgroundColor(theme.indHoverSpan, M_ACCENT);
|
|
|
|
// Margin extended styles
|
|
if (m_marginStyleBase >= 0) {
|
|
long base = m_marginStyleBase;
|
|
for (int s = 0; s <= 1; s++) {
|
|
unsigned long abs = (unsigned long)(base + s);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE,
|
|
abs, theme.textFaint);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK,
|
|
abs, editorBg);
|
|
}
|
|
}
|
|
|
|
// Find bar
|
|
if (m_findBarContainer) {
|
|
m_findBar->setStyleSheet(
|
|
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
|
" padding: 2px 6px; font-size: 13px; }")
|
|
.arg(theme.backgroundAlt.name(), theme.text.name(), theme.border.name()));
|
|
m_findBarContainer->setStyleSheet(
|
|
QStringLiteral("QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }"
|
|
"QToolButton:hover { background: %4; }"
|
|
"QToolButton:pressed { background: %5; }")
|
|
.arg(theme.background.name(), theme.text.name(), theme.border.name(),
|
|
theme.hover.name(), theme.backgroundAlt.name()));
|
|
}
|
|
}
|
|
|
|
void RcxEditor::applyDocument(const ComposeResult& result) {
|
|
// Silently deactivate inline edit (no signal — refresh is already happening)
|
|
if (m_editState.active)
|
|
endInlineEdit();
|
|
|
|
// Guard: suppress popup dismiss during setText() which fires synthetic Leave events
|
|
m_applyingDocument = true;
|
|
|
|
// Save hover state — setText() triggers viewport Leave events that would clear it
|
|
uint64_t savedHoverId = m_hoveredNodeId;
|
|
int savedHoverLine = m_hoveredLine;
|
|
bool savedHoverInside = m_hoverInside;
|
|
|
|
m_meta = result.meta;
|
|
m_layout = result.layout;
|
|
|
|
// Build nodeId → display-line index for O(1) hover/selection lookup
|
|
m_nodeLineIndex.clear();
|
|
m_nodeLineIndex.reserve(m_meta.size());
|
|
for (int i = 0; i < m_meta.size(); i++) {
|
|
if (m_meta[i].nodeId != 0)
|
|
m_nodeLineIndex[m_meta[i].nodeId].append(i);
|
|
}
|
|
|
|
// Dynamically resize margin to fit the current hex digit tier
|
|
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
|
|
m_sci->setMarginWidth(0, marginSizer);
|
|
|
|
m_sci->setReadOnly(false);
|
|
m_sci->setText(result.text);
|
|
m_sci->setReadOnly(true);
|
|
|
|
// Set horizontal scroll width to match the longest line (ignoring trailing spaces)
|
|
{
|
|
int maxLen = 0;
|
|
const QStringList lines = result.text.split(QChar('\n'));
|
|
for (const auto& line : lines) {
|
|
int len = (int)line.size();
|
|
while (len > 0 && line[len - 1] == QChar(' ')) --len;
|
|
maxLen = std::max(len, maxLen);
|
|
}
|
|
QFontMetrics fm(editorFont());
|
|
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH,
|
|
(unsigned long)qMax(1, pixelWidth));
|
|
|
|
// Reset horizontal scroll to 0. The controller's restoreViewState()
|
|
// will set it back to the (clamped) saved position afterward.
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET, (unsigned long)0);
|
|
}
|
|
|
|
// Force full re-lex to fix stale syntax coloring after edits
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_COLOURISE, (uintptr_t)0, (long)-1);
|
|
|
|
applyMarginText(result.meta);
|
|
applyMarkers(result.meta);
|
|
applyFoldLevels(result.meta);
|
|
applyHexDimming(result.meta);
|
|
applyHeatmapHighlight(result.meta);
|
|
applySymbolColoring(result.meta);
|
|
applyCommandRowPills();
|
|
|
|
// Reset hint line - applySelectionOverlay will repaint indicators
|
|
m_hintLine = -1;
|
|
|
|
// Restore hover state — but clear if the node was deleted
|
|
m_hoveredNodeId = savedHoverId;
|
|
m_hoveredLine = savedHoverLine;
|
|
m_hoverInside = savedHoverInside;
|
|
m_applyingDocument = false;
|
|
|
|
if (m_hoveredNodeId != 0 && !m_nodeLineIndex.contains(m_hoveredNodeId)) {
|
|
m_hoveredNodeId = 0;
|
|
m_hoveredLine = -1;
|
|
dismissHistoryPopup();
|
|
if (m_disasmPopup) m_disasmPopup->hide();
|
|
if (m_structPreviewPopup) m_structPreviewPopup->hide();
|
|
}
|
|
|
|
// Re-apply hover markers (setText() clears all Scintilla markers).
|
|
// Reset m_prevHoveredNodeId so the incremental logic re-adds markers.
|
|
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
|
|
// composed text that updateCommandRow() will overwrite. The correct call
|
|
// happens via applySelectionOverlays() after all text is finalized.
|
|
m_prevHoveredNodeId = 0;
|
|
m_prevHoveredLine = -1;
|
|
applyHoverHighlight();
|
|
|
|
// Re-apply find indicator (setText() clears all indicators)
|
|
if (m_findBarContainer && m_findBarContainer->isVisible()) {
|
|
QString needle = m_findBar->text();
|
|
if (!needle.isEmpty()) {
|
|
QByteArray nb = needle.toUtf8();
|
|
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEARCHFLAGS, (long)0);
|
|
long pos = 0;
|
|
while (pos < docLen) {
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, pos);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, docLen);
|
|
long found = m_sci->SendScintilla(QsciScintillaBase::SCI_SEARCHINTARGET,
|
|
(uintptr_t)nb.size(), nb.constData());
|
|
if (found < 0) break;
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, found, (long)nb.size());
|
|
pos = found + nb.size();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
|
|
if (m_relativeOffsets)
|
|
return reformatMargins();
|
|
|
|
m_sci->clearMarginText(-1);
|
|
|
|
for (int i = 0; i < meta.size(); i++) {
|
|
const auto& lm = meta[i];
|
|
if (lm.offsetText.isEmpty()) continue;
|
|
|
|
QByteArray text = lm.offsetText.toUtf8();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETTEXT,
|
|
(uintptr_t)i, text.constData());
|
|
QByteArray styles(text.size(), '\0'); // style 0 = dim
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLES,
|
|
(uintptr_t)i, styles.constData());
|
|
}
|
|
}
|
|
|
|
void RcxEditor::reformatMargins() {
|
|
uint64_t base = m_layout.baseAddress;
|
|
int hexDigits = m_layout.offsetHexDigits;
|
|
|
|
// ── Pass 1: margin text (global offset only) ──
|
|
m_sci->clearMarginText(-1);
|
|
for (int i = 0; i < m_meta.size(); i++) {
|
|
auto& lm = m_meta[i];
|
|
|
|
if (lm.isContinuation || lm.isMemberLine) {
|
|
lm.offsetText = QStringLiteral(" \u00B7 ");
|
|
} else if (lm.offsetText.isEmpty()) {
|
|
continue;
|
|
} else if (m_relativeOffsets) {
|
|
if (lm.lineKind == LineKind::Footer ||
|
|
lm.lineKind == LineKind::ArrayElementSeparator ||
|
|
lm.lineKind == LineKind::CommandRow) {
|
|
lm.offsetText = QString(hexDigits + 1, ' ');
|
|
} else {
|
|
uint64_t rvaBase = lm.ptrBase ? lm.ptrBase : base;
|
|
uint64_t rel = lm.offsetAddr >= rvaBase ? lm.offsetAddr - rvaBase : 0;
|
|
lm.offsetText = (QStringLiteral("+") +
|
|
QString::number(rel, 16).toUpper())
|
|
.rightJustified(hexDigits, ' ') + QChar(' ');
|
|
}
|
|
} else {
|
|
lm.offsetText = QString::number(lm.offsetAddr, 16).toUpper()
|
|
.rightJustified(hexDigits, '0') + QChar(' ');
|
|
}
|
|
|
|
QByteArray text = lm.offsetText.toUtf8();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETTEXT,
|
|
(uintptr_t)i, text.constData());
|
|
QByteArray styles(text.size(), '\0');
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLES,
|
|
(uintptr_t)i, styles.constData());
|
|
}
|
|
|
|
// ── Pass 2: inline local offsets in the text indent area ──
|
|
// Skip when tree lines are active — the compose step already placed
|
|
// Unicode tree connectors in the indent area; overwriting with spaces
|
|
// or offsets would destroy them.
|
|
if (m_layout.treeLines)
|
|
return;
|
|
m_sci->setReadOnly(false);
|
|
for (int i = 0; i < m_meta.size(); i++) {
|
|
const auto& lm = m_meta[i];
|
|
if (lm.depth <= 1 || lm.isContinuation) continue;
|
|
if (lm.lineKind != LineKind::Field && lm.lineKind != LineKind::Header)
|
|
continue;
|
|
|
|
// Place offset in the parent's indent slot (one level above the field's own indent)
|
|
// so the field's own 3-char indent acts as visual separator from the type column
|
|
int col = kFoldCol + (lm.depth - 2) * 3;
|
|
int slotWidth = 5;
|
|
|
|
auto pos = [&](int c) -> long {
|
|
return m_sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
|
(unsigned long)i, (long)c);
|
|
};
|
|
|
|
if (m_relativeOffsets) {
|
|
// Derive local offset: for pointer-expanded children use ptrBase,
|
|
// otherwise find enclosing header or array element separator
|
|
uint64_t parentAddr = base;
|
|
if (lm.ptrBase != 0) {
|
|
parentAddr = lm.ptrBase;
|
|
} else {
|
|
for (int j = i - 1; j >= 0; j--) {
|
|
const auto& pLm = m_meta[j];
|
|
if (pLm.lineKind == LineKind::Header && pLm.depth < lm.depth) {
|
|
parentAddr = pLm.offsetAddr;
|
|
break;
|
|
}
|
|
if (pLm.lineKind == LineKind::ArrayElementSeparator && pLm.depth <= lm.depth) {
|
|
parentAddr = pLm.offsetAddr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
uint64_t localOff = lm.offsetAddr >= parentAddr ? lm.offsetAddr - parentAddr : 0;
|
|
|
|
QString off = QStringLiteral("+") +
|
|
QString::number(localOff, 16).toUpper();
|
|
QString padded = off.size() <= slotWidth
|
|
? off.rightJustified(slotWidth, ' ')
|
|
: off;
|
|
long posA = pos(col);
|
|
long posB = pos(col + slotWidth);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, posB);
|
|
QByteArray utf8 = padded.left(slotWidth).toUtf8();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET,
|
|
(uintptr_t)utf8.size(), utf8.constData());
|
|
// Color the local offset dim
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_LOCAL_OFF);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE,
|
|
posA, posB - posA);
|
|
} else {
|
|
// Restore spaces when toggling off
|
|
long posA = pos(col);
|
|
long posB = pos(col + slotWidth);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, posB);
|
|
QByteArray spaces(slotWidth, ' ');
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET,
|
|
(uintptr_t)spaces.size(), spaces.constData());
|
|
}
|
|
}
|
|
m_sci->setReadOnly(true);
|
|
}
|
|
|
|
void RcxEditor::applyMarkers(const QVector<LineMeta>& meta) {
|
|
for (int m = M_CONT; m <= M_STRUCT_BG; m++) {
|
|
m_sci->markerDeleteAll(m);
|
|
}
|
|
m_sci->markerDeleteAll(M_CMD_ROW);
|
|
for (int i = 0; i < meta.size(); i++) {
|
|
if (meta[i].lineKind == LineKind::CommandRow) {
|
|
m_sci->markerAdd(i, M_CMD_ROW);
|
|
continue;
|
|
}
|
|
uint32_t mask = meta[i].markerMask;
|
|
for (int m = M_CONT; m <= M_STRUCT_BG; m++) {
|
|
if (mask & (1u << m)) {
|
|
m_sci->markerAdd(i, m);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RcxEditor::applyFoldLevels(const QVector<LineMeta>& meta) {
|
|
for (int i = 0; i < meta.size(); i++) {
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETFOLDLEVEL,
|
|
(unsigned long)i, (long)meta[i].foldLevel);
|
|
}
|
|
}
|
|
|
|
static inline void lineRangeNoEol(QsciScintilla* sci, int line, long& start, long& len) {
|
|
start = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line);
|
|
long end = sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, (unsigned long)line);
|
|
len = (end > start) ? (end - start) : 0;
|
|
}
|
|
|
|
// UTF-8 safe column-to-position conversion
|
|
static inline long posFromCol(QsciScintilla* sci, int line, int col) {
|
|
return sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
|
(unsigned long)line, (long)col);
|
|
}
|
|
|
|
void RcxEditor::clearIndicatorLine(int indic, int line) {
|
|
if (line < 0) return;
|
|
long start, len;
|
|
lineRangeNoEol(m_sci, line, start, len);
|
|
if (len <= 0) return;
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, indic);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, start, len);
|
|
}
|
|
|
|
void RcxEditor::fillIndicatorCols(int indic, int line, int colA, int colB) {
|
|
long a = posFromCol(m_sci, line, colA);
|
|
long b = posFromCol(m_sci, line, colB);
|
|
if (b > a) {
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, indic);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, a, b - a);
|
|
}
|
|
}
|
|
|
|
void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM);
|
|
for (int i = 0; i < meta.size(); i++) {
|
|
// Dim fold arrows (▸/▾) on fold head lines
|
|
if (meta[i].foldHead && meta[i].lineKind != LineKind::CommandRow)
|
|
fillIndicatorCols(IND_HEX_DIM, i, 0, kFoldCol);
|
|
|
|
if (isHexPreview(meta[i].nodeKind)) {
|
|
long pos, len; lineRangeNoEol(m_sci, i, pos, len);
|
|
if (len > 0)
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len);
|
|
}
|
|
// Dim struct/array braces: entire footer line, trailing "{" on headers
|
|
if (meta[i].lineKind == LineKind::Footer) {
|
|
long pos, len; lineRangeNoEol(m_sci, i, pos, len);
|
|
if (len > 0)
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len);
|
|
} else if (meta[i].lineKind == LineKind::Header ||
|
|
meta[i].lineKind == LineKind::CommandRow) {
|
|
long endPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, (unsigned long)i);
|
|
for (long p = endPos - 1; p >= 0; --p) {
|
|
int ch = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETCHARAT, (unsigned long)p);
|
|
if (ch == ' ' || ch == '\t') continue;
|
|
if (ch == '{')
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, p, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
|
m_currentSelIds = selIds;
|
|
m_sci->markerDeleteAll(M_SELECTED);
|
|
m_sci->markerDeleteAll(M_ACCENT);
|
|
|
|
// Clear all editable indicators, then repaint for selected lines only
|
|
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
|
|
|
|
// Use index: iterate selected IDs, look up their lines
|
|
for (uint64_t selId : selIds) {
|
|
bool isFooterSel = (selId & kFooterIdBit) != 0;
|
|
bool isArrayElemSel = (selId & kArrayElemBit) != 0;
|
|
bool isMemberSel = (selId & kMemberBit) != 0;
|
|
int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1;
|
|
int memberSubLine = isMemberSel ? memberSubFromSelId(selId) : -1;
|
|
uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
|
| kMemberBit | kMemberSubMask);
|
|
auto it = m_nodeLineIndex.constFind(nodeId);
|
|
if (it == m_nodeLineIndex.constEnd()) continue;
|
|
for (int ln : *it) {
|
|
if (isSyntheticLine(m_meta[ln])) continue;
|
|
bool isFooter = (m_meta[ln].lineKind == LineKind::Footer);
|
|
// Match selection type to line type
|
|
if (isFooterSel && !isFooter) continue;
|
|
if (!isFooterSel && isFooter) continue;
|
|
// Array element: match by element index
|
|
if (isArrayElemSel) {
|
|
if (!m_meta[ln].isArrayElement || m_meta[ln].arrayElementIdx != arrayElemIdx)
|
|
continue;
|
|
} else if (m_meta[ln].isArrayElement) {
|
|
continue;
|
|
}
|
|
// Member line: match by subLine index
|
|
if (isMemberSel) {
|
|
if (!m_meta[ln].isMemberLine || m_meta[ln].subLine != memberSubLine)
|
|
continue;
|
|
} else if (m_meta[ln].isMemberLine) {
|
|
continue;
|
|
}
|
|
m_sci->markerAdd(ln, M_SELECTED);
|
|
m_sci->markerAdd(ln, M_ACCENT);
|
|
if (!isFooter)
|
|
paintEditableSpans(ln);
|
|
}
|
|
}
|
|
|
|
// Reset hint line - updateEditableIndicators will handle cursor hints
|
|
// on actual user navigation (not stale restored positions)
|
|
m_hintLine = -1;
|
|
|
|
applyHoverHighlight();
|
|
applyHoverCursor();
|
|
}
|
|
|
|
void RcxEditor::applyHoverHighlight() {
|
|
uint64_t prevId = m_prevHoveredNodeId;
|
|
int prevLine = m_prevHoveredLine;
|
|
m_prevHoveredNodeId = m_hoveredNodeId;
|
|
m_prevHoveredLine = m_hoveredLine;
|
|
|
|
// Fast path: nothing changed (same node AND same line)
|
|
if (prevId == m_hoveredNodeId && prevLine == m_hoveredLine
|
|
&& m_hoveredNodeId != 0) return;
|
|
|
|
// Remove old hover markers
|
|
if (prevId != 0) {
|
|
// Check if old hovered line was a single-line highlight (footer or array element)
|
|
bool prevSingleLine = (prevLine >= 0 && prevLine < m_meta.size() &&
|
|
(m_meta[prevLine].lineKind == LineKind::Footer || m_meta[prevLine].isArrayElement
|
|
|| m_meta[prevLine].isMemberLine));
|
|
if (prevSingleLine) {
|
|
m_sci->markerDelete(prevLine, M_HOVER);
|
|
} else {
|
|
auto it = m_nodeLineIndex.constFind(prevId);
|
|
if (it != m_nodeLineIndex.constEnd()) {
|
|
for (int ln : *it)
|
|
m_sci->markerDelete(ln, M_HOVER);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_editState.active) return;
|
|
if (!m_hoverInside) return;
|
|
if (m_hoveredNodeId == 0) return;
|
|
|
|
// Footer, array elements, and member lines highlight only the specific line
|
|
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
|
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
|
|
bool hoveringArrayElem = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
|
m_meta[m_hoveredLine].isArrayElement);
|
|
bool hoveringMember = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
|
m_meta[m_hoveredLine].isMemberLine);
|
|
|
|
// Check if the hovered item is already selected (using appropriate ID)
|
|
uint64_t checkId;
|
|
if (hoveringFooter)
|
|
checkId = m_hoveredNodeId | kFooterIdBit;
|
|
else if (hoveringArrayElem)
|
|
checkId = makeArrayElemSelId(m_hoveredNodeId, m_meta[m_hoveredLine].arrayElementIdx);
|
|
else if (hoveringMember)
|
|
checkId = makeMemberSelId(m_hoveredNodeId, m_meta[m_hoveredLine].subLine);
|
|
else
|
|
checkId = m_hoveredNodeId;
|
|
if (m_currentSelIds.contains(checkId)) return;
|
|
|
|
if (hoveringFooter || hoveringArrayElem || hoveringMember) {
|
|
// Single-line highlight for footers, array elements, and member lines
|
|
m_sci->markerAdd(m_hoveredLine, M_HOVER);
|
|
} else {
|
|
// Non-footer, non-array-element: highlight all lines for this node
|
|
auto it = m_nodeLineIndex.constFind(m_hoveredNodeId);
|
|
if (it != m_nodeLineIndex.constEnd()) {
|
|
for (int ln : *it) {
|
|
if (m_meta[ln].lineKind != LineKind::Footer &&
|
|
!m_meta[ln].isArrayElement)
|
|
m_sci->markerAdd(ln, M_HOVER);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ViewState RcxEditor::saveViewState() const {
|
|
ViewState vs;
|
|
vs.scrollLine = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETFIRSTVISIBLELINE);
|
|
int line, col;
|
|
m_sci->getCursorPosition(&line, &col);
|
|
vs.cursorLine = line;
|
|
vs.cursorCol = col;
|
|
vs.xOffset = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETXOFFSET);
|
|
return vs;
|
|
}
|
|
|
|
void RcxEditor::restoreViewState(const ViewState& vs) {
|
|
int maxLine = std::max(0, m_sci->lines() - 1);
|
|
int line = std::clamp(vs.cursorLine, 0, maxLine);
|
|
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
|
(unsigned long)line,
|
|
(long)std::max(0, vs.cursorCol));
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, (unsigned long)pos);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE,
|
|
(unsigned long)vs.scrollLine);
|
|
// Clamp xOffset so it doesn't exceed the current content width.
|
|
// After a rename that shrinks content, the saved offset may be stale.
|
|
int scrollW = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETSCROLLWIDTH);
|
|
int vpW = m_sci->viewport() ? m_sci->viewport()->width() : 0;
|
|
int maxXOff = qMax(0, scrollW - vpW);
|
|
int xOff = qBound(0, vs.xOffset, maxXOff);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET, (unsigned long)xOff);
|
|
}
|
|
|
|
const LineMeta* RcxEditor::metaForLine(int line) const {
|
|
if (line >= 0 && line < m_meta.size())
|
|
return &m_meta[line];
|
|
return nullptr;
|
|
}
|
|
|
|
int RcxEditor::currentNodeIndex() const {
|
|
int line, col;
|
|
m_sci->getCursorPosition(&line, &col);
|
|
auto* lm = metaForLine(line);
|
|
return lm ? lm->nodeIdx : -1;
|
|
}
|
|
|
|
void RcxEditor::showFindBar() {
|
|
m_findBarContainer->setVisible(true);
|
|
m_findBar->setFocus();
|
|
m_findBar->selectAll();
|
|
m_findPos = 0;
|
|
}
|
|
|
|
void RcxEditor::dismissHistoryPopup() {
|
|
if (m_historyPopup)
|
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
|
}
|
|
|
|
void RcxEditor::hideFindBar() {
|
|
m_findBarContainer->setVisible(false);
|
|
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (long)0, docLen);
|
|
m_findPos = 0;
|
|
m_sci->setFocus();
|
|
}
|
|
|
|
void RcxEditor::scrollToNodeId(uint64_t nodeId) {
|
|
for (int i = 0; i < m_meta.size(); i++) {
|
|
if (m_meta[i].nodeId == nodeId && m_meta[i].lineKind != LineKind::Footer) {
|
|
m_sci->setCursorPosition(i, 0);
|
|
m_sci->ensureLineVisible(i);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Column span computation ──
|
|
|
|
ColumnSpan RcxEditor::typeSpan(const LineMeta& lm, int typeW) { return typeSpanFor(lm, typeW); }
|
|
ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int typeW, int nameW) { return nameSpanFor(lm, typeW, nameW); }
|
|
ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int typeW, int nameW) { return valueSpanFor(lm, lineLength, typeW, nameW); }
|
|
|
|
// For pointer-like nodes, narrow value span to just the address portion
|
|
// (before the " // " separator that precedes the symbol like "Module+0x1234").
|
|
static ColumnSpan narrowPtrValueSpan(const LineMeta& lm, const ColumnSpan& vs,
|
|
const QString& lineText) {
|
|
if (!vs.valid) return vs;
|
|
if (!isFuncPtr(lm.nodeKind)
|
|
&& lm.nodeKind != NodeKind::Pointer32
|
|
&& lm.nodeKind != NodeKind::Pointer64)
|
|
return vs;
|
|
QString valText = lineText.mid(vs.start, vs.end - vs.start);
|
|
int sep = valText.indexOf(QLatin1String(" // "));
|
|
if (sep > 0)
|
|
return {vs.start, vs.start + sep, true};
|
|
return vs;
|
|
}
|
|
|
|
// ── Multi-selection ──
|
|
|
|
QSet<int> RcxEditor::selectedNodeIndices() const {
|
|
int lineFrom, indexFrom, lineTo, indexTo;
|
|
m_sci->getSelection(&lineFrom, &indexFrom, &lineTo, &indexTo);
|
|
if (lineFrom < 0) {
|
|
int line, col;
|
|
m_sci->getCursorPosition(&line, &col);
|
|
auto* lm = metaForLine(line);
|
|
return lm && lm->nodeIdx >= 0 ? QSet<int>{lm->nodeIdx} : QSet<int>{};
|
|
}
|
|
QSet<int> result;
|
|
for (int line = lineFrom; line <= lineTo; line++) {
|
|
auto* lm = metaForLine(line);
|
|
if (lm && lm->nodeIdx >= 0) result.insert(lm->nodeIdx);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ── Inline edit helpers ──
|
|
|
|
static QString getLineText(QsciScintilla* sci, int line) {
|
|
int len = (int)sci->SendScintilla(QsciScintillaBase::SCI_LINELENGTH, (unsigned long)line);
|
|
if (len <= 0) return {};
|
|
QByteArray buf(len + 1, '\0');
|
|
sci->SendScintilla(QsciScintillaBase::SCI_GETLINE, (unsigned long)line, (void*)buf.data());
|
|
QString text = QString::fromUtf8(buf.data(), len);
|
|
while (text.endsWith('\n') || text.endsWith('\r'))
|
|
text.chop(1);
|
|
return text;
|
|
}
|
|
|
|
void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
|
|
static constexpr int heatIndicators[] = { IND_HEAT_COLD, IND_HEAT_WARM, IND_HEAT_HOT };
|
|
|
|
for (int i = 0; i < meta.size(); i++) {
|
|
const LineMeta& lm = meta[i];
|
|
if (isSyntheticLine(lm)) continue;
|
|
|
|
int heat = lm.heatLevel;
|
|
int typeW = lm.effectiveTypeW;
|
|
int nameW = lm.effectiveNameW;
|
|
|
|
if (heat <= 0) {
|
|
// Clear any stale heat indicators from a previous frame
|
|
for (int hi : heatIndicators)
|
|
clearIndicatorLine(hi, i);
|
|
continue;
|
|
}
|
|
|
|
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
|
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
|
|
|
// Apply heat-level indicator to value span (narrowed for pointer-like nodes)
|
|
QString lineText = getLineText(m_sci, i);
|
|
ColumnSpan vs = narrowPtrValueSpan(lm,
|
|
valueSpan(lm, lineText.size(), typeW, nameW), lineText);
|
|
if (!vs.valid) continue;
|
|
|
|
fillIndicatorCols(activeInd, i, vs.start, vs.end);
|
|
|
|
// Clear the other two heat indicators on this span to avoid overlap
|
|
for (int hi : heatIndicators) {
|
|
if (hi != activeInd)
|
|
clearIndicatorLine(hi, i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RcxEditor::applySymbolColoring(const QVector<LineMeta>& meta) {
|
|
for (int i = 0; i < meta.size(); i++) {
|
|
const LineMeta& lm = meta[i];
|
|
if (!isFuncPtr(lm.nodeKind)
|
|
&& lm.nodeKind != NodeKind::Pointer32
|
|
&& lm.nodeKind != NodeKind::Pointer64)
|
|
continue;
|
|
QString lineText = getLineText(m_sci, i);
|
|
// Find " // " within the value region and color "// sym" portion green
|
|
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
|
if (!vs.valid) continue;
|
|
int searchFrom = vs.start;
|
|
int sep = lineText.indexOf(QLatin1String(" // "), searchFrom);
|
|
if (sep < 0 || sep >= vs.end) continue;
|
|
int symStart = sep + 2; // start of "// sym"
|
|
int symEnd = vs.end;
|
|
while (symEnd > symStart && lineText[symEnd - 1] == ' ') symEnd--;
|
|
if (symEnd > symStart)
|
|
fillIndicatorCols(IND_HINT_GREEN, i, symStart, symEnd);
|
|
}
|
|
}
|
|
|
|
void RcxEditor::applyBaseAddressColoring(const QVector<LineMeta>& meta) {
|
|
if (meta.isEmpty() || meta[0].lineKind != LineKind::CommandRow) return;
|
|
|
|
clearIndicatorLine(IND_BASE_ADDR, 0);
|
|
// Override lexer's green number coloring on the address with default text color
|
|
QString t = getLineText(m_sci, 0);
|
|
ColumnSpan addr = commandRowAddrSpan(t);
|
|
if (addr.valid)
|
|
fillIndicatorCols(IND_BASE_ADDR, 0, addr.start, addr.end);
|
|
}
|
|
|
|
void RcxEditor::applyCommandRowPills() {
|
|
if (m_meta.isEmpty() || m_meta[0].lineKind != LineKind::CommandRow) return;
|
|
|
|
constexpr int line = 0;
|
|
QString t = getLineText(m_sci, line);
|
|
|
|
clearIndicatorLine(IND_HEX_DIM, line);
|
|
clearIndicatorLine(IND_CLASS_NAME, line);
|
|
|
|
// Dim the [▾] type-selector chevron
|
|
ColumnSpan chevron = commandRowChevronSpan(t);
|
|
if (chevron.valid)
|
|
fillIndicatorCols(IND_HEX_DIM, line, chevron.start, chevron.end);
|
|
|
|
// Dim label text: source arrow/placeholder + its ▾ dropdown arrow
|
|
ColumnSpan srcSpan = commandRowSrcSpan(t);
|
|
if (srcSpan.valid) {
|
|
int quotePos = t.indexOf('\'', srcSpan.start);
|
|
int kindEnd = (quotePos > srcSpan.start) ? quotePos : srcSpan.end;
|
|
while (kindEnd > srcSpan.start && t[kindEnd - 1].isSpace()) kindEnd--;
|
|
if (kindEnd > srcSpan.start)
|
|
fillIndicatorCols(IND_HEX_DIM, line, srcSpan.start, kindEnd);
|
|
// Dim the source ▾ dropdown arrow to match (like struct▾)
|
|
int srcDrop = t.indexOf(QChar(0x25BE));
|
|
int rootStart = commandRowRootStart(t);
|
|
if (srcDrop >= 0 && (rootStart < 0 || srcDrop < rootStart))
|
|
fillIndicatorCols(IND_HEX_DIM, line, srcDrop, srcDrop + 1);
|
|
}
|
|
// Dim base address to match source/struct grey
|
|
ColumnSpan addrSpan = commandRowAddrSpan(t);
|
|
if (addrSpan.valid)
|
|
fillIndicatorCols(IND_HEX_DIM, line, addrSpan.start, addrSpan.end);
|
|
|
|
// Root class styling (type dim + class-name teal, no underline)
|
|
ColumnSpan rt = commandRowRootTypeSpan(t);
|
|
if (rt.valid) {
|
|
fillIndicatorCols(IND_HEX_DIM, line, rt.start, rt.end);
|
|
int drop = t.indexOf(QChar(0x25BE), rt.start);
|
|
if (drop >= 0)
|
|
fillIndicatorCols(IND_HEX_DIM, line, drop, qMin(drop + 2, t.size()));
|
|
}
|
|
ColumnSpan rn = commandRowRootNameSpan(t);
|
|
if (rn.valid) {
|
|
fillIndicatorCols(IND_CLASS_NAME, line, rn.start, rn.end);
|
|
}
|
|
|
|
// Dim trailing opening brace to match the rest of the command row grey
|
|
for (int i = t.size() - 1; i >= 0; --i) {
|
|
if (t[i] == ' ' || t[i] == '\t') continue;
|
|
if (t[i] == '{')
|
|
fillIndicatorCols(IND_HEX_DIM, line, i, i + 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ── Shared inline-edit shutdown ──
|
|
|
|
RcxEditor::EndEditInfo RcxEditor::endInlineEdit() {
|
|
// Dismiss any open user list / autocomplete popup
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL);
|
|
// Clear edit comment and error marker before deactivating
|
|
if (m_editState.target == EditTarget::Value) {
|
|
setEditComment({}); // Clear to spaces
|
|
m_sci->markerDelete(m_editState.line, M_ERR);
|
|
}
|
|
EndEditInfo info{m_editState.nodeIdx, m_editState.subLine, m_editState.target};
|
|
m_editState.active = false;
|
|
m_sci->setReadOnly(true);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 0);
|
|
// Switch back to Arrow cursor (widget-local, doesn't fight splitters/menus)
|
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
|
// Disable selection rendering again
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)1);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_EMPTYUNDOBUFFER);
|
|
return info;
|
|
}
|
|
|
|
// ── Span helpers ──
|
|
|
|
// Name span for struct/array headers - uses column-based positioning
|
|
// Format: [fold][indent][type col][sep][name col][sep][suffix]
|
|
static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
|
|
if (lm.lineKind != LineKind::Header) return {};
|
|
|
|
int ind = kFoldCol + lm.depth * 3;
|
|
int typeW = lm.effectiveTypeW;
|
|
int nameStart = ind + typeW + kSepWidth;
|
|
|
|
if (nameStart >= lineText.size()) return {};
|
|
|
|
// Name ends before " {" suffix (expanded) or at line end (collapsed)
|
|
int nameEnd = lineText.size();
|
|
if (lineText.endsWith(QStringLiteral(" {")))
|
|
nameEnd = lineText.size() - 2;
|
|
|
|
if (nameEnd <= nameStart) return {};
|
|
|
|
// Don't allow editing array element names like "[0]", "[1]", etc.
|
|
QString name = lineText.mid(nameStart, nameEnd - nameStart).trimmed();
|
|
if (name.isEmpty()) return {};
|
|
if (name.startsWith('[') && name.endsWith(']'))
|
|
return {};
|
|
|
|
return {nameStart, nameEnd, true};
|
|
}
|
|
|
|
// Type name span for struct headers (not arrays)
|
|
// Named structs format as: "_MMPTE OriginalPte {" (type column = just the name)
|
|
// Anonymous structs format as: "union {" or "struct {" (no clickable type)
|
|
static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText) {
|
|
if (lm.lineKind != LineKind::Header) return {};
|
|
if (lm.isArrayHeader) return {};
|
|
|
|
int ind = kFoldCol + lm.depth * 3;
|
|
int typeW = lm.effectiveTypeW;
|
|
int typeEnd = ind + typeW;
|
|
if (typeEnd > lineText.size()) typeEnd = lineText.size();
|
|
|
|
QString typeCol = lineText.mid(ind, typeEnd - ind).trimmed();
|
|
if (typeCol.isEmpty()) return {};
|
|
|
|
// Anonymous structs use bare keywords — not clickable
|
|
static const QStringList kKeywords = {
|
|
QStringLiteral("struct"), QStringLiteral("union"), QStringLiteral("class")
|
|
};
|
|
if (kKeywords.contains(typeCol)) return {};
|
|
|
|
// Static field headers: "static hex64 target {" — skip "static " prefix
|
|
if (lm.isStaticLine) {
|
|
int cursor = ind;
|
|
while (cursor < typeEnd && lineText[cursor] == ' ') cursor++;
|
|
if (lineText.mid(cursor, 7) == QLatin1String("static "))
|
|
cursor += 7;
|
|
while (cursor < typeEnd && lineText[cursor] == ' ') cursor++;
|
|
int tStart = cursor;
|
|
while (cursor < typeEnd && lineText[cursor] != ' ') cursor++;
|
|
if (cursor > tStart)
|
|
return {tStart, cursor, true};
|
|
return {};
|
|
}
|
|
|
|
// Named struct: entire type column is the type name (e.g. "_MMPTE")
|
|
// Find the actual text bounds within the padded column
|
|
int start = ind;
|
|
while (start < typeEnd && lineText[start] == ' ') start++;
|
|
int end = start;
|
|
while (end < typeEnd && lineText[end] != ' ') end++;
|
|
if (end <= start) return {};
|
|
|
|
return {start, end, true};
|
|
}
|
|
|
|
// Type span for array headers: "int32_t[10]" in "int32_t[10] positions {"
|
|
static ColumnSpan arrayHeaderTypeSpan(const LineMeta& lm, const QString& lineText) {
|
|
if (lm.lineKind != LineKind::Header || !lm.isArrayHeader) return {};
|
|
int ind = kFoldCol + lm.depth * 3;
|
|
int typeEnd = lineText.indexOf(' ', ind);
|
|
if (typeEnd <= ind) return {};
|
|
return {ind, typeEnd, true};
|
|
}
|
|
|
|
RcxEditor::NormalizedSpan RcxEditor::normalizeSpan(
|
|
const ColumnSpan& raw, const QString& lineText,
|
|
EditTarget target, bool skipPrefixes) const
|
|
{
|
|
if (!raw.valid) return {};
|
|
int textLen = lineText.size();
|
|
if (raw.start >= textLen) return {};
|
|
|
|
int start = raw.start;
|
|
int end = qMin(raw.end, textLen);
|
|
if (end <= start) return {};
|
|
|
|
if (skipPrefixes && target == EditTarget::Value) {
|
|
QString spanText = lineText.mid(start, end - start);
|
|
int arrow = spanText.indexOf(QStringLiteral("->"));
|
|
if (arrow >= 0) {
|
|
int i = arrow + 2;
|
|
while (i < spanText.size() && spanText[i].isSpace()) i++;
|
|
start += i;
|
|
} else {
|
|
int eq = spanText.indexOf('=');
|
|
if (eq >= 0 && eq <= 3) {
|
|
int i = eq + 1;
|
|
while (i < spanText.size() && spanText[i].isSpace()) i++;
|
|
start += i;
|
|
}
|
|
}
|
|
if (start >= end) return {};
|
|
}
|
|
|
|
QString inner = lineText.mid(start, end - start);
|
|
int lead = 0;
|
|
while (lead < inner.size() && inner[lead].isSpace()) lead++;
|
|
int trail = inner.size();
|
|
while (trail > lead && inner[trail - 1].isSpace()) trail--;
|
|
if (trail <= lead) return {};
|
|
|
|
return {start + lead, start + trail, true};
|
|
}
|
|
|
|
bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
|
NormalizedSpan& out, QString* lineTextOut) const {
|
|
const LineMeta* lm = metaForLine(line);
|
|
if (!lm) return false;
|
|
|
|
// CommandRow: Source / BaseAddress / Root class (type+name) editing
|
|
if (lm->lineKind == LineKind::CommandRow) {
|
|
if (t != EditTarget::BaseAddress && t != EditTarget::Source
|
|
&& t != EditTarget::RootClassType && t != EditTarget::RootClassName
|
|
&& t != EditTarget::TypeSelector) return false;
|
|
QString lineText = getLineText(m_sci, line);
|
|
ColumnSpan s;
|
|
if (t == EditTarget::TypeSelector) s = commandRowChevronSpan(lineText);
|
|
else if (t == EditTarget::Source) s = commandRowSrcSpan(lineText);
|
|
else if (t == EditTarget::BaseAddress) s = commandRowAddrSpan(lineText);
|
|
else if (t == EditTarget::RootClassType) s = commandRowRootTypeSpan(lineText);
|
|
else s = commandRowRootNameSpan(lineText);
|
|
out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/(t == EditTarget::BaseAddress));
|
|
if (lineTextOut) *lineTextOut = lineText;
|
|
return out.valid;
|
|
}
|
|
|
|
if (lm->nodeIdx < 0) return false;
|
|
|
|
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
|
// Exception: static field names are always editable (they're function names)
|
|
if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind) && !lm->isStaticLine)
|
|
return false;
|
|
|
|
QString lineText = getLineText(m_sci, line);
|
|
int textLen = lineText.size();
|
|
|
|
// Use per-line effective widths (set during compose based on containing scope)
|
|
int typeW = lm->effectiveTypeW;
|
|
int nameW = lm->effectiveNameW;
|
|
|
|
ColumnSpan s;
|
|
switch (t) {
|
|
case EditTarget::Type: s = typeSpan(*lm, typeW); break;
|
|
case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break;
|
|
case EditTarget::Value: s = narrowPtrValueSpan(*lm,
|
|
valueSpan(*lm, textLen, typeW, nameW), lineText); break;
|
|
case EditTarget::BaseAddress: break; // No longer on header lines
|
|
case EditTarget::ArrayIndex:
|
|
case EditTarget::ArrayCount:
|
|
break; // Array navigation removed
|
|
case EditTarget::ArrayElementType:
|
|
s = arrayElemTypeSpanFor(*lm, lineText); break;
|
|
case EditTarget::ArrayElementCount:
|
|
s = arrayElemCountSpanFor(*lm, lineText); break;
|
|
case EditTarget::PointerTarget:
|
|
s = pointerTargetSpanFor(*lm, lineText); break;
|
|
case EditTarget::StaticExpr:
|
|
if (lm->isStaticLine)
|
|
s = staticExprSpanFor(*lm, lineText);
|
|
break;
|
|
case EditTarget::Source: break;
|
|
}
|
|
|
|
// Fallback spans for header lines
|
|
if (!s.valid && t == EditTarget::Type) {
|
|
// For pointer fields, the full type span acts as "kind" span
|
|
// For array headers, fall back to the full type[count] span
|
|
s = arrayHeaderTypeSpan(*lm, lineText);
|
|
if (!s.valid)
|
|
s = headerTypeNameSpan(*lm, lineText);
|
|
if (!s.valid)
|
|
s = pointerKindSpanFor(*lm, lineText);
|
|
}
|
|
if (!s.valid && t == EditTarget::Name)
|
|
s = headerNameSpan(*lm, lineText);
|
|
|
|
// Member lines: override Name/Value spans
|
|
if (!s.valid && lm->isMemberLine) {
|
|
if (t == EditTarget::Name) s = memberNameSpanFor(*lm, lineText);
|
|
if (t == EditTarget::Value) s = memberValueSpanFor(*lm, lineText);
|
|
}
|
|
|
|
out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/true);
|
|
if (lineTextOut) *lineTextOut = lineText;
|
|
return out.valid;
|
|
}
|
|
|
|
// ── Point → line/col/nodeId resolution ──
|
|
|
|
RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const {
|
|
HitInfo h;
|
|
|
|
// Try precise position first (works when cursor is over actual text)
|
|
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
|
|
(unsigned long)vp.x(), (long)vp.y());
|
|
if (pos >= 0) {
|
|
h.line = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos);
|
|
h.col = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos);
|
|
} else {
|
|
// Fallback: calculate line from Y coordinate (for empty space past text)
|
|
int firstVisible = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_GETFIRSTVISIBLELINE);
|
|
int lineHeight = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_TEXTHEIGHT, 0);
|
|
if (lineHeight > 0)
|
|
h.line = firstVisible + vp.y() / lineHeight;
|
|
}
|
|
|
|
if (h.line >= 0 && h.line < m_meta.size()) {
|
|
h.nodeId = m_meta[h.line].nodeId;
|
|
h.inFoldCol = (h.col >= 0 && h.col < kFoldCol + 1 && m_meta[h.line].foldHead);
|
|
}
|
|
return h;
|
|
}
|
|
|
|
// ── Double-click hit test ──
|
|
|
|
static bool hitTestTarget(QsciScintilla* sci,
|
|
const QVector<LineMeta>& meta,
|
|
const QPoint& viewportPos,
|
|
int& outLine, int& outCol, EditTarget& outTarget)
|
|
{
|
|
long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
|
|
(unsigned long)viewportPos.x(), (long)viewportPos.y());
|
|
if (pos < 0) return false;
|
|
int line = (int)sci->SendScintilla(QsciScintillaBase::SCI_LINEFROMPOSITION,
|
|
(unsigned long)pos);
|
|
int col = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETCOLUMN,
|
|
(unsigned long)pos);
|
|
outCol = col;
|
|
if (line < 0 || line >= meta.size()) return false;
|
|
|
|
QString lineText = getLineText(sci, line);
|
|
int textLen = lineText.size();
|
|
|
|
const LineMeta& lm = meta[line];
|
|
|
|
if (lm.lineKind == LineKind::ArrayElementSeparator) return false;
|
|
|
|
auto inSpan = [&](const ColumnSpan& s) {
|
|
return s.valid && col >= s.start && col < s.end;
|
|
};
|
|
|
|
// CommandRow: interactive chevron/SRC/ADDR + root class (type+name)
|
|
if (lm.lineKind == LineKind::CommandRow) {
|
|
ColumnSpan chevron = commandRowChevronSpan(lineText);
|
|
if (inSpan(chevron)) { outTarget = EditTarget::TypeSelector; outLine = line; return true; }
|
|
ColumnSpan ss = commandRowSrcSpan(lineText);
|
|
if (inSpan(ss)) { outTarget = EditTarget::Source; outLine = line; return true; }
|
|
ColumnSpan as = commandRowAddrSpan(lineText);
|
|
if (inSpan(as)) { outTarget = EditTarget::BaseAddress; outLine = line; return true; }
|
|
|
|
// RootClassType is no longer clickable — use right-click to convert
|
|
ColumnSpan rns = commandRowRootNameSpan(lineText);
|
|
if (inSpan(rns)) { outTarget = EditTarget::RootClassName; outLine = line; return true; }
|
|
return false;
|
|
}
|
|
|
|
// Use per-line effective widths from LineMeta
|
|
int typeW = lm.effectiveTypeW;
|
|
int nameW = lm.effectiveNameW;
|
|
|
|
ColumnSpan ts = RcxEditor::typeSpan(lm, typeW);
|
|
ColumnSpan ns = RcxEditor::nameSpan(lm, typeW, nameW);
|
|
ColumnSpan vs = narrowPtrValueSpan(lm,
|
|
RcxEditor::valueSpan(lm, textLen, typeW, nameW), lineText);
|
|
|
|
// Pointer fields/headers: check sub-spans within type column first
|
|
if (lm.nodeKind == NodeKind::Pointer32 || lm.nodeKind == NodeKind::Pointer64) {
|
|
ColumnSpan ptrTarget = pointerTargetSpanFor(lm, lineText);
|
|
ColumnSpan ptrKind = pointerKindSpanFor(lm, lineText);
|
|
if (inSpan(ptrTarget)) { outTarget = EditTarget::PointerTarget; outLine = line; return true; }
|
|
if (inSpan(ptrKind)) { outTarget = EditTarget::Type; outLine = line; return true; }
|
|
}
|
|
|
|
// Array headers: check element type and count sub-spans first
|
|
// Count click area includes brackets [N] so clicking [ or ] edits the count
|
|
if (lm.isArrayHeader) {
|
|
ColumnSpan elemCountClick = arrayElemCountClickSpanFor(lm, lineText);
|
|
ColumnSpan elemType = arrayElemTypeSpanFor(lm, lineText);
|
|
if (inSpan(elemCountClick)) { outTarget = EditTarget::ArrayElementCount; outLine = line; return true; }
|
|
if (inSpan(elemType)) { outTarget = EditTarget::ArrayElementType; outLine = line; return true; }
|
|
}
|
|
|
|
// Fallback spans for header lines
|
|
if (!ts.valid) {
|
|
ts = arrayHeaderTypeSpan(lm, lineText);
|
|
if (!ts.valid)
|
|
ts = headerTypeNameSpan(lm, lineText);
|
|
}
|
|
if (!ns.valid)
|
|
ns = headerNameSpan(lm, lineText);
|
|
|
|
// Member lines: use name/value spans from line text (no type span)
|
|
if (lm.isMemberLine) {
|
|
ns = memberNameSpanFor(lm, lineText);
|
|
vs = memberValueSpanFor(lm, lineText);
|
|
}
|
|
|
|
if (inSpan(ts)) outTarget = EditTarget::Type;
|
|
else if (inSpan(ns)) outTarget = EditTarget::Name;
|
|
else if (inSpan(vs)) outTarget = EditTarget::Value;
|
|
else return false;
|
|
|
|
// Array headers: redirect generic Type hit to ArrayElementType (uses popup, not inline edit)
|
|
if (lm.isArrayHeader && outTarget == EditTarget::Type) {
|
|
outTarget = EditTarget::ArrayElementType;
|
|
outLine = line;
|
|
return true;
|
|
}
|
|
// Array element lines: type/name click opens element type picker on the parent array header
|
|
if (lm.isArrayElement && (outTarget == EditTarget::Type || outTarget == EditTarget::Name)) {
|
|
outTarget = EditTarget::ArrayElementType;
|
|
// Find the array header line (previous line with isArrayHeader and same nodeIdx)
|
|
for (int l = line - 1; l >= 0; l--) {
|
|
if (l >= meta.size()) continue;
|
|
const LineMeta& hdr = meta[l];
|
|
if (hdr.isArrayHeader && hdr.nodeIdx == lm.nodeIdx) {
|
|
outLine = l;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
|
if ((outTarget == EditTarget::Name || outTarget == EditTarget::Value) && isHexNode(lm.nodeKind))
|
|
return false;
|
|
|
|
outLine = line;
|
|
return true;
|
|
}
|
|
|
|
// ── Event filter ──
|
|
|
|
bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|
if (obj == m_sci && event->type() == QEvent::KeyPress) {
|
|
auto* ke = static_cast<QKeyEvent*>(event);
|
|
if (ke->matches(QKeySequence::Find)) {
|
|
showFindBar();
|
|
return true;
|
|
}
|
|
bool handled = m_editState.active ? handleEditKey(ke) : handleNormalKey(ke);
|
|
if (!handled && !m_editState.active) {
|
|
// Clear hover on keyboard navigation (stale after scroll)
|
|
m_hoveredNodeId = 0;
|
|
m_hoveredLine = -1;
|
|
applyHoverHighlight();
|
|
}
|
|
return handled;
|
|
}
|
|
if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonPress
|
|
&& m_editState.active) {
|
|
auto* me = static_cast<QMouseEvent*>(event);
|
|
auto h = hitTest(me->pos());
|
|
|
|
if (h.line == m_editState.line) {
|
|
int editEnd = editEndCol();
|
|
bool insideTrimmed = (h.col >= m_editState.spanStart && h.col <= editEnd);
|
|
|
|
if (insideTrimmed)
|
|
return false; // inside trimmed text: let Scintilla position cursor
|
|
|
|
// Check raw span (full column width) - click in padding moves cursor to end
|
|
const LineMeta* lm = metaForLine(m_editState.line);
|
|
if (lm) {
|
|
QString lineText = getLineText(m_sci, h.line);
|
|
// Use per-line effective widths
|
|
int typeW = lm->effectiveTypeW;
|
|
int nameW = lm->effectiveNameW;
|
|
ColumnSpan raw;
|
|
switch (m_editState.target) {
|
|
case EditTarget::Type: raw = typeSpan(*lm, typeW); break;
|
|
case EditTarget::Name: raw = nameSpan(*lm, typeW, nameW); break;
|
|
case EditTarget::Value: raw = valueSpan(*lm, lineText.size(), typeW, nameW); break;
|
|
case EditTarget::BaseAddress: raw = commandRowAddrSpan(lineText); break;
|
|
case EditTarget::Source: raw = commandRowSrcSpan(lineText); break;
|
|
case EditTarget::ArrayIndex: raw = arrayIndexSpanFor(*lm, lineText); break;
|
|
case EditTarget::ArrayCount: raw = arrayCountSpanFor(*lm, lineText); break;
|
|
case EditTarget::ArrayElementType: raw = arrayElemTypeSpanFor(*lm, lineText); break;
|
|
case EditTarget::ArrayElementCount: raw = arrayElemCountSpanFor(*lm, lineText); break;
|
|
case EditTarget::PointerTarget: raw = pointerTargetSpanFor(*lm, lineText); break;
|
|
}
|
|
if (raw.valid && h.col >= raw.start && h.col < raw.end) {
|
|
// Within raw span but outside trimmed text → move cursor to end
|
|
long endPos = posFromCol(m_sci, m_editState.line, editEnd);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, endPos);
|
|
return true; // consume event
|
|
}
|
|
}
|
|
}
|
|
|
|
commitInlineEdit();
|
|
m_currentSelIds.clear();
|
|
return true; // consume — metadata was recomposed; stale coords unsafe
|
|
}
|
|
// Single-click on fold column (" - " / " + ") toggles fold
|
|
// Other left-clicks emit nodeClicked for selection
|
|
if (obj == m_sci->viewport() && !m_editState.active
|
|
&& event->type() == QEvent::MouseButtonPress) {
|
|
auto* me = static_cast<QMouseEvent*>(event);
|
|
if (me->button() == Qt::LeftButton) {
|
|
// Sync hover to click position (prevents hover/selection desync)
|
|
m_lastHoverPos = me->pos();
|
|
m_hoverInside = true;
|
|
auto h = hitTest(me->pos());
|
|
uint64_t newHoverId = (h.line >= 0) ? h.nodeId : 0;
|
|
if (newHoverId != m_hoveredNodeId || h.line != m_hoveredLine) {
|
|
m_hoveredNodeId = newHoverId;
|
|
m_hoveredLine = h.line;
|
|
applyHoverHighlight();
|
|
}
|
|
|
|
if (h.inFoldCol) {
|
|
emit marginClicked(0, h.line, me->modifiers());
|
|
return true;
|
|
}
|
|
// CommandRow: try chevron/ADDR edit or consume
|
|
if (h.nodeId == kCommandRowId) {
|
|
int tLine, tCol; EditTarget t;
|
|
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
|
|
if (t == EditTarget::TypeSelector)
|
|
emit typeSelectorRequested();
|
|
else
|
|
beginInlineEdit(t, tLine, tCol);
|
|
}
|
|
return true; // consume all CommandRow clicks
|
|
}
|
|
if (h.nodeId != 0) {
|
|
bool alreadySelected = m_currentSelIds.contains(h.nodeId);
|
|
bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier));
|
|
|
|
// Single-click on editable token of already-selected node → edit
|
|
int tLine, tCol; EditTarget t;
|
|
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
|
|
if (alreadySelected && plain) {
|
|
m_pendingClickNodeId = 0;
|
|
return beginInlineEdit(t, tLine, tCol);
|
|
}
|
|
}
|
|
|
|
m_dragging = true;
|
|
m_dragStarted = false; // require threshold before extending
|
|
m_dragStartPos = me->pos();
|
|
m_dragLastLine = h.line;
|
|
m_dragInitMods = me->modifiers();
|
|
|
|
bool multi = m_currentSelIds.size() > 1;
|
|
|
|
if (alreadySelected && multi && plain) {
|
|
// Defer: might be start of double-click-to-edit
|
|
m_pendingClickNodeId = h.nodeId;
|
|
m_pendingClickLine = h.line;
|
|
m_pendingClickMods = me->modifiers();
|
|
} else {
|
|
emit nodeClicked(h.line, h.nodeId, me->modifiers());
|
|
m_pendingClickNodeId = 0;
|
|
}
|
|
}
|
|
return true; // consume ALL left-clicks (prevent QScintilla caret/cursor)
|
|
}
|
|
}
|
|
// Drag-select: extend selection as mouse moves with button held
|
|
// Requires minimum drag distance to prevent accidental micro-drag selection
|
|
if (obj == m_sci->viewport() && !m_editState.active
|
|
&& event->type() == QEvent::MouseMove && m_dragging) {
|
|
auto* me = static_cast<QMouseEvent*>(event);
|
|
if (me->buttons() & Qt::LeftButton) {
|
|
// Check drag threshold (8 pixels) before starting drag-selection
|
|
if (!m_dragStarted) {
|
|
int dy = me->pos().y() - m_dragStartPos.y();
|
|
if (qAbs(dy) < 8)
|
|
return true; // not yet a drag, but still consume (don't let Scintilla handle)
|
|
m_dragStarted = true;
|
|
}
|
|
|
|
// Flush deferred click before extending drag
|
|
if (m_pendingClickNodeId != 0) {
|
|
emit nodeClicked(m_pendingClickLine, m_pendingClickNodeId,
|
|
m_pendingClickMods);
|
|
m_pendingClickNodeId = 0;
|
|
}
|
|
auto h = hitTest(me->pos());
|
|
if (h.line >= 0 && h.line != m_dragLastLine && h.nodeId != 0) {
|
|
emit nodeClicked(h.line, h.nodeId, m_dragInitMods | Qt::ShiftModifier);
|
|
m_dragLastLine = h.line;
|
|
}
|
|
} else {
|
|
m_dragging = false;
|
|
m_dragStarted = false;
|
|
}
|
|
}
|
|
if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonRelease) {
|
|
m_dragging = false;
|
|
m_dragStarted = false;
|
|
if (m_pendingClickNodeId != 0) {
|
|
emit nodeClicked(m_pendingClickLine, m_pendingClickNodeId,
|
|
m_pendingClickMods);
|
|
m_pendingClickNodeId = 0;
|
|
}
|
|
return true; // consume release (prevent QScintilla from acting on it)
|
|
}
|
|
// Double-click on offset margin → toggle absolute/relative
|
|
if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonDblClick) {
|
|
auto* me = static_cast<QMouseEvent*>(event);
|
|
int margin0Width = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_GETMARGINWIDTHN, 0UL, 0L);
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
if ((int)me->position().x() < margin0Width) {
|
|
#else
|
|
if ((int)me->pos().x() < margin0Width) {
|
|
#endif
|
|
m_relativeOffsets = !m_relativeOffsets;
|
|
reformatMargins();
|
|
return true;
|
|
}
|
|
}
|
|
// Double-click during edit mode: select entire editable text
|
|
if (obj == m_sci->viewport() && m_editState.active
|
|
&& event->type() == QEvent::MouseButtonDblClick) {
|
|
m_sci->setSelection(m_editState.line, m_editState.spanStart,
|
|
m_editState.line, editEndCol());
|
|
return true;
|
|
}
|
|
if (obj == m_sci->viewport() && !m_editState.active
|
|
&& event->type() == QEvent::MouseButtonDblClick) {
|
|
auto* me = static_cast<QMouseEvent*>(event);
|
|
int line, tCol; EditTarget t;
|
|
if (hitTestTarget(m_sci, m_meta, me->pos(), line, tCol, t)) {
|
|
m_pendingClickNodeId = 0; // cancel deferred selection change
|
|
// Narrow selection to this node before editing
|
|
auto h = hitTest(me->pos());
|
|
if (h.nodeId != 0 && h.nodeId != kCommandRowId)
|
|
emit nodeClicked(h.line, h.nodeId, Qt::NoModifier);
|
|
return beginInlineEdit(t, line, tCol);
|
|
}
|
|
return true; // consume even on miss (prevent QScintilla word-select)
|
|
}
|
|
if (obj == m_sci && event->type() == QEvent::FocusOut) {
|
|
auto* fe = static_cast<QFocusEvent*>(event);
|
|
// Commit active edit on focus loss (click-away = save)
|
|
// Deferred so autocomplete popup has time to register as active
|
|
if (m_editState.active && fe->reason() != Qt::PopupFocusReason) {
|
|
QTimer::singleShot(0, this, [this]() {
|
|
if (m_editState.active && !m_sci->hasFocus()
|
|
&& !m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCACTIVE))
|
|
commitInlineEdit();
|
|
});
|
|
}
|
|
// Clear editable indicators when editor loses focus
|
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
|
m_hintLine = -1;
|
|
}
|
|
if (obj == m_sci && event->type() == QEvent::FocusIn) {
|
|
int line, col;
|
|
m_sci->getCursorPosition(&line, &col);
|
|
updateEditableIndicators(line);
|
|
}
|
|
// Track mouse position for cursor updates (both edit and non-edit mode)
|
|
if (obj == m_sci->viewport()) {
|
|
// Ignore synthetic Leave from setText() during document refresh
|
|
if (m_applyingDocument && event->type() == QEvent::Leave)
|
|
return true;
|
|
|
|
if (event->type() == QEvent::MouseMove) {
|
|
m_lastHoverPos = static_cast<QMouseEvent*>(event)->pos();
|
|
m_hoverInside = true;
|
|
} else if (event->type() == QEvent::Leave) {
|
|
// Don't dismiss if cursor moved onto one of our own popups
|
|
QPoint globalCursor = QCursor::pos();
|
|
bool onPopup = false;
|
|
if (m_historyPopup && m_historyPopup->isVisible()
|
|
&& m_historyPopup->geometry().contains(globalCursor))
|
|
onPopup = true;
|
|
if (m_disasmPopup && m_disasmPopup->isVisible()
|
|
&& m_disasmPopup->geometry().contains(globalCursor))
|
|
onPopup = true;
|
|
if (m_structPreviewPopup && m_structPreviewPopup->isVisible()
|
|
&& m_structPreviewPopup->geometry().contains(globalCursor))
|
|
onPopup = true;
|
|
if (!onPopup) {
|
|
m_hoverInside = false;
|
|
if (!m_editState.active) {
|
|
m_hoveredNodeId = 0;
|
|
m_hoveredLine = -1;
|
|
applyHoverHighlight();
|
|
}
|
|
}
|
|
} else if (event->type() == QEvent::Wheel) {
|
|
m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos());
|
|
m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos);
|
|
}
|
|
// Resolve hovered nodeId on move/wheel (non-edit mode only)
|
|
if (!m_editState.active &&
|
|
(event->type() == QEvent::MouseMove || event->type() == QEvent::Wheel)) {
|
|
auto h = hitTest(m_lastHoverPos);
|
|
uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0;
|
|
int newHoverLine = (m_hoverInside && h.line >= 0) ? h.line : -1;
|
|
if (newHoverId != m_hoveredNodeId || newHoverLine != m_hoveredLine) {
|
|
m_hoveredNodeId = newHoverId;
|
|
m_hoveredLine = newHoverLine;
|
|
applyHoverHighlight();
|
|
}
|
|
}
|
|
// Update cursor on move/leave/wheel (both edit and non-edit mode)
|
|
if (event->type() == QEvent::MouseMove
|
|
|| event->type() == QEvent::Leave
|
|
|| event->type() == QEvent::Wheel)
|
|
applyHoverCursor();
|
|
|
|
// Consume MouseMove in non-edit mode so QScintilla's internal handler
|
|
// doesn't override our cursor (it resets to Arrow for read-only widgets)
|
|
if (!m_editState.active && event->type() == QEvent::MouseMove)
|
|
return true;
|
|
}
|
|
return QWidget::eventFilter(obj, event);
|
|
}
|
|
|
|
// ── Normal mode key handling ──
|
|
|
|
bool RcxEditor::handleNormalKey(QKeyEvent* ke) {
|
|
switch (ke->key()) {
|
|
case Qt::Key_F2:
|
|
return beginInlineEdit(EditTarget::Name);
|
|
case Qt::Key_T:
|
|
if (ke->modifiers() == Qt::NoModifier)
|
|
return beginInlineEdit(EditTarget::Type);
|
|
return false;
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
return beginInlineEdit(EditTarget::Value);
|
|
case Qt::Key_Insert:
|
|
if (ke->modifiers() & Qt::ShiftModifier)
|
|
emit insertAboveRequested(currentNodeIndex(), NodeKind::Hex32);
|
|
else
|
|
emit insertAboveRequested(currentNodeIndex(), NodeKind::Hex64);
|
|
return true;
|
|
case Qt::Key_Tab: {
|
|
EditTarget order[] = {EditTarget::Name, EditTarget::Type, EditTarget::Value,
|
|
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
|
|
EditTarget::PointerTarget};
|
|
constexpr int N = 6;
|
|
int start = 0;
|
|
for (int i = 0; i < N; i++)
|
|
if (order[i] == m_lastTabTarget) { start = (i + 1) % N; break; }
|
|
for (int i = 0; i < N; i++) {
|
|
EditTarget t = order[(start + i) % N];
|
|
if (beginInlineEdit(t)) { m_lastTabTarget = t; return true; }
|
|
}
|
|
return true;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ── Edit mode key handling ──
|
|
|
|
bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
|
// User list is handled via userListActivated signal, not here
|
|
// SCI_AUTOCACTIVE is for autocomplete, not user lists
|
|
|
|
switch (ke->key()) {
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
commitInlineEdit();
|
|
return true;
|
|
case Qt::Key_Tab:
|
|
m_lastTabTarget = m_editState.target;
|
|
commitInlineEdit();
|
|
return true;
|
|
case Qt::Key_Escape:
|
|
cancelInlineEdit();
|
|
return true;
|
|
case Qt::Key_Up:
|
|
case Qt::Key_Down:
|
|
case Qt::Key_PageUp:
|
|
case Qt::Key_PageDown:
|
|
return true; // block line navigation
|
|
case Qt::Key_Delete: {
|
|
int line, col;
|
|
m_sci->getCursorPosition(&line, &col);
|
|
if (col >= editEndCol()) return true; // block at end
|
|
return false; // allow delete within span
|
|
}
|
|
case Qt::Key_Left:
|
|
case Qt::Key_Backspace: {
|
|
int line, col;
|
|
m_sci->getCursorPosition(&line, &col);
|
|
int minCol = m_editState.spanStart;
|
|
// Don't allow backing into "0x" prefix
|
|
if (m_editState.target == EditTarget::Value || m_editState.target == EditTarget::BaseAddress) {
|
|
QString lineText = getLineText(m_sci, m_editState.line);
|
|
if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
|
minCol = m_editState.spanStart + 2;
|
|
}
|
|
// If there's an active selection, collapse it to the left end (Left only, not Backspace)
|
|
if (ke->key() == Qt::Key_Left) {
|
|
int sL, sC, eL, eC;
|
|
m_sci->getSelection(&sL, &sC, &eL, &eC);
|
|
if (sL >= 0 && (sL != eL || sC != eC)) {
|
|
int leftEnd = qMax(qMin(sC, eC), minCol);
|
|
m_sci->setCursorPosition(m_editState.line, leftEnd);
|
|
return true;
|
|
}
|
|
}
|
|
if (col <= minCol) return true;
|
|
return false;
|
|
}
|
|
case Qt::Key_Right: {
|
|
int line, col;
|
|
m_sci->getCursorPosition(&line, &col);
|
|
// If there's an active selection, collapse it to the right end first
|
|
int sL, sC, eL, eC;
|
|
m_sci->getSelection(&sL, &sC, &eL, &eC);
|
|
if (sL >= 0 && (sL != eL || sC != eC)) {
|
|
int rightEnd = qMin(qMax(sC, eC), editEndCol());
|
|
m_sci->setCursorPosition(m_editState.line, rightEnd);
|
|
return true;
|
|
}
|
|
if (col >= editEndCol()) return true; // block past end
|
|
return false;
|
|
}
|
|
case Qt::Key_Home: {
|
|
int home = m_editState.spanStart;
|
|
// Skip "0x" prefix for hex values
|
|
if (m_editState.target == EditTarget::Value || m_editState.target == EditTarget::BaseAddress) {
|
|
QString lineText = getLineText(m_sci, m_editState.line);
|
|
if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
|
home = m_editState.spanStart + 2;
|
|
}
|
|
m_sci->setCursorPosition(m_editState.line, home);
|
|
return true;
|
|
}
|
|
case Qt::Key_End:
|
|
m_sci->setCursorPosition(m_editState.line, editEndCol());
|
|
return true;
|
|
case Qt::Key_V:
|
|
if (ke->modifiers() & Qt::ControlModifier) {
|
|
// Sanitized paste: strip newlines (and backticks for base addresses)
|
|
QString clip = QApplication::clipboard()->text();
|
|
clip.remove('\n');
|
|
clip.remove('\r');
|
|
if (m_editState.target == EditTarget::BaseAddress)
|
|
clip.remove('`');
|
|
if (!clip.isEmpty()) {
|
|
QByteArray utf8 = clip.toUtf8();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
|
|
(uintptr_t)0, utf8.constData());
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ── Begin inline edit ──
|
|
|
|
bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|
if (target == EditTarget::TypeSelector) return false; // handled by popup, not inline edit
|
|
|
|
// Type, array element type and pointer target: handled by TypeSelectorPopup, not inline edit
|
|
if (target == EditTarget::Type || target == EditTarget::ArrayElementType || target == EditTarget::PointerTarget) {
|
|
if (line < 0) {
|
|
int c;
|
|
m_sci->getCursorPosition(&line, &c);
|
|
}
|
|
auto* lm = metaForLine(line);
|
|
if (!lm) return false;
|
|
// Reject lines that don't support type editing
|
|
if (lm->nodeIdx < 0) return false; // CommandRow etc.
|
|
if (lm->lineKind == LineKind::Footer) return false;
|
|
// Position popup at the type column start
|
|
ColumnSpan ts = typeSpan(*lm);
|
|
long typePos = posFromCol(m_sci, line, ts.valid ? ts.start : 0);
|
|
int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, (unsigned long)line);
|
|
int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION, (unsigned long)0, typePos);
|
|
int y = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION, (unsigned long)0, typePos);
|
|
QPoint pos = m_sci->viewport()->mapToGlobal(QPoint(x, y + lineH));
|
|
emit typePickerRequested(target, lm->nodeIdx, pos);
|
|
return true;
|
|
}
|
|
|
|
if (m_editState.active) return false;
|
|
m_hoveredNodeId = 0;
|
|
m_hoveredLine = -1;
|
|
applyHoverHighlight();
|
|
// Dismiss hover popups so they get recreated with Set buttons once edit starts
|
|
if (m_historyPopup)
|
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
|
if (m_structPreviewPopup)
|
|
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
|
// Clear editable-token color hints (de-emphasize non-active tokens)
|
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
|
m_hintLine = -1;
|
|
|
|
if (line >= 0) {
|
|
m_sci->setCursorPosition(line, col >= 0 ? col : 0);
|
|
}
|
|
if (col < 0) {
|
|
m_sci->getCursorPosition(&line, &col);
|
|
}
|
|
auto* lm = metaForLine(line);
|
|
if (!lm) return false;
|
|
// Allow nodeIdx=-1 only for CommandRow editing (command bar)
|
|
if (lm->nodeIdx < 0 && !(lm->lineKind == LineKind::CommandRow &&
|
|
(target == EditTarget::BaseAddress || target == EditTarget::Source
|
|
|| target == EditTarget::RootClassType || target == EditTarget::RootClassName)))
|
|
return false;
|
|
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
|
// Exception: static field names are always editable (they're function names, not hex labels)
|
|
if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind) && !lm->isStaticLine)
|
|
return false;
|
|
|
|
QString lineText;
|
|
NormalizedSpan norm;
|
|
if (!resolvedSpanFor(line, target, norm, &lineText)) return false;
|
|
|
|
QString trimmed = lineText.mid(norm.start, norm.end - norm.start);
|
|
|
|
int vecComponent = 0; // which vector/matrix component
|
|
|
|
// Helper: parse comma-separated components, narrow span to clicked one
|
|
auto narrowToComponent = [&](const QString& inner, int innerAbsStart) {
|
|
QVector<int> compStarts, compEnds;
|
|
for (int i = 0; i < inner.size(); i++) {
|
|
if (inner[i] == ',') {
|
|
compEnds.append(i);
|
|
int next = i + 1;
|
|
while (next < inner.size() && inner[next] == ' ') next++;
|
|
compStarts.append(next);
|
|
}
|
|
}
|
|
compStarts.prepend(0);
|
|
compEnds.append(inner.size());
|
|
|
|
int relCol = col - innerAbsStart;
|
|
vecComponent = 0;
|
|
for (int i = 0; i < compStarts.size(); i++) {
|
|
if (relCol >= compStarts[i] && (i == compStarts.size() - 1 || relCol < compStarts[i + 1]))
|
|
{ vecComponent = i; break; }
|
|
}
|
|
if (vecComponent >= compStarts.size()) vecComponent = compStarts.size() - 1;
|
|
|
|
int cStart = innerAbsStart + compStarts[vecComponent];
|
|
int cEnd = innerAbsStart + compEnds[vecComponent];
|
|
while (cEnd > cStart && lineText[cEnd - 1] == ' ') cEnd--;
|
|
norm.start = cStart;
|
|
norm.end = cEnd;
|
|
trimmed = lineText.mid(norm.start, norm.end - norm.start);
|
|
};
|
|
|
|
// For vector value editing: narrow span to the clicked component
|
|
if (target == EditTarget::Value && isVectorKind(lm->nodeKind)) {
|
|
narrowToComponent(trimmed, norm.start);
|
|
}
|
|
|
|
// For Mat4x4 value editing: skip "rowN [...]" and narrow to clicked component
|
|
if (target == EditTarget::Value && isMatrixKind(lm->nodeKind)) {
|
|
int bracketOpen = trimmed.indexOf('[');
|
|
int bracketClose = trimmed.lastIndexOf(']');
|
|
if (bracketOpen < 0 || bracketClose <= bracketOpen)
|
|
return false;
|
|
QString inner = trimmed.mid(bracketOpen + 1, bracketClose - bracketOpen - 1);
|
|
int innerAbsStart = norm.start + bracketOpen + 1;
|
|
narrowToComponent(inner, innerAbsStart);
|
|
}
|
|
|
|
m_editState.active = true;
|
|
m_editState.line = line;
|
|
m_editState.nodeIdx = lm->nodeIdx;
|
|
m_editState.subLine = lm->subLine;
|
|
m_editState.target = target;
|
|
m_editState.spanStart = norm.start;
|
|
m_editState.original = trimmed;
|
|
m_editState.linelenAfterReplace = lineText.size();
|
|
m_editState.editKind = lm->nodeKind;
|
|
if (isVectorKind(lm->nodeKind)) {
|
|
m_editState.subLine = vecComponent;
|
|
m_editState.editKind = NodeKind::Float;
|
|
}
|
|
if (isMatrixKind(lm->nodeKind)) {
|
|
m_editState.subLine = lm->subLine * 4 + vecComponent; // flat index 0-15
|
|
m_editState.editKind = NodeKind::Float;
|
|
}
|
|
|
|
// Store fixed comment column position for value editing
|
|
// Use large lineLength so commentCol is always computed (padding added dynamically)
|
|
if (target == EditTarget::Value) {
|
|
ColumnSpan cs = commentSpanFor(*lm, 9999, lm->effectiveTypeW, lm->effectiveNameW);
|
|
m_editState.commentCol = cs.valid ? cs.start : -1;
|
|
m_editState.lastValidationOk = true; // original value is always valid
|
|
} else {
|
|
m_editState.commentCol = -1;
|
|
}
|
|
|
|
// Keep undo collection enabled during inline edit so CellBuffer::DeleteChars
|
|
// returns valid text pointers (collectingUndo=false returns nullptr, which
|
|
// crashes QsciAccessibleBase::textDeleted). We clear the buffer on edit end.
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)1);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1);
|
|
m_sci->setReadOnly(false);
|
|
|
|
// For value editing: extend line with trailing spaces for the edit comment area
|
|
// (comment padding is no longer baked into every line to avoid unnecessary scroll width)
|
|
if (target == EditTarget::Value && m_editState.commentCol >= 0) {
|
|
int commentStart = norm.end + 2;
|
|
int neededLen = commentStart + kColComment;
|
|
int currentLen = (int)lineText.size();
|
|
if (currentLen < neededLen) {
|
|
int extend = neededLen - currentLen;
|
|
long lineEndPos = posFromCol(m_sci, line, currentLen);
|
|
QString pad(extend, ' ');
|
|
QByteArray padUtf8 = pad.toUtf8();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, lineEndPos);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, lineEndPos);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET,
|
|
(uintptr_t)padUtf8.size(), padUtf8.constData());
|
|
m_editState.linelenAfterReplace += extend;
|
|
}
|
|
}
|
|
|
|
// Switch to I-beam for editing (skip for picker-based targets)
|
|
if (target != EditTarget::Type && target != EditTarget::Source
|
|
&& target != EditTarget::ArrayElementType && target != EditTarget::PointerTarget
|
|
&& target != EditTarget::RootClassType) {
|
|
m_sci->viewport()->setCursor(Qt::IBeamCursor);
|
|
}
|
|
|
|
// Re-enable selection rendering for inline edit (skip for picker-based targets)
|
|
bool isPicker = (target == EditTarget::Type || target == EditTarget::Source
|
|
|| target == EditTarget::ArrayElementType
|
|
|| target == EditTarget::PointerTarget
|
|
|| target == EditTarget::RootClassType);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
|
if (!isPicker)
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1,
|
|
ThemeManager::instance().current().selection);
|
|
|
|
// Use correct UTF-8 position conversion (not lineStart + col!)
|
|
m_editState.posStart = posFromCol(m_sci, line, norm.start);
|
|
m_editState.posEnd = posFromCol(m_sci, line, norm.end);
|
|
|
|
// For Value/BaseAddress: skip 0x prefix in selection (select only the number)
|
|
long selStart = m_editState.posStart;
|
|
if ((target == EditTarget::Value || target == EditTarget::BaseAddress) &&
|
|
trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) {
|
|
selStart = m_editState.posStart + 2; // Skip "0x"
|
|
}
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, selStart, m_editState.posEnd);
|
|
|
|
// Show initial edit hint in comment column
|
|
if (target == EditTarget::Value)
|
|
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
|
|
|
|
// Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup
|
|
// and exit early above (never reach here).
|
|
if (target == EditTarget::Source)
|
|
QTimer::singleShot(0, this, &RcxEditor::showSourcePicker);
|
|
// RootClassType is no longer editable via click — use right-click conversion instead
|
|
// Refresh hover cursor so value history popup appears with Set buttons immediately
|
|
if (target == EditTarget::Value)
|
|
QTimer::singleShot(0, this, &RcxEditor::applyHoverCursor);
|
|
return true;
|
|
}
|
|
|
|
int RcxEditor::editEndCol() const {
|
|
QString lineText = getLineText(m_sci, m_editState.line);
|
|
int delta = lineText.size() - m_editState.linelenAfterReplace;
|
|
return m_editState.spanStart + m_editState.original.size() + delta;
|
|
}
|
|
|
|
void RcxEditor::clampEditSelection() {
|
|
if (!m_editState.active) return;
|
|
|
|
if (m_clampingSelection) return;
|
|
m_clampingSelection = true;
|
|
|
|
int selStartLine, selStartCol, selEndLine, selEndCol;
|
|
m_sci->getSelection(&selStartLine, &selStartCol, &selEndLine, &selEndCol);
|
|
|
|
int editEnd = editEndCol();
|
|
bool isCursor = (selStartLine == selEndLine && selStartCol == selEndCol);
|
|
|
|
// Don't fight cursor positioning - only clamp actual selections
|
|
if (isCursor) {
|
|
m_clampingSelection = false;
|
|
return;
|
|
}
|
|
|
|
// Actual selection - clamp both ends to edit span
|
|
bool clamped = false;
|
|
|
|
// Force to edit line
|
|
if (selStartLine != m_editState.line || selEndLine != m_editState.line) {
|
|
m_sci->setSelection(m_editState.line, m_editState.spanStart,
|
|
m_editState.line, editEnd);
|
|
m_clampingSelection = false;
|
|
return;
|
|
}
|
|
|
|
if (selStartCol < m_editState.spanStart) { selStartCol = m_editState.spanStart; clamped = true; }
|
|
if (selEndCol < m_editState.spanStart) { selEndCol = m_editState.spanStart; clamped = true; }
|
|
if (selStartCol > editEnd) { selStartCol = editEnd; clamped = true; }
|
|
if (selEndCol > editEnd) { selEndCol = editEnd; clamped = true; }
|
|
|
|
if (clamped)
|
|
m_sci->setSelection(selStartLine, selStartCol, selEndLine, selEndCol);
|
|
|
|
m_clampingSelection = false;
|
|
}
|
|
|
|
// ── Commit inline edit ──
|
|
|
|
void RcxEditor::commitInlineEdit() {
|
|
if (!m_editState.active) return;
|
|
|
|
QString lineText = getLineText(m_sci, m_editState.line);
|
|
int currentLen = lineText.size();
|
|
int delta = currentLen - m_editState.linelenAfterReplace;
|
|
int editedLen = m_editState.original.size() + delta;
|
|
|
|
QString editedText;
|
|
if (editedLen > 0)
|
|
editedText = lineText.mid(m_editState.spanStart, editedLen).trimmed();
|
|
|
|
// For Type edits: if nothing changed, commit original
|
|
if (m_editState.target == EditTarget::Type && editedText.isEmpty())
|
|
editedText = m_editState.original;
|
|
|
|
// Grab resolved address from LineMeta before endInlineEdit clears state
|
|
const LineMeta* lm = metaForLine(m_editState.line);
|
|
uint64_t addr = lm ? lm->offsetAddr : 0;
|
|
|
|
auto info = endInlineEdit();
|
|
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText, addr);
|
|
}
|
|
|
|
// ── Cancel inline edit ──
|
|
|
|
void RcxEditor::cancelInlineEdit() {
|
|
if (!m_editState.active) return;
|
|
|
|
endInlineEdit();
|
|
emit inlineEditCancelled();
|
|
}
|
|
|
|
// ── Type picker (user list) ──
|
|
|
|
void RcxEditor::showTypeAutocomplete() {
|
|
if (!m_editState.active ||
|
|
(m_editState.target != EditTarget::Type && m_editState.target != EditTarget::ArrayElementType))
|
|
return;
|
|
// Replace original type with spaces (keeps layout, clears for typing)
|
|
int len = m_editState.original.size();
|
|
QString spaces(len, ' ');
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
|
|
m_editState.posStart, m_editState.posEnd);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
|
|
(uintptr_t)0, spaces.toUtf8().constData());
|
|
|
|
// Position cursor at start
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart);
|
|
|
|
showTypeListFiltered(QString()); // Show full list initially
|
|
}
|
|
|
|
void RcxEditor::showTypeListFiltered(const QString& filter) {
|
|
if (!m_editState.active ||
|
|
(m_editState.target != EditTarget::Type && m_editState.target != EditTarget::ArrayElementType))
|
|
return;
|
|
|
|
// Combine native types with custom (struct) type names
|
|
QStringList all = allTypeNamesForUI();
|
|
for (const QString& ct : m_customTypeNames) {
|
|
if (!all.contains(ct))
|
|
all << ct;
|
|
}
|
|
all.sort(Qt::CaseInsensitive);
|
|
|
|
// Filter by prefix
|
|
QStringList filtered;
|
|
for (const QString& t : all) {
|
|
if (filter.isEmpty() || t.startsWith(filter, Qt::CaseInsensitive))
|
|
filtered << t;
|
|
}
|
|
if (filtered.isEmpty()) return; // No matches - keep list hidden
|
|
|
|
// Show user list (id=1 for types) - selection handled by userListActivated signal
|
|
QByteArray list = filtered.join('\n').toUtf8();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n');
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
|
|
(uintptr_t)1, list.constData());
|
|
// Force Arrow cursor immediately (don't wait for mouse move)
|
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
|
}
|
|
|
|
void RcxEditor::showSourcePicker() {
|
|
if (!m_editState.active || m_editState.target != EditTarget::Source)
|
|
return;
|
|
QMenu menu;
|
|
QFont menuFont = editorFont();
|
|
int zoom = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
|
|
menuFont.setPointSize(menuFont.pointSize() + zoom);
|
|
menu.setFont(menuFont);
|
|
menu.addAction("File");
|
|
|
|
// Add all registered providers from global registry
|
|
const auto& providers = ProviderRegistry::instance().providers();
|
|
for (const auto& provider : providers)
|
|
menu.addAction(provider.name);
|
|
|
|
// Saved sources below separator (with checkmarks)
|
|
if (!m_savedSourceDisplay.isEmpty()) {
|
|
menu.addSeparator();
|
|
for (int i = 0; i < m_savedSourceDisplay.size(); i++) {
|
|
auto* act = menu.addAction(m_savedSourceDisplay[i].text);
|
|
act->setCheckable(true);
|
|
act->setChecked(m_savedSourceDisplay[i].active);
|
|
act->setData(i);
|
|
}
|
|
menu.addSeparator();
|
|
auto* clearAct = menu.addAction("Clear All");
|
|
clearAct->setData(QStringLiteral("#clear"));
|
|
}
|
|
|
|
int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
|
|
int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
|
0, m_editState.posStart);
|
|
int y = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
|
0, m_editState.posStart);
|
|
QPoint pos = m_sci->viewport()->mapToGlobal(QPoint(x, y + lineH));
|
|
|
|
QAction* sel = menu.exec(pos);
|
|
if (sel) {
|
|
const LineMeta* lm = metaForLine(m_editState.line);
|
|
uint64_t addr = lm ? lm->offsetAddr : 0;
|
|
auto info = endInlineEdit();
|
|
QString text = sel->text();
|
|
if (sel->data().toString() == QStringLiteral("#clear"))
|
|
text = QStringLiteral("#clear");
|
|
else if (sel->data().isValid())
|
|
text = QStringLiteral("#saved:") + QString::number(sel->data().toInt());
|
|
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
|
|
} else {
|
|
cancelInlineEdit();
|
|
}
|
|
}
|
|
|
|
void RcxEditor::updateTypeListFilter() {
|
|
if (!m_editState.active ||
|
|
(m_editState.target != EditTarget::Type && m_editState.target != EditTarget::ArrayElementType))
|
|
return;
|
|
|
|
// Get currently typed text from line
|
|
QString lineText = getLineText(m_sci, m_editState.line);
|
|
long curPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
|
|
int col = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETCOLUMN,
|
|
(unsigned long)curPos);
|
|
|
|
// Extract text from spanStart to cursor
|
|
int len = col - m_editState.spanStart;
|
|
if (len <= 0) {
|
|
showTypeListFiltered(QString()); // Show full list
|
|
return;
|
|
}
|
|
|
|
QString typed = lineText.mid(m_editState.spanStart, len);
|
|
showTypeListFiltered(typed);
|
|
}
|
|
|
|
// ── Pointer target picker ──
|
|
|
|
void RcxEditor::showPointerTargetPicker() {
|
|
if (!m_editState.active || m_editState.target != EditTarget::PointerTarget)
|
|
return;
|
|
// Replace original target with spaces (keeps layout, clears for typing)
|
|
int len = m_editState.original.size();
|
|
QString spaces(len, ' ');
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
|
|
m_editState.posStart, m_editState.posEnd);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
|
|
(uintptr_t)0, spaces.toUtf8().constData());
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart);
|
|
showPointerTargetListFiltered(QString());
|
|
}
|
|
|
|
void RcxEditor::showPointerTargetListFiltered(const QString& filter) {
|
|
if (!m_editState.active || m_editState.target != EditTarget::PointerTarget)
|
|
return;
|
|
|
|
// Build list: "void" + all struct type names
|
|
QStringList all;
|
|
all << QStringLiteral("void");
|
|
for (const QString& ct : m_customTypeNames) {
|
|
if (!all.contains(ct))
|
|
all << ct;
|
|
}
|
|
all.sort(Qt::CaseInsensitive);
|
|
// Ensure "void" is always first
|
|
all.removeAll(QStringLiteral("void"));
|
|
all.prepend(QStringLiteral("void"));
|
|
|
|
QStringList filtered;
|
|
for (const QString& t : all) {
|
|
if (filter.isEmpty() || t.startsWith(filter, Qt::CaseInsensitive))
|
|
filtered << t;
|
|
}
|
|
if (filtered.isEmpty()) return;
|
|
|
|
QByteArray list = filtered.join('\n').toUtf8();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n');
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
|
|
(uintptr_t)1, list.constData());
|
|
// Force Arrow cursor immediately (don't wait for mouse move)
|
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
|
}
|
|
|
|
void RcxEditor::updatePointerTargetFilter() {
|
|
if (!m_editState.active || m_editState.target != EditTarget::PointerTarget)
|
|
return;
|
|
|
|
QString lineText = getLineText(m_sci, m_editState.line);
|
|
long curPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
|
|
int col = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETCOLUMN,
|
|
(unsigned long)curPos);
|
|
int len = col - m_editState.spanStart;
|
|
if (len <= 0) {
|
|
showPointerTargetListFiltered(QString());
|
|
return;
|
|
}
|
|
QString typed = lineText.mid(m_editState.spanStart, len);
|
|
showPointerTargetListFiltered(typed);
|
|
}
|
|
|
|
// ── Editable-field text-color indicator ──
|
|
|
|
void RcxEditor::paintEditableSpans(int line) {
|
|
const LineMeta* lm = metaForLine(line);
|
|
if (!lm) return;
|
|
// CommandRow: paint Source/BaseAddress + root class (type+name) spans
|
|
if (lm->lineKind == LineKind::CommandRow) {
|
|
NormalizedSpan norm;
|
|
if (resolvedSpanFor(line, EditTarget::Source, norm))
|
|
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
|
if (resolvedSpanFor(line, EditTarget::BaseAddress, norm))
|
|
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
|
// RootClassType no longer shown as editable — right-click conversion instead
|
|
if (resolvedSpanFor(line, EditTarget::RootClassName, norm))
|
|
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
|
return;
|
|
}
|
|
if (isSyntheticLine(*lm)) return;
|
|
NormalizedSpan norm;
|
|
for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value,
|
|
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
|
|
EditTarget::PointerTarget}) {
|
|
if (resolvedSpanFor(line, t, norm))
|
|
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
|
}
|
|
}
|
|
|
|
void RcxEditor::updateEditableIndicators(int line) {
|
|
if (m_editState.active) return;
|
|
if (line == m_hintLine) return;
|
|
|
|
// No cursor hints when selection is empty (prevents desync during batch ops)
|
|
if (m_currentSelIds.isEmpty()) {
|
|
if (m_hintLine >= 0) {
|
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
|
m_hintLine = -1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Helper to check if a line's node is selected (handles footer/array element IDs)
|
|
auto isLineSelected = [this](const LineMeta* lm) -> bool {
|
|
if (!lm) return false;
|
|
uint64_t checkId;
|
|
if (lm->lineKind == LineKind::Footer)
|
|
checkId = lm->nodeId | kFooterIdBit;
|
|
else if (lm->isArrayElement && lm->arrayElementIdx >= 0)
|
|
checkId = makeArrayElemSelId(lm->nodeId, lm->arrayElementIdx);
|
|
else if (lm->isMemberLine && lm->subLine >= 0)
|
|
checkId = makeMemberSelId(lm->nodeId, lm->subLine);
|
|
else
|
|
checkId = lm->nodeId;
|
|
return m_currentSelIds.contains(checkId);
|
|
};
|
|
|
|
// If new line is selected, its indicators are managed by applySelectionOverlay
|
|
// But we still need to clear the old non-selected hint line
|
|
const LineMeta* newLm = metaForLine(line);
|
|
if (isLineSelected(newLm)) {
|
|
if (m_hintLine >= 0) {
|
|
const LineMeta* oldLm = metaForLine(m_hintLine);
|
|
if (!isLineSelected(oldLm))
|
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
|
}
|
|
m_hintLine = line;
|
|
return;
|
|
}
|
|
|
|
// Clear old cursor line (only if not a selected node)
|
|
if (m_hintLine >= 0) {
|
|
const LineMeta* oldLm = metaForLine(m_hintLine);
|
|
if (!isLineSelected(oldLm))
|
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
|
}
|
|
|
|
m_hintLine = line;
|
|
paintEditableSpans(line);
|
|
}
|
|
|
|
// ── Hover cursor ──
|
|
|
|
void RcxEditor::applyHoverCursor() {
|
|
// Clear previous hover span indicators
|
|
for (int ln : m_hoverSpanLines)
|
|
clearIndicatorLine(IND_HOVER_SPAN, ln);
|
|
m_hoverSpanLines.clear();
|
|
|
|
// Lock cursor to Arrow during drag-selection (prevents flicker)
|
|
if (m_dragStarted) {
|
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
|
return;
|
|
}
|
|
|
|
// Edit mode: IBeam inside edit span, Arrow outside
|
|
if (m_editState.active) {
|
|
if (m_sci->isListActive()) {
|
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
|
} else {
|
|
auto h = hitTest(m_lastHoverPos);
|
|
if (h.line == m_editState.line &&
|
|
h.col >= m_editState.spanStart && h.col <= editEndCol()) {
|
|
m_sci->viewport()->setCursor(Qt::IBeamCursor);
|
|
} else {
|
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
|
}
|
|
}
|
|
// Value history popup — only during inline value editing on a heated node
|
|
{
|
|
bool showPopup = false;
|
|
if (m_valueHistory && m_editState.target == EditTarget::Value
|
|
&& m_editState.line >= 0 && m_editState.line < m_meta.size()) {
|
|
const LineMeta& lm = m_meta[m_editState.line];
|
|
if (lm.heatLevel > 0 && lm.nodeId != 0) {
|
|
auto it = m_valueHistory->find(lm.nodeId);
|
|
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
|
|
if (!m_historyPopup) {
|
|
m_historyPopup = new ValueHistoryPopup(this);
|
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
|
QPoint gp = e->globalPosition().toPoint();
|
|
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
|
|
m_lastHoverPos = vp;
|
|
m_hoverInside = m_sci->viewport()->rect().contains(vp);
|
|
if (!m_editState.active) {
|
|
auto h2 = hitTest(m_lastHoverPos);
|
|
uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0;
|
|
int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1;
|
|
if (nid != m_hoveredNodeId || nln != m_hoveredLine) {
|
|
m_hoveredNodeId = nid;
|
|
m_hoveredLine = nln;
|
|
applyHoverHighlight();
|
|
}
|
|
}
|
|
applyHoverCursor();
|
|
});
|
|
}
|
|
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
|
|
popup->setOnSet([this](const QString& val) {
|
|
if (!m_editState.active) return;
|
|
long endPos = posFromCol(m_sci, m_editState.line, editEndCol());
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
|
|
m_editState.posStart, endPos);
|
|
QByteArray utf8 = val.toUtf8();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
|
|
(uintptr_t)0, utf8.constData());
|
|
});
|
|
popup->populate(lm.nodeId, *it, editorFont(), true);
|
|
int px = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
|
(unsigned long)0, m_editState.posStart);
|
|
int py = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
|
(unsigned long)0, m_editState.posStart);
|
|
int lh = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT,
|
|
(unsigned long)m_editState.line);
|
|
QPoint anchor = m_sci->viewport()->mapToGlobal(QPoint(px, py + lh));
|
|
popup->showAt(anchor, lh);
|
|
showPopup = true;
|
|
}
|
|
}
|
|
}
|
|
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
|
}
|
|
// Always dismiss disasm/preview popups during inline editing
|
|
if (m_disasmPopup && m_disasmPopup->isVisible())
|
|
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
|
if (m_structPreviewPopup && m_structPreviewPopup->isVisible())
|
|
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
|
return;
|
|
}
|
|
|
|
// Mouse left viewport - set Arrow, dismiss popups
|
|
// (but not during applyDocument — the Leave is synthetic from setText)
|
|
if (!m_hoverInside) {
|
|
if (m_historyPopup && !m_applyingDocument)
|
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
|
if (m_disasmPopup && !m_applyingDocument)
|
|
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
|
if (m_structPreviewPopup && !m_applyingDocument)
|
|
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
|
return;
|
|
}
|
|
|
|
// If autocomplete/user list popup is active, use arrow cursor
|
|
if (m_sci->isListActive()) {
|
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
|
return;
|
|
}
|
|
|
|
auto h = hitTest(m_lastHoverPos);
|
|
int line, hCol; EditTarget t;
|
|
bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, hCol, t);
|
|
|
|
// Skip hover span on footer lines (nothing editable)
|
|
int hoverLine = h.line;
|
|
bool isFooterLine = (hoverLine >= 0 && hoverLine < m_meta.size()
|
|
&& m_meta[hoverLine].lineKind == LineKind::Footer);
|
|
|
|
// Apply hover span indicator for editable tokens
|
|
if (tokenHit && !isFooterLine) {
|
|
NormalizedSpan span;
|
|
QString lineText;
|
|
if (resolvedSpanFor(line, t, span, &lineText)) {
|
|
// For vector/matrix values: narrow hover to the component under cursor
|
|
bool narrowed = false;
|
|
if (t == EditTarget::Value && line >= 0 && line < m_meta.size()) {
|
|
const auto& lm = m_meta[line];
|
|
if (isVectorKind(lm.nodeKind) || isMatrixKind(lm.nodeKind)) {
|
|
QString val = lineText.mid(span.start, span.end - span.start);
|
|
int innerStart = span.start;
|
|
QString inner = val;
|
|
if (isMatrixKind(lm.nodeKind)) {
|
|
int bo = val.indexOf('['), bc = val.lastIndexOf(']');
|
|
if (bo >= 0 && bc > bo) {
|
|
inner = val.mid(bo + 1, bc - bo - 1);
|
|
innerStart = span.start + bo + 1;
|
|
}
|
|
}
|
|
QVector<int> starts, ends;
|
|
starts.append(0);
|
|
for (int i = 0; i < inner.size(); i++) {
|
|
if (inner[i] == ',') {
|
|
ends.append(i);
|
|
int n = i + 1;
|
|
while (n < inner.size() && inner[n] == ' ') n++;
|
|
starts.append(n);
|
|
}
|
|
}
|
|
ends.append(inner.size());
|
|
// Trim trailing spaces from last component to get true end
|
|
int lastEnd = ends.last();
|
|
while (lastEnd > 0 && inner[lastEnd - 1] == ' ') lastEnd--;
|
|
// Skip highlight if cursor is past the last component
|
|
int relCol = h.col - innerStart;
|
|
if (relCol >= lastEnd) {
|
|
narrowed = true; // suppress highlight entirely
|
|
} else {
|
|
int comp = 0;
|
|
for (int i = 0; i < starts.size(); i++) {
|
|
if (relCol >= starts[i] && (i == starts.size() - 1 || relCol < starts[i + 1])) {
|
|
comp = i; break;
|
|
}
|
|
}
|
|
int cS = innerStart + starts[comp];
|
|
int cE = innerStart + ends[comp];
|
|
while (cE > cS && lineText[cE - 1] == ' ') cE--;
|
|
span.start = cS;
|
|
span.end = cE;
|
|
narrowed = true;
|
|
fillIndicatorCols(IND_HOVER_SPAN, line, span.start, span.end);
|
|
m_hoverSpanLines.append(line);
|
|
}
|
|
}
|
|
// Narrow pointer-like nodes to address portion only (exclude symbol)
|
|
if (!narrowed && (isFuncPtr(lm.nodeKind)
|
|
|| lm.nodeKind == NodeKind::Pointer32
|
|
|| lm.nodeKind == NodeKind::Pointer64)) {
|
|
ColumnSpan full = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
|
ColumnSpan narrow = narrowPtrValueSpan(lm, full, lineText);
|
|
if (h.col >= narrow.start && h.col < narrow.end) {
|
|
fillIndicatorCols(IND_HOVER_SPAN, line, narrow.start, narrow.end);
|
|
m_hoverSpanLines.append(line);
|
|
}
|
|
narrowed = true;
|
|
}
|
|
}
|
|
if (!narrowed && h.col >= span.start && h.col < span.end) {
|
|
fillIndicatorCols(IND_HOVER_SPAN, line, span.start, span.end);
|
|
m_hoverSpanLines.append(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply hover span on fold arrows (▸/▾) — same visual feedback as editable tokens
|
|
if (h.inFoldCol && h.line >= 0 && h.line < m_meta.size()) {
|
|
fillIndicatorCols(IND_HOVER_SPAN, h.line, 0, kFoldCol);
|
|
m_hoverSpanLines.append(h.line);
|
|
}
|
|
|
|
// Value history popup on hover (read-only, no buttons)
|
|
// Skip FuncPtr and void-Pointer nodes — they use the disasm popup instead.
|
|
{
|
|
bool showPopup = false;
|
|
if (m_valueHistory && h.line >= 0 && h.line < m_meta.size()) {
|
|
const LineMeta& lm = m_meta[h.line];
|
|
bool skipForDisasm = isFuncPtr(lm.nodeKind)
|
|
|| ((lm.nodeKind == NodeKind::Pointer32
|
|
|| lm.nodeKind == NodeKind::Pointer64)
|
|
&& lm.pointerTargetName.isEmpty());
|
|
if (lm.heatLevel > 0 && lm.nodeId != 0 && !skipForDisasm) {
|
|
auto it = m_valueHistory->find(lm.nodeId);
|
|
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
|
|
QString lineText = getLineText(m_sci, h.line);
|
|
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
|
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
|
|
if (!m_historyPopup) {
|
|
m_historyPopup = new ValueHistoryPopup(this);
|
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
|
QPoint gp = e->globalPosition().toPoint();
|
|
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
|
|
m_lastHoverPos = vp;
|
|
m_hoverInside = m_sci->viewport()->rect().contains(vp);
|
|
if (!m_editState.active) {
|
|
auto h2 = hitTest(m_lastHoverPos);
|
|
uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0;
|
|
int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1;
|
|
if (nid != m_hoveredNodeId || nln != m_hoveredLine) {
|
|
m_hoveredNodeId = nid;
|
|
m_hoveredLine = nln;
|
|
applyHoverHighlight();
|
|
}
|
|
}
|
|
applyHoverCursor();
|
|
});
|
|
}
|
|
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
|
|
popup->populate(lm.nodeId, *it, editorFont(), false);
|
|
long linePos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
|
(unsigned long)h.line);
|
|
long byteOff = lineText.left(vs.start).toUtf8().size();
|
|
int px = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
|
(unsigned long)0, linePos + byteOff);
|
|
int py = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
|
(unsigned long)0, linePos);
|
|
int lh = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT,
|
|
(unsigned long)h.line);
|
|
QPoint anchor = m_sci->viewport()->mapToGlobal(QPoint(px, py + lh));
|
|
popup->showAt(anchor, lh);
|
|
showPopup = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
|
}
|
|
|
|
// Disasm / hex-dump popup on hover for FuncPtr and void Pointer nodes
|
|
{
|
|
bool showDisasm = false;
|
|
if (m_disasmProvider && m_disasmTree && h.line >= 0 && h.line < m_meta.size()) {
|
|
const LineMeta& lm = m_meta[h.line];
|
|
bool isFP = isFuncPtr(lm.nodeKind);
|
|
bool isVoidPtr = (lm.nodeKind == NodeKind::Pointer32
|
|
|| lm.nodeKind == NodeKind::Pointer64)
|
|
&& lm.pointerTargetName.isEmpty();
|
|
if ((isFP || isVoidPtr) && lm.nodeIdx >= 0
|
|
&& lm.nodeIdx < m_disasmTree->nodes.size()) {
|
|
// Check hover is over the address portion of the value column
|
|
QString lineText = getLineText(m_sci, h.line);
|
|
ColumnSpan vs = narrowPtrValueSpan(lm,
|
|
valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW),
|
|
lineText);
|
|
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
|
|
const Node& node = m_disasmTree->nodes[lm.nodeIdx];
|
|
// For void ptrs, only show hex dump if refId == 0
|
|
if (!isVoidPtr || node.refId == 0) {
|
|
bool is64 = (lm.nodeKind == NodeKind::FuncPtr64
|
|
|| lm.nodeKind == NodeKind::Pointer64);
|
|
// Use composed address (absolute, correct for pointer-expanded nodes)
|
|
uint64_t provAddr = lm.offsetAddr;
|
|
uint64_t ptrVal = is64
|
|
? m_disasmProvider->readU64(provAddr)
|
|
: (uint64_t)m_disasmProvider->readU32(provAddr);
|
|
if (ptrVal != 0 && ptrVal != UINT64_MAX
|
|
&& !(is64 == false && ptrVal == 0xFFFFFFFF)) {
|
|
// Read code bytes from the function target address.
|
|
// Use the real provider (not snapshot) because function
|
|
// code lives at arbitrary process addresses that aren't
|
|
// in the snapshot page table.
|
|
const Provider* codeProv = m_disasmRealProv
|
|
? m_disasmRealProv : m_disasmProvider;
|
|
constexpr int kMaxRead = 128;
|
|
uint64_t codeAddr = ptrVal;
|
|
QByteArray bytes(kMaxRead, Qt::Uninitialized);
|
|
bool readOk = codeProv->read(codeAddr, bytes.data(), kMaxRead);
|
|
if (readOk) {
|
|
QString title, body;
|
|
if (isFP) {
|
|
title = QStringLiteral("Disassembly");
|
|
body = disassemble(bytes, ptrVal,
|
|
is64 ? 64 : 32, kMaxRead);
|
|
} else {
|
|
title = QStringLiteral("Hex Dump");
|
|
body = hexDump(bytes, ptrVal, kMaxRead);
|
|
}
|
|
// Cap at 6 lines so the popup stays compact
|
|
{
|
|
const int kMaxLines = 6;
|
|
int nth = 0, idx = 0;
|
|
while (nth < kMaxLines && (idx = body.indexOf('\n', idx)) != -1)
|
|
{ ++nth; ++idx; }
|
|
if (nth == kMaxLines && idx < body.size()) {
|
|
body.truncate(idx);
|
|
body += QStringLiteral("...");
|
|
}
|
|
}
|
|
if (!body.isEmpty()) {
|
|
if (!m_disasmPopup) {
|
|
m_disasmPopup = new DisasmPopup(this);
|
|
static_cast<DisasmPopup*>(m_disasmPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
|
QPoint gp = e->globalPosition().toPoint();
|
|
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
|
|
m_lastHoverPos = vp;
|
|
m_hoverInside = m_sci->viewport()->rect().contains(vp);
|
|
if (!m_editState.active) {
|
|
auto h2 = hitTest(m_lastHoverPos);
|
|
uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0;
|
|
int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1;
|
|
if (nid != m_hoveredNodeId || nln != m_hoveredLine) {
|
|
m_hoveredNodeId = nid;
|
|
m_hoveredLine = nln;
|
|
applyHoverHighlight();
|
|
}
|
|
}
|
|
applyHoverCursor();
|
|
});
|
|
}
|
|
auto* popup = static_cast<DisasmPopup*>(
|
|
m_disasmPopup);
|
|
popup->populate(lm.nodeId, title, body,
|
|
editorFont());
|
|
long linePos = m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_POSITIONFROMLINE,
|
|
(unsigned long)h.line);
|
|
long byteOff = lineText.left(vs.start)
|
|
.toUtf8().size();
|
|
int px = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
|
(unsigned long)0, linePos + byteOff);
|
|
int py = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
|
(unsigned long)0, linePos);
|
|
int lh = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_TEXTHEIGHT,
|
|
(unsigned long)h.line);
|
|
QPoint anchor = m_sci->viewport()->mapToGlobal(
|
|
QPoint(px, py + lh));
|
|
popup->showAt(anchor, lh);
|
|
showDisasm = true;
|
|
// Dismiss value history popup to avoid fighting
|
|
if (m_historyPopup && m_historyPopup->isVisible())
|
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!showDisasm && m_disasmPopup && m_disasmPopup->isVisible())
|
|
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
|
}
|
|
|
|
// Struct preview popup for collapsed typed pointers
|
|
{
|
|
bool showPreview = false;
|
|
if (m_disasmTree && m_disasmProvider && h.line >= 0 && h.line < m_meta.size()) {
|
|
const LineMeta& lm = m_meta[h.line];
|
|
bool isTypedPtr = (lm.nodeKind == NodeKind::Pointer32
|
|
|| lm.nodeKind == NodeKind::Pointer64)
|
|
&& !lm.pointerTargetName.isEmpty();
|
|
if (isTypedPtr && lm.foldCollapsed
|
|
&& lm.nodeIdx >= 0 && lm.nodeIdx < m_disasmTree->nodes.size()) {
|
|
const Node& node = m_disasmTree->nodes[lm.nodeIdx];
|
|
if (node.refId != 0) {
|
|
QString lineText = getLineText(m_sci, h.line);
|
|
ColumnSpan vs = narrowPtrValueSpan(lm,
|
|
valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW),
|
|
lineText);
|
|
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
|
|
ComposeResult cr = rcx::compose(*m_disasmTree, *m_disasmProvider, node.refId);
|
|
// Skip command row (line 0), take first 5 data lines
|
|
QStringList lines = cr.text.split('\n');
|
|
constexpr int kMaxLines = 5;
|
|
QString body;
|
|
int count = 0;
|
|
for (int i = 1; i < lines.size() && count < kMaxLines; ++i) {
|
|
if (!lines[i].isEmpty()) {
|
|
if (count > 0) body += '\n';
|
|
body += lines[i];
|
|
++count;
|
|
}
|
|
}
|
|
if (!body.isEmpty()) {
|
|
if (!m_structPreviewPopup) {
|
|
m_structPreviewPopup = new StructPreviewPopup(this);
|
|
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
|
QPoint gp = e->globalPosition().toPoint();
|
|
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
|
|
m_lastHoverPos = vp;
|
|
m_hoverInside = m_sci->viewport()->rect().contains(vp);
|
|
if (!m_editState.active) {
|
|
auto h2 = hitTest(m_lastHoverPos);
|
|
uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0;
|
|
int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1;
|
|
if (nid != m_hoveredNodeId || nln != m_hoveredLine) {
|
|
m_hoveredNodeId = nid;
|
|
m_hoveredLine = nln;
|
|
applyHoverHighlight();
|
|
}
|
|
}
|
|
applyHoverCursor();
|
|
});
|
|
}
|
|
auto* popup = static_cast<StructPreviewPopup*>(m_structPreviewPopup);
|
|
popup->populate(lm.nodeId,
|
|
lm.pointerTargetName, body, editorFont());
|
|
long linePos = m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_POSITIONFROMLINE,
|
|
(unsigned long)h.line);
|
|
long byteOff = lineText.left(vs.start).toUtf8().size();
|
|
int px = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
|
(unsigned long)0, linePos + byteOff);
|
|
int py = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
|
(unsigned long)0, linePos);
|
|
int lh = (int)m_sci->SendScintilla(
|
|
QsciScintillaBase::SCI_TEXTHEIGHT,
|
|
(unsigned long)h.line);
|
|
QPoint anchor = m_sci->viewport()->mapToGlobal(
|
|
QPoint(px, py + lh));
|
|
popup->showAt(anchor, lh);
|
|
showPreview = true;
|
|
if (m_historyPopup && m_historyPopup->isVisible())
|
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!showPreview && m_structPreviewPopup && m_structPreviewPopup->isVisible())
|
|
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
|
}
|
|
|
|
// Determine cursor shape based on interaction type
|
|
Qt::CursorShape desired = Qt::ArrowCursor;
|
|
|
|
if (h.inFoldCol) {
|
|
desired = Qt::PointingHandCursor; // fold toggle = button
|
|
} else if (tokenHit) {
|
|
// Check if mouse is actually over trimmed text content (not column padding)
|
|
NormalizedSpan trimmed;
|
|
bool overText = resolvedSpanFor(line, t, trimmed)
|
|
&& h.col >= trimmed.start && h.col < trimmed.end;
|
|
if (overText) {
|
|
switch (t) {
|
|
case EditTarget::Type:
|
|
case EditTarget::Source:
|
|
case EditTarget::ArrayElementType:
|
|
case EditTarget::PointerTarget:
|
|
case EditTarget::RootClassType:
|
|
case EditTarget::TypeSelector:
|
|
desired = Qt::PointingHandCursor;
|
|
break;
|
|
default:
|
|
desired = Qt::IBeamCursor;
|
|
break;
|
|
}
|
|
}
|
|
// else: desired stays Arrow (hovering over column padding)
|
|
}
|
|
|
|
m_sci->viewport()->setCursor(desired);
|
|
}
|
|
|
|
// ── Live value validation ──
|
|
|
|
void RcxEditor::setEditComment(const QString& comment) {
|
|
// Value edit must be active
|
|
if (m_editState.commentCol < 0) return;
|
|
|
|
// Prevent re-entrancy from textChanged signal
|
|
if (m_updatingComment) return;
|
|
m_updatingComment = true;
|
|
|
|
QString lineText = getLineText(m_sci, m_editState.line);
|
|
|
|
// Place comment 2 spaces after current value, prefixed with //
|
|
int valueEnd = editEndCol();
|
|
int startCol = valueEnd + 2; // 2 spaces after value
|
|
int endCol = lineText.size();
|
|
int availWidth = endCol - startCol;
|
|
if (availWidth <= 0) { m_updatingComment = false; return; }
|
|
|
|
// Format as "//<comment>" (no space after //)
|
|
QString formatted = QStringLiteral("//") + comment;
|
|
QString padded = formatted.leftJustified(availWidth, ' ').left(availWidth);
|
|
|
|
// Use UTF-8 safe column-to-position conversion
|
|
long posA = posFromCol(m_sci, m_editState.line, startCol);
|
|
long posB = posFromCol(m_sci, m_editState.line, endCol);
|
|
|
|
QByteArray utf8 = padded.toUtf8();
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, posB);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET,
|
|
(uintptr_t)utf8.size(), utf8.constData());
|
|
|
|
// Apply green color to hint text
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HINT_GREEN);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA);
|
|
|
|
m_updatingComment = false;
|
|
}
|
|
|
|
void RcxEditor::validateEditLive() {
|
|
QString lineText = getLineText(m_sci, m_editState.line);
|
|
int delta = lineText.size() - m_editState.linelenAfterReplace;
|
|
int editedLen = m_editState.original.size() + delta;
|
|
QString text = (editedLen > 0)
|
|
? lineText.mid(m_editState.spanStart, editedLen).trimmed() : QString();
|
|
QString errorMsg = (m_editState.target == EditTarget::BaseAddress)
|
|
? fmt::validateBaseAddress(text)
|
|
: fmt::validateValue(m_editState.editKind, text);
|
|
|
|
const LineMeta* lm = metaForLine(m_editState.line);
|
|
const bool isSelected = lm && m_currentSelIds.contains(lm->nodeId);
|
|
const bool isValid = errorMsg.isEmpty();
|
|
|
|
// Only update comment when validation state changes (avoid lag)
|
|
const bool stateChanged = (isValid != m_editState.lastValidationOk);
|
|
m_editState.lastValidationOk = isValid;
|
|
|
|
// Show/hide error marker (red background)
|
|
// M_SELECTED has higher priority than M_ERR, so temporarily remove it when error
|
|
if (isValid) {
|
|
m_sci->markerDelete(m_editState.line, M_ERR);
|
|
if (isSelected) m_sci->markerAdd(m_editState.line, M_SELECTED);
|
|
if (stateChanged) setEditComment("Enter=Save Esc=Cancel");
|
|
} else {
|
|
if (isSelected) m_sci->markerDelete(m_editState.line, M_SELECTED);
|
|
m_sci->markerAdd(m_editState.line, M_ERR);
|
|
if (stateChanged) setEditComment("! " + errorMsg);
|
|
}
|
|
}
|
|
|
|
void RcxEditor::setCommandRowText(const QString& line) {
|
|
if (m_sci->lines() <= 0) return;
|
|
QString s = line;
|
|
s.replace('\n', ' ');
|
|
s.replace('\r', ' ');
|
|
|
|
bool wasReadOnly = m_sci->isReadOnly();
|
|
bool wasModified = m_sci->SendScintilla(QsciScintillaBase::SCI_GETMODIFY);
|
|
long savedPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
|
|
long savedAnchor = m_sci->SendScintilla(QsciScintillaBase::SCI_GETANCHOR);
|
|
|
|
m_sci->setReadOnly(false);
|
|
|
|
long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0);
|
|
long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 0);
|
|
QByteArray utf8 = s.toUtf8();
|
|
long oldLen = end - start;
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, start);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, end);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData());
|
|
|
|
// Adjust saved cursor/anchor for length change in line 0
|
|
long delta = (long)utf8.size() - oldLen;
|
|
if (savedPos > end) savedPos += delta;
|
|
if (savedAnchor > end) savedAnchor += delta;
|
|
|
|
if (wasReadOnly) m_sci->setReadOnly(true);
|
|
if (!wasModified) m_sci->SendScintilla(QsciScintillaBase::SCI_SETSAVEPOINT);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCURRENTPOS, savedPos);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETANCHOR, savedAnchor);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_COLOURISE, start, start + utf8.size());
|
|
applyCommandRowPills();
|
|
}
|
|
|
|
void RcxEditor::setEditorFont(const QString& fontName) {
|
|
g_fontName = fontName;
|
|
QFont f = editorFont();
|
|
|
|
m_sci->setFont(f);
|
|
m_lexer->setFont(f);
|
|
for (int i = 0; i <= 127; i++)
|
|
m_lexer->setFont(f, i);
|
|
m_sci->setMarginsFont(f);
|
|
|
|
// Re-apply margin styles and width with new font metrics
|
|
allocateMarginStyles();
|
|
applyTheme(ThemeManager::instance().current());
|
|
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
|
|
m_sci->setMarginWidth(0, marginSizer);
|
|
}
|
|
|
|
void RcxEditor::setGlobalFontName(const QString& fontName) {
|
|
g_fontName = fontName;
|
|
}
|
|
|
|
QString RcxEditor::globalFontName() {
|
|
return g_fontName;
|
|
}
|
|
|
|
QString RcxEditor::textWithMargins() const {
|
|
int lineCount = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINECOUNT);
|
|
QStringList lines;
|
|
lines.reserve(lineCount);
|
|
for (int i = 0; i < lineCount; i++) {
|
|
QString margin;
|
|
if (i < m_meta.size())
|
|
margin = m_meta[i].offsetText;
|
|
QString lineText = getLineText(m_sci, i);
|
|
lines.append(margin + lineText);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
} // namespace rcx
|