#include "editor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace rcx { // ── Theme constants ── static const QColor kBgText("#1e1e1e"); static const QColor kBgMargin("#252526"); static const QColor kFgMargin("#858585"); static const QColor kFgMarginDim("#505050"); static constexpr int IND_EDITABLE = 8; static constexpr int IND_HEX_DIM = 9; static constexpr int IND_BASE_ADDR = 10; // Green color for base address static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like) static QString g_fontName = "Consolas"; 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(); m_sci->installEventFilter(this); m_sci->viewport()->installEventFilter(this); m_sci->viewport()->setMouseTracking(true); // 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) { int line = m_sci->lineAt(pos); 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 (id == 1 && m_editState.active && m_editState.target == EditTarget::Type) { auto info = endInlineEdit(); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text); } }); 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_editState.target == EditTarget::Value) QTimer::singleShot(0, this, &RcxEditor::validateEditLive); if (m_editState.target == EditTarget::Type) QTimer::singleShot(0, this, &RcxEditor::updateTypeListFilter); }); connect(m_sci, &QsciScintilla::selectionChanged, this, &RcxEditor::clampEditSelection); } 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->setPaper(kBgText); m_sci->setColor(QColor("#d4d4d4")); m_sci->setTabWidth(2); m_sci->setIndentationsUseTabs(false); // Caret color for dark theme m_sci->setCaretForegroundColor(QColor("#d4d4d4")); // 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); // Editable-field indicator - set to HIDDEN (no visual, avoids INDIC_PLAIN underline) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_EDITABLE, 5 /*INDIC_HIDDEN*/); // Hex/Padding node dim indicator — overrides text color to gray m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_HEX_DIM, QColor("#505050")); // Base address indicator — green like comments m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_BASE_ADDR, 17 /*INDIC_TEXTFORE*/); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_BASE_ADDR, QColor("#6a9955")); // Hover span indicator — blue text like a link m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_HOVER_SPAN, 17 /*INDIC_TEXTFORE*/); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_HOVER_SPAN, QColor("#569cd6")); } void RcxEditor::setupLexer() { m_lexer = new QsciLexerCPP(m_sci); QFont font = editorFont(); m_lexer->setFont(font); // Dark theme colors m_lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword); m_lexer->setColor(QColor("#569cd6"), QsciLexerCPP::KeywordSet2); m_lexer->setColor(QColor("#b5cea8"), QsciLexerCPP::Number); m_lexer->setColor(QColor("#ce9178"), QsciLexerCPP::DoubleQuotedString); m_lexer->setColor(QColor("#ce9178"), QsciLexerCPP::SingleQuotedString); m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::Comment); m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine); m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc); m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Default); m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Identifier); m_lexer->setColor(QColor("#c586c0"), QsciLexerCPP::PreProcessor); m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator); // Dark background for all styles for (int i = 0; i <= 127; i++) { m_lexer->setPaper(kBgText, i); m_lexer->setFont(font, i); } m_sci->setLexer(m_lexer); m_sci->setBraceMatching(QsciScintilla::SloppyBraceMatch); // Add type names to keyword set 2 → teal coloring (distinct from identifiers) QByteArray kw2 = allTypeNamesForUI(/*stripBrackets=*/true).join(' ').toLatin1(); m_sci->SendScintilla(QsciScintillaBase::SCI_SETKEYWORDS, (uintptr_t)1, kw2.constData()); } void RcxEditor::setupMargins() { m_sci->setMarginsFont(editorFont()); // Margin 0: Offset text m_sci->setMarginType(0, QsciScintilla::TextMarginRightJustified); m_sci->setMarginWidth(0, " +0x00000000 "); m_sci->setMarginsBackgroundColor(kBgMargin); m_sci->setMarginsForegroundColor(kFgMarginDim); m_sci->setMarginSensitivity(0, true); // Margin 1: hidden (fold chevrons moved to text column) m_sci->setMarginWidth(1, 0); } void RcxEditor::setupFolding() { // Hide fold margin (fold indicators are text-based now) m_sci->setMarginWidth(2, 0); m_sci->setFoldMarginColors(kBgMargin, kBgMargin); // 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_PAD (1): padding line (metadata only, no visual) m_sci->markerDefine(QsciScintilla::Invisible, M_PAD); // M_PTR0 (2): right triangle (red) m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0); m_sci->setMarkerBackgroundColor(QColor("#f44747"), M_PTR0); m_sci->setMarkerForegroundColor(QColor("#f44747"), M_PTR0); // M_CYCLE (3): arrows (orange) m_sci->markerDefine(QsciScintilla::ThreeRightArrows, M_CYCLE); m_sci->setMarkerBackgroundColor(QColor("#e5a00d"), M_CYCLE); m_sci->setMarkerForegroundColor(QColor("#e5a00d"), M_CYCLE); // M_ERR (4): background (dark red - brightened for visibility) m_sci->markerDefine(QsciScintilla::Background, M_ERR); m_sci->setMarkerBackgroundColor(QColor("#7a2e2e"), M_ERR); m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR); // M_STRUCT_BG (5): struct header/footer (matches regular bg, may remove later) m_sci->markerDefine(QsciScintilla::Background, M_STRUCT_BG); m_sci->setMarkerBackgroundColor(QColor("#1e1e1e"), M_STRUCT_BG); m_sci->setMarkerForegroundColor(QColor("#d4d4d4"), M_STRUCT_BG); // M_HOVER (6): full-row hover highlight m_sci->markerDefine(QsciScintilla::Background, M_HOVER); m_sci->setMarkerBackgroundColor(QColor(43, 43, 43), M_HOVER); // M_SELECTED (7): full-row selection highlight (higher = wins over hover) m_sci->markerDefine(QsciScintilla::Background, M_SELECTED); m_sci->setMarkerBackgroundColor(QColor(35, 35, 35), M_SELECTED); } 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); const long bgrMargin = 0x262525; // BGR for #252526 QByteArray fontName = editorFont().family().toUtf8(); int fontSize = editorFont().pointSize(); // Margin styles (dim gray text) for (int s = MSTYLE_NORMAL; s <= MSTYLE_CONT; s++) { unsigned long abs = (unsigned long)(base + s); m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE, abs, (long)0x505050); m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK, abs, bgrMargin); m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFONT, (uintptr_t)abs, fontName.constData()); m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETSIZE, abs, (long)fontSize); } } void RcxEditor::applyDocument(const ComposeResult& result) { // Silently deactivate inline edit (no signal — refresh is already happening) if (m_editState.active) endInlineEdit(); m_meta = result.meta; m_layout = result.layout; m_sci->setReadOnly(false); m_sci->setText(result.text); m_sci->setReadOnly(true); // 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); applyBaseAddressColoring(result.meta); // Reset hint line - applySelectionOverlay will repaint indicators m_hintLine = -1; } void RcxEditor::applyMarginText(const QVector& meta) { // Clear all margin text 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::applyMarkers(const QVector& meta) { for (int m = M_CONT; m <= M_STRUCT_BG; m++) { m_sci->markerDeleteAll(m); } for (int i = 0; i < meta.size(); i++) { 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++) { 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); } } } void RcxEditor::applySelectionOverlay(const QSet& selIds) { m_currentSelIds = selIds; m_sci->markerDeleteAll(M_SELECTED); // 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); for (int i = 0; i < m_meta.size(); i++) { if (selIds.contains(m_meta[i].nodeId)) { m_sci->markerAdd(i, M_SELECTED); paintEditableSpans(i); } } // Reset hint line - updateEditableIndicators will handle cursor hints // on actual user navigation (not stale restored positions) m_hintLine = -1; applyHoverHighlight(); } void RcxEditor::applyHoverHighlight() { m_sci->markerDeleteAll(M_HOVER); if (m_editState.active) return; if (!m_hoverInside) return; if (m_hoveredNodeId == 0) return; if (m_currentSelIds.contains(m_hoveredNodeId)) return; for (int i = 0; i < m_meta.size(); i++) { if (m_meta[i].nodeId == m_hoveredNodeId) m_sci->markerAdd(i, 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; 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); } 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; } // ── Column span computation ── ColumnSpan RcxEditor::typeSpan(const LineMeta& lm) { return typeSpanFor(lm); } ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int nameW) { return nameSpanFor(lm, nameW); } ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int nameW) { return valueSpanFor(lm, lineLength, nameW); } // ── 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::applyBaseAddressColoring(const QVector& meta) { m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_BASE_ADDR); for (int i = 0; i < meta.size(); i++) { const LineMeta& lm = meta[i]; if (!lm.isRootHeader) continue; QString lineText = getLineText(m_sci, i); ColumnSpan span = baseAddressFullSpanFor(lm, lineText); if (!span.valid) continue; long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)i); long posA = lineStart + span.start; long posB = lineStart + span.end; if (posB > posA) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA); } } // ── Shared inline-edit shutdown ── RcxEditor::EndEditInfo RcxEditor::endInlineEdit() { // 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 from I-beam to Arrow (keep override active to block Scintilla's cursor) if (m_cursorOverridden) { QApplication::changeOverrideCursor(Qt::ArrowCursor); } else { QApplication::setOverrideCursor(Qt::ArrowCursor); m_cursorOverridden = true; } // 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 ── static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) { if (lm.lineKind != LineKind::Header) return {}; int bracePos = lineText.lastIndexOf(QStringLiteral(" {")); if (bracePos <= 0) return {}; int ind = kFoldCol + lm.depth * 3; int typeEnd = lineText.indexOf(' ', ind); if (typeEnd <= ind || typeEnd >= bracePos) return {}; return {typeEnd + 1, bracePos, 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 || lm->nodeIdx < 0) return false; QString lineText = getLineText(m_sci, line); int textLen = lineText.size(); ColumnSpan s; switch (t) { case EditTarget::Type: s = typeSpan(*lm); break; case EditTarget::Name: s = nameSpan(*lm, m_layout.nameW); break; case EditTarget::Value: s = valueSpan(*lm, textLen, m_layout.nameW); break; case EditTarget::BaseAddress: s = baseAddressSpanFor(*lm, lineText); break; } if (!s.valid && t == EditTarget::Name) s = headerNameSpan(*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 && m_meta[h.line].foldHead); } return h; } // ── Double-click hit test ── static bool hitTestTarget(QsciScintilla* sci, const QVector& meta, const QPoint& viewportPos, int& outLine, EditTarget& outTarget, int nameW = kColName) { 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); if (line < 0 || line >= meta.size()) return false; QString lineText = getLineText(sci, line); int textLen = lineText.size(); const LineMeta& lm = meta[line]; ColumnSpan ts = RcxEditor::typeSpan(lm); ColumnSpan ns = RcxEditor::nameSpan(lm, nameW); ColumnSpan vs = RcxEditor::valueSpan(lm, textLen, nameW); ColumnSpan bs = baseAddressSpanFor(lm, lineText); // Base address for root headers if (!ns.valid) ns = headerNameSpan(lm, lineText); auto inSpan = [&](const ColumnSpan& s) { return s.valid && col >= s.start && col < s.end; }; if (inSpan(bs)) outTarget = EditTarget::BaseAddress; else if (inSpan(ts)) outTarget = EditTarget::Type; else if (inSpan(ns)) outTarget = EditTarget::Name; else if (inSpan(vs)) outTarget = EditTarget::Value; else 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); return m_editState.active ? handleEditKey(ke) : handleNormalKey(ke); } 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); ColumnSpan raw; switch (m_editState.target) { case EditTarget::Type: raw = typeSpan(*lm); break; case EditTarget::Name: raw = nameSpan(*lm, m_layout.nameW); break; case EditTarget::Value: raw = valueSpan(*lm, lineText.size(), m_layout.nameW); break; case EditTarget::BaseAddress: raw = baseAddressSpanFor(*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(); // stale — normal handler will re-establish // Fall through to normal click handler below } // 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) { m_hoveredNodeId = newHoverId; applyHoverHighlight(); } if (h.inFoldCol) { emit marginClicked(0, h.line, me->modifiers()); return true; } 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 if (alreadySelected && plain) { int tLine; EditTarget t; if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t, m_layout.nameW)) { m_pendingClickNodeId = 0; return beginInlineEdit(t, tLine); } } 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; } } } } // 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 false; // not yet a drag, 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; } } // 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; EditTarget t; if (hitTestTarget(m_sci, m_meta, me->pos(), line, t, m_layout.nameW)) { m_pendingClickNodeId = 0; // cancel deferred selection change return beginInlineEdit(t, line); } } 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); } if (obj == m_sci->viewport() && !m_editState.active) { if (event->type() == QEvent::MouseMove) { m_lastHoverPos = static_cast(event)->pos(); m_hoverInside = true; } else if (event->type() == QEvent::Leave) { m_hoverInside = false; m_hoveredNodeId = 0; 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 if (event->type() == QEvent::MouseMove || event->type() == QEvent::Wheel) { auto h = hitTest(m_lastHoverPos); uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0; if (newHoverId != m_hoveredNodeId) { m_hoveredNodeId = newHoverId; applyHoverHighlight(); } } if (event->type() == QEvent::MouseMove || event->type() == QEvent::Leave || event->type() == QEvent::Wheel) applyHoverCursor(); } 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); 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: case Qt::Key_Tab: 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: return true; // block to prevent eating trailing content case Qt::Key_Left: case Qt::Key_Backspace: { int line, col; m_sci->getCursorPosition(&line, &col); if (col <= m_editState.spanStart) return true; return false; } case Qt::Key_Right: { int line, col; m_sci->getCursorPosition(&line, &col); if (col >= editEndCol()) return true; // block past end return false; } case Qt::Key_Home: m_sci->setCursorPosition(m_editState.line, m_editState.spanStart); return true; case Qt::Key_End: m_sci->setCursorPosition(m_editState.line, editEndCol()); return true; default: return false; } } // ── Begin inline edit ── bool RcxEditor::beginInlineEdit(EditTarget target, int line) { if (m_editState.active) return false; m_hoveredNodeId = 0; applyHoverHighlight(); // 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, 0); } int col; m_sci->getCursorPosition(&line, &col); auto* lm = metaForLine(line); if (!lm || lm->nodeIdx < 0) return false; QString lineText; NormalizedSpan norm; if (!resolvedSpanFor(line, target, norm, &lineText)) return false; QString trimmed = lineText.mid(norm.start, norm.end - norm.start); 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 ((lm->nodeKind == NodeKind::Vec2 || lm->nodeKind == NodeKind::Vec3 || lm->nodeKind == NodeKind::Vec4) && lm->subLine >= 0) m_editState.editKind = NodeKind::Float; // Store fixed comment column position for value editing if (target == EditTarget::Value) { ColumnSpan cs = commentSpanFor(*lm, lineText.size(), m_layout.nameW); 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); // Switch to I-beam for editing (skip for Type which uses dropdown picker) if (target != EditTarget::Type) { if (m_cursorOverridden) { QApplication::changeOverrideCursor(Qt::IBeamCursor); } else { QApplication::setOverrideCursor(Qt::IBeamCursor); m_cursorOverridden = true; } } // Re-enable selection rendering for inline edit m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1, QColor("#264f78")); // 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")); if (target == EditTarget::Type) QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete); 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; static bool s_clamping = false; if (s_clamping) return; s_clamping = 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) { s_clamping = 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); s_clamping = 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); s_clamping = 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; auto info = endInlineEdit(); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText); } // ── Cancel inline edit ── void RcxEditor::cancelInlineEdit() { if (!m_editState.active) return; endInlineEdit(); emit inlineEditCancelled(); } // ── Type picker (user list) ── void RcxEditor::showTypeAutocomplete() { // 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) return; // Filter type names by prefix QStringList all = allTypeNamesForUI(); 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(' ').toUtf8(); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' '); m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW, (uintptr_t)1, list.constData()); // Arrow cursor for popup is handled by applyHoverCursor() via isListActive() } void RcxEditor::updateTypeListFilter() { if (!m_editState.active || m_editState.target != EditTarget::Type) return; // Get currently typed text from line QString lineText = getLineText(m_sci, m_editState.line); long curPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS); long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)m_editState.line); int col = (int)(curPos - lineStart); // 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); } // ── Editable-field text-color indicator ── void RcxEditor::paintEditableSpans(int line) { NormalizedSpan norm; for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value}) { 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; } // 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 (newLm && m_currentSelIds.contains(newLm->nodeId)) { if (m_hintLine >= 0) { const LineMeta* oldLm = metaForLine(m_hintLine); if (!oldLm || !m_currentSelIds.contains(oldLm->nodeId)) 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 (!oldLm || !m_currentSelIds.contains(oldLm->nodeId)) clearIndicatorLine(IND_EDITABLE, m_hintLine); } m_hintLine = line; paintEditableSpans(line); } // ── Hover cursor ── void RcxEditor::applyHoverCursor() { // Clear previous hover span indicator if (m_hoverSpanLine >= 0) { clearIndicatorLine(IND_HOVER_SPAN, m_hoverSpanLine); m_hoverSpanLine = -1; } // Edit mode handles its own cursor (I-beam) if (m_editState.active) return; // Mouse left viewport - set Arrow if (!m_hoverInside || !m_sci->viewport()->underMouse()) { if (!m_cursorOverridden) { QApplication::setOverrideCursor(Qt::ArrowCursor); m_cursorOverridden = true; } else { QApplication::changeOverrideCursor(Qt::ArrowCursor); } return; } // If autocomplete/user list popup is active, use arrow cursor if (m_sci->isListActive()) { if (!m_cursorOverridden) { QApplication::setOverrideCursor(Qt::ArrowCursor); m_cursorOverridden = true; } else { QApplication::changeOverrideCursor(Qt::ArrowCursor); } return; } int line; EditTarget t; bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t, m_layout.nameW); // Apply hover span indicator (blue text like a link) if (tokenHit) { NormalizedSpan span; if (resolvedSpanFor(line, t, span)) { fillIndicatorCols(IND_HOVER_SPAN, line, span.start, span.end); m_hoverSpanLine = line; } } // Also show pointer cursor for fold column on fold-head lines bool interactive = tokenHit; if (!interactive) { auto h = hitTest(m_lastHoverPos); if (h.inFoldCol) interactive = true; } // Set cursor: pointing hand for interactive, arrow otherwise Qt::CursorShape desired = interactive ? Qt::PointingHandCursor : Qt::ArrowCursor; if (!m_cursorOverridden) { QApplication::setOverrideCursor(desired); m_cursorOverridden = true; } else { QApplication::changeOverrideCursor(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 static bool s_updating = false; if (s_updating) return; s_updating = 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) { s_updating = false; return; } // Format as "//" (no space after //) QString formatted = QStringLiteral("//") + comment; QString padded = formatted.leftJustified(availWidth, ' ').left(availWidth); // Use direct position calculation from line start long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)m_editState.line); long posA = lineStart + startCol; long posB = lineStart + 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 (reuse IND_BASE_ADDR which is green) m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_BASE_ADDR); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA); s_updating = 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 = 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::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 with new font allocateMarginStyles(); } void RcxEditor::setGlobalFontName(const QString& fontName) { g_fontName = fontName; } } // namespace rcx