#include "editor.h" #include "disasm.h" #include "providerregistry.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 m_labels; std::function 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 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(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()) { 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()) { 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); // Autocomplete for static field expressions — show field names as user types if (m_editState.target == EditTarget::StaticExpr && !m_staticCompletions.isEmpty()) { // Get word at cursor long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS); long wordStart = m_sci->SendScintilla(QsciScintillaBase::SCI_WORDSTARTPOSITION, pos, (long)1); int wordLen = (int)(pos - wordStart); if (wordLen >= 1) { QByteArray list = m_staticCompletions.join(' ').toUtf8(); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' '); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSHOW, (uintptr_t)wordLen, list.constData()); } } }); connect(m_sci, &QsciScintilla::selectionChanged, this, &RcxEditor::clampEditSelection); } RcxEditor::~RcxEditor() { } void RcxEditor::setupScintilla() { m_sci->setFont(editorFont()); m_sci->setReadOnly(true); m_sci->setWrapMode(QsciScintilla::WrapNone); m_sci->setCaretLineVisible(false); m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 0); // Arrow cursor by default — not the I-beam (this is a structured viewer, not a text editor) m_sci->viewport()->setCursor(Qt::ArrowCursor); m_sci->setTabWidth(2); m_sci->setIndentationsUseTabs(false); // Line spacing for readability m_sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2); m_sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2); // Disable native selection rendering — we use markers for selection m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0); // Horizontal scrollbar: sized explicitly in applyDocument() to match content m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTHTRACKING, 0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH, 1); // Vertical scrollbar: don't allow scrolling past the last line m_sci->SendScintilla(QsciScintillaBase::SCI_SETENDATLASTLINE, 1); // Editable-field indicator - HIDDEN (no visual) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_EDITABLE, 5 /*INDIC_HIDDEN*/); // Hex node dim indicator — overrides text color m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/); // Base address indicator — text color override on command row m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_BASE_ADDR, 17 /*INDIC_TEXTFORE*/); // Hover span indicator — link-like text m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_HOVER_SPAN, 17 /*INDIC_TEXTFORE*/); // Command-row pill background m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_CMD_PILL, 8 /*INDIC_STRAIGHTBOX*/); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETALPHA, IND_CMD_PILL, (long)100); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER, IND_CMD_PILL, (long)1); // Heatmap indicators (cold / warm / hot) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_HEAT_COLD, 17 /*INDIC_TEXTFORE*/); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_HEAT_WARM, 17 /*INDIC_TEXTFORE*/); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_HEAT_HOT, 17 /*INDIC_TEXTFORE*/); // Root class name — type color m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_CLASS_NAME, 17 /*INDIC_TEXTFORE*/); // Green text for hint/comment annotations m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_HINT_GREEN, 17 /*INDIC_TEXTFORE*/); // Local offset text color (dim, like margin text) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_LOCAL_OFF, 17 /*INDIC_TEXTFORE*/); } 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.background, M_CYCLE); m_sci->setMarkerForegroundColor(theme.background, 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& 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& 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& 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& 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& 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 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{lm->nodeIdx} : QSet{}; } QSet 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& 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& 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& meta) { if (meta.isEmpty() || meta[0].lineKind != LineKind::CommandRow) return; clearIndicatorLine(IND_BASE_ADDR, 0); // Override lexer's green number coloring on the address with default text color QString t = getLineText(m_sci, 0); ColumnSpan addr = commandRowAddrSpan(t); if (addr.valid) fillIndicatorCols(IND_BASE_ADDR, 0, addr.start, addr.end); } void RcxEditor::applyCommandRowPills() { if (m_meta.isEmpty() || m_meta[0].lineKind != LineKind::CommandRow) return; constexpr int line = 0; QString t = getLineText(m_sci, line); clearIndicatorLine(IND_HEX_DIM, line); clearIndicatorLine(IND_CLASS_NAME, line); // Dim the [▾] type-selector chevron ColumnSpan chevron = commandRowChevronSpan(t); if (chevron.valid) fillIndicatorCols(IND_HEX_DIM, line, chevron.start, chevron.end); // Dim label text: source arrow/placeholder + its ▾ dropdown arrow ColumnSpan srcSpan = commandRowSrcSpan(t); if (srcSpan.valid) { int quotePos = t.indexOf('\'', srcSpan.start); int kindEnd = (quotePos > srcSpan.start) ? quotePos : srcSpan.end; while (kindEnd > srcSpan.start && t[kindEnd - 1].isSpace()) kindEnd--; if (kindEnd > srcSpan.start) fillIndicatorCols(IND_HEX_DIM, line, srcSpan.start, kindEnd); // Dim the source ▾ dropdown arrow to match (like struct▾) int srcDrop = t.indexOf(QChar(0x25BE)); int rootStart = commandRowRootStart(t); if (srcDrop >= 0 && (rootStart < 0 || srcDrop < rootStart)) fillIndicatorCols(IND_HEX_DIM, line, srcDrop, srcDrop + 1); } // Dim base address to match source/struct grey ColumnSpan addrSpan = commandRowAddrSpan(t); if (addrSpan.valid) fillIndicatorCols(IND_HEX_DIM, line, addrSpan.start, addrSpan.end); // Root class styling (type dim + class-name teal, no underline) ColumnSpan rt = commandRowRootTypeSpan(t); if (rt.valid) { fillIndicatorCols(IND_HEX_DIM, line, rt.start, rt.end); int drop = t.indexOf(QChar(0x25BE), rt.start); if (drop >= 0) fillIndicatorCols(IND_HEX_DIM, line, drop, qMin(drop + 2, t.size())); } ColumnSpan rn = commandRowRootNameSpan(t); if (rn.valid) { fillIndicatorCols(IND_CLASS_NAME, line, rn.start, rn.end); } // Dim trailing opening brace to match the rest of the command row grey for (int i = t.size() - 1; i >= 0; --i) { if (t[i] == ' ' || t[i] == '\t') continue; if (t[i] == '{') fillIndicatorCols(IND_HEX_DIM, line, i, i + 1); break; } } // ── Shared inline-edit shutdown ── RcxEditor::EndEditInfo RcxEditor::endInlineEdit() { // Dismiss any open user list / autocomplete popup m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL); // Clear edit comment and error marker before deactivating if (m_editState.target == EditTarget::Value) { setEditComment({}); // Clear to spaces m_sci->markerDelete(m_editState.line, M_ERR); } EndEditInfo info{m_editState.nodeIdx, m_editState.subLine, m_editState.target}; m_editState.active = false; m_sci->setReadOnly(true); m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 0); // Switch back to Arrow cursor (widget-local, doesn't fight splitters/menus) m_sci->viewport()->setCursor(Qt::ArrowCursor); // Disable selection rendering again m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)1); m_sci->SendScintilla(QsciScintillaBase::SCI_EMPTYUNDOBUFFER); return info; } // ── Span helpers ── // Name span for struct/array headers - uses column-based positioning // Format: [fold][indent][type col][sep][name col][sep][suffix] static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) { if (lm.lineKind != LineKind::Header) return {}; int ind = kFoldCol + lm.depth * 3; int typeW = lm.effectiveTypeW; int nameStart = ind + typeW + kSepWidth; if (nameStart >= lineText.size()) return {}; // Name ends before " {" suffix (expanded) or at line end (collapsed) int nameEnd = lineText.size(); if (lineText.endsWith(QStringLiteral(" {"))) nameEnd = lineText.size() - 2; if (nameEnd <= nameStart) return {}; // Don't allow editing array element names like "[0]", "[1]", etc. QString name = lineText.mid(nameStart, nameEnd - nameStart).trimmed(); if (name.isEmpty()) return {}; if (name.startsWith('[') && name.endsWith(']')) return {}; return {nameStart, nameEnd, true}; } // Type name span for struct headers (not arrays) // Named structs format as: "_MMPTE OriginalPte {" (type column = just the name) // Anonymous structs format as: "union {" or "struct {" (no clickable type) static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText) { if (lm.lineKind != LineKind::Header) return {}; if (lm.isArrayHeader) return {}; int ind = kFoldCol + lm.depth * 3; int typeW = lm.effectiveTypeW; int typeEnd = ind + typeW; if (typeEnd > lineText.size()) typeEnd = lineText.size(); QString typeCol = lineText.mid(ind, typeEnd - ind).trimmed(); if (typeCol.isEmpty()) return {}; // Anonymous structs use bare keywords — not clickable static const QStringList kKeywords = { QStringLiteral("struct"), QStringLiteral("union"), QStringLiteral("class") }; if (kKeywords.contains(typeCol)) return {}; // Static field headers: "static hex64 target {" — skip "static " prefix if (lm.isStaticLine) { int cursor = ind; while (cursor < typeEnd && lineText[cursor] == ' ') cursor++; if (lineText.mid(cursor, 7) == QLatin1String("static ")) cursor += 7; while (cursor < typeEnd && lineText[cursor] == ' ') cursor++; int tStart = cursor; while (cursor < typeEnd && lineText[cursor] != ' ') cursor++; if (cursor > tStart) return {tStart, cursor, true}; return {}; } // Named struct: entire type column is the type name (e.g. "_MMPTE") // Find the actual text bounds within the padded column int start = ind; while (start < typeEnd && lineText[start] == ' ') start++; int end = start; while (end < typeEnd && lineText[end] != ' ') end++; if (end <= start) return {}; return {start, end, true}; } // Type span for array headers: "int32_t[10]" in "int32_t[10] positions {" static ColumnSpan arrayHeaderTypeSpan(const LineMeta& lm, const QString& lineText) { if (lm.lineKind != LineKind::Header || !lm.isArrayHeader) return {}; int ind = kFoldCol + lm.depth * 3; int typeEnd = lineText.indexOf(' ', ind); if (typeEnd <= ind) return {}; return {ind, typeEnd, true}; } RcxEditor::NormalizedSpan RcxEditor::normalizeSpan( const ColumnSpan& raw, const QString& lineText, EditTarget target, bool skipPrefixes) const { if (!raw.valid) return {}; int textLen = lineText.size(); if (raw.start >= textLen) return {}; int start = raw.start; int end = qMin(raw.end, textLen); if (end <= start) return {}; if (skipPrefixes && target == EditTarget::Value) { QString spanText = lineText.mid(start, end - start); int arrow = spanText.indexOf(QStringLiteral("->")); if (arrow >= 0) { int i = arrow + 2; while (i < spanText.size() && spanText[i].isSpace()) i++; start += i; } else { int eq = spanText.indexOf('='); if (eq >= 0 && eq <= 3) { int i = eq + 1; while (i < spanText.size() && spanText[i].isSpace()) i++; start += i; } } if (start >= end) return {}; } QString inner = lineText.mid(start, end - start); int lead = 0; while (lead < inner.size() && inner[lead].isSpace()) lead++; int trail = inner.size(); while (trail > lead && inner[trail - 1].isSpace()) trail--; if (trail <= lead) return {}; return {start + lead, start + trail, true}; } bool RcxEditor::resolvedSpanFor(int line, EditTarget t, NormalizedSpan& out, QString* lineTextOut) const { const LineMeta* lm = metaForLine(line); if (!lm) return false; // CommandRow: Source / BaseAddress / Root class (type+name) editing if (lm->lineKind == LineKind::CommandRow) { if (t != EditTarget::BaseAddress && t != EditTarget::Source && t != EditTarget::RootClassType && t != EditTarget::RootClassName && t != EditTarget::TypeSelector) return false; QString lineText = getLineText(m_sci, line); ColumnSpan s; if (t == EditTarget::TypeSelector) s = commandRowChevronSpan(lineText); else if (t == EditTarget::Source) s = commandRowSrcSpan(lineText); else if (t == EditTarget::BaseAddress) s = commandRowAddrSpan(lineText); else if (t == EditTarget::RootClassType) s = commandRowRootTypeSpan(lineText); else s = commandRowRootNameSpan(lineText); out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/(t == EditTarget::BaseAddress)); if (lineTextOut) *lineTextOut = lineText; return out.valid; } if (lm->nodeIdx < 0) return false; // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only) // Exception: static field names are always editable (they're function names) if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind) && !lm->isStaticLine) return false; QString lineText = getLineText(m_sci, line); int textLen = lineText.size(); // Use per-line effective widths (set during compose based on containing scope) int typeW = lm->effectiveTypeW; int nameW = lm->effectiveNameW; ColumnSpan s; switch (t) { case EditTarget::Type: s = typeSpan(*lm, typeW); break; case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break; case EditTarget::Value: s = narrowPtrValueSpan(*lm, valueSpan(*lm, textLen, typeW, nameW), lineText); break; case EditTarget::BaseAddress: break; // No longer on header lines case EditTarget::ArrayIndex: case EditTarget::ArrayCount: break; // Array navigation removed case EditTarget::ArrayElementType: s = arrayElemTypeSpanFor(*lm, lineText); break; case EditTarget::ArrayElementCount: s = arrayElemCountSpanFor(*lm, lineText); break; case EditTarget::PointerTarget: s = pointerTargetSpanFor(*lm, lineText); break; case EditTarget::StaticExpr: if (lm->isStaticLine) s = staticExprSpanFor(*lm, lineText); break; case EditTarget::Source: break; } // Fallback spans for header lines if (!s.valid && t == EditTarget::Type) { // For pointer fields, the full type span acts as "kind" span // For array headers, fall back to the full type[count] span s = arrayHeaderTypeSpan(*lm, lineText); if (!s.valid) s = headerTypeNameSpan(*lm, lineText); if (!s.valid) s = pointerKindSpanFor(*lm, lineText); } if (!s.valid && t == EditTarget::Name) s = headerNameSpan(*lm, lineText); // Member lines: override Name/Value spans if (!s.valid && lm->isMemberLine) { if (t == EditTarget::Name) s = memberNameSpanFor(*lm, lineText); if (t == EditTarget::Value) s = memberValueSpanFor(*lm, lineText); } out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/true); if (lineTextOut) *lineTextOut = lineText; return out.valid; } // ── Point → line/col/nodeId resolution ── RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const { HitInfo h; // Try precise position first (works when cursor is over actual text) long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, (unsigned long)vp.x(), (long)vp.y()); if (pos >= 0) { h.line = (int)m_sci->SendScintilla( QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); h.col = (int)m_sci->SendScintilla( QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos); } else { // Fallback: calculate line from Y coordinate (for empty space past text) int firstVisible = (int)m_sci->SendScintilla( QsciScintillaBase::SCI_GETFIRSTVISIBLELINE); int lineHeight = (int)m_sci->SendScintilla( QsciScintillaBase::SCI_TEXTHEIGHT, 0); if (lineHeight > 0) h.line = firstVisible + vp.y() / lineHeight; } if (h.line >= 0 && h.line < m_meta.size()) { h.nodeId = m_meta[h.line].nodeId; h.inFoldCol = (h.col >= 0 && h.col < kFoldCol + 1 && m_meta[h.line].foldHead); } return h; } // ── Double-click hit test ── static bool hitTestTarget(QsciScintilla* sci, const QVector& 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(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(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(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(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(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(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(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(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(m_historyPopup)->dismiss(); if (m_structPreviewPopup) static_cast(m_structPreviewPopup)->dismiss(); // Clear editable-token color hints (de-emphasize non-active tokens) clearIndicatorLine(IND_EDITABLE, m_hintLine); m_hintLine = -1; if (line >= 0) { m_sci->setCursorPosition(line, col >= 0 ? col : 0); } if (col < 0) { m_sci->getCursorPosition(&line, &col); } auto* lm = metaForLine(line); if (!lm) return false; // Allow nodeIdx=-1 only for CommandRow editing (command bar) if (lm->nodeIdx < 0 && !(lm->lineKind == LineKind::CommandRow && (target == EditTarget::BaseAddress || target == EditTarget::Source || target == EditTarget::RootClassType || target == EditTarget::RootClassName))) return false; // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only) // Exception: static field names are always editable (they're function names, not hex labels) if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind) && !lm->isStaticLine) return false; QString lineText; NormalizedSpan norm; if (!resolvedSpanFor(line, target, norm, &lineText)) return false; QString trimmed = lineText.mid(norm.start, norm.end - norm.start); int vecComponent = 0; // which vector/matrix component // Helper: parse comma-separated components, narrow span to clicked one auto narrowToComponent = [&](const QString& inner, int innerAbsStart) { QVector 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(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(m_historyPopup)->dismiss(); } // Always dismiss disasm/preview popups during inline editing if (m_disasmPopup && m_disasmPopup->isVisible()) static_cast(m_disasmPopup)->dismiss(); if (m_structPreviewPopup && m_structPreviewPopup->isVisible()) static_cast(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(m_historyPopup)->dismiss(); if (m_disasmPopup && !m_applyingDocument) static_cast(m_disasmPopup)->dismiss(); if (m_structPreviewPopup && !m_applyingDocument) static_cast(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 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(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(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( 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(m_historyPopup)->dismiss(); } } } } } } } if (!showDisasm && m_disasmPopup && m_disasmPopup->isVisible()) static_cast(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(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(m_historyPopup)->dismiss(); } } } } } if (!showPreview && m_structPreviewPopup && m_structPreviewPopup->isVisible()) static_cast(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 "//" (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); // Suppress modification notifications during replace to avoid // QScintilla accessibility crash (textDeleted called with null text). long savedMask = m_sci->SendScintilla(QsciScintillaBase::SCI_GETMODEVENTMASK); m_sci->SendScintilla(QsciScintillaBase::SCI_SETMODEVENTMASK, 0); 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()); m_sci->SendScintilla(QsciScintillaBase::SCI_SETMODEVENTMASK, savedMask); // 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