mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
- Enum inline editing: name/value commit handling, auto-sort by value - Bitfield support in PDB import with proper container nodes - Per-member hover/selection highlighting (kMemberBit encoding) - Context menu fixes for enum/bitfield member lines - MCP pagination (limit/offset), includeMembers param, tree.search tool - MCP status bar activity indicator for tool calls - PDB anonymous type inlining: inline <unnamed-tag> types as children - Skip anonymous pointer targets to prevent root orphans - Enum import diagnostics for debugging missing enums
3298 lines
137 KiB
C++
3298 lines
137 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 <QScreen>
|
|
#include <QScrollBar>
|
|
#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;
|
|
public:
|
|
explicit ValueHistoryPopup(QWidget* parent)
|
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
|
{
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
|
setFrameShape(QFrame::NoFrame);
|
|
setAutoFillBackground(true);
|
|
}
|
|
|
|
uint64_t nodeId() const { return m_nodeId; }
|
|
void setOnSet(std::function<void(const QString&)> fn) { m_onSet = std::move(fn); }
|
|
|
|
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);
|
|
|
|
for (const QString& v : vals) {
|
|
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 (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) {
|
|
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() - 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;
|
|
public:
|
|
explicit DisasmPopup(QWidget* parent)
|
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
|
{
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
setAttribute(Qt::WA_ShowWithoutActivating, 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 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) {
|
|
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() - 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;
|
|
public:
|
|
explicit StructPreviewPopup(QWidget* parent)
|
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
|
{
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
setAttribute(Qt::WA_ShowWithoutActivating, 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 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) {
|
|
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() - 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 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);
|
|
|
|
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);
|
|
|
|
// 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);
|
|
});
|
|
|
|
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*/);
|
|
|
|
}
|
|
|
|
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) {
|
|
// Paper and text
|
|
m_sci->setPaper(theme.background);
|
|
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);
|
|
|
|
// 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(theme.background, i);
|
|
|
|
// Margins
|
|
m_sci->setMarginsBackgroundColor(theme.background);
|
|
m_sci->setMarginsForegroundColor(theme.textFaint);
|
|
m_sci->setFoldMarginColors(theme.background, theme.background);
|
|
|
|
// Markers
|
|
m_sci->setMarkerBackgroundColor(theme.markerPtr, M_PTR0);
|
|
m_sci->setMarkerForegroundColor(theme.markerPtr, M_PTR0);
|
|
m_sci->setMarkerBackgroundColor(theme.markerCycle, M_CYCLE);
|
|
m_sci->setMarkerForegroundColor(theme.markerCycle, M_CYCLE);
|
|
m_sci->setMarkerBackgroundColor(theme.markerError, M_ERR);
|
|
m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR);
|
|
m_sci->setMarkerBackgroundColor(theme.background, 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(theme.background, 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, theme.background);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = line.size();
|
|
while (len > 0 && line[len - 1] == QChar(' ')) --len;
|
|
if (len > maxLen) maxLen = len;
|
|
}
|
|
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
|
|
m_hoveredNodeId = savedHoverId;
|
|
m_hoveredLine = savedHoverLine;
|
|
m_hoverInside = savedHoverInside;
|
|
m_applyingDocument = false;
|
|
|
|
// 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();
|
|
}
|
|
|
|
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 ──
|
|
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::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) 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)
|
|
// Format: "struct TYPENAME NAME {" or collapsed variants
|
|
// For "struct NAME {" (no typename), returns invalid span
|
|
static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText) {
|
|
if (lm.lineKind != LineKind::Header) return {};
|
|
if (lm.isArrayHeader) return {}; // Arrays use arrayHeaderTypeSpan instead
|
|
|
|
int ind = kFoldCol + lm.depth * 3;
|
|
int typeW = lm.effectiveTypeW;
|
|
int typeEnd = ind + typeW;
|
|
|
|
// Clamp to actual line content
|
|
if (typeEnd > lineText.size()) typeEnd = lineText.size();
|
|
|
|
// Extract the type column text and check if it has a typename
|
|
// Format: "struct" or "struct TYPENAME"
|
|
QString typeCol = lineText.mid(ind, typeEnd - ind).trimmed();
|
|
|
|
// Find first space (after "struct")
|
|
int firstSpace = typeCol.indexOf(' ');
|
|
if (firstSpace < 0) return {}; // Just "struct", no typename
|
|
|
|
// If there's content after "struct ", that's the typename
|
|
QString typename_ = typeCol.mid(firstSpace + 1).trimmed();
|
|
if (typename_.isEmpty()) return {};
|
|
|
|
// Return span of the typename within the type column
|
|
int typenameStart = ind + firstSpace + 1;
|
|
// Find where the typename actually ends (skip padding)
|
|
int typenameEnd = typenameStart;
|
|
while (typenameEnd < typeEnd && lineText[typenameEnd] != ' ')
|
|
typenameEnd++;
|
|
|
|
return {typenameStart, typenameEnd, 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)
|
|
if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind))
|
|
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::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);
|
|
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) {
|
|
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_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)
|
|
if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind))
|
|
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;
|
|
}
|
|
|
|
// Disable Scintilla undo during inline edit
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0);
|
|
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);
|
|
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);
|
|
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);
|
|
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);
|
|
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);
|
|
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);
|
|
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);
|
|
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);
|
|
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->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 0);
|
|
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);
|
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 1);
|
|
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
|