From 2c0090202071ab654f5afec5c3257ea8b7b64ed6 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Tue, 3 Feb 2026 08:34:31 -0700 Subject: [PATCH] Fix mouse event sync: hover on click, drag threshold, indicator/selection alignment --- src/controller.cpp | 45 ++++++++- src/core.h | 45 +++++++-- src/editor.cpp | 232 ++++++++++++++++++++++++++------------------- src/editor.h | 7 +- src/format.cpp | 57 +++++++---- 5 files changed, 257 insertions(+), 129 deletions(-) diff --git a/src/controller.cpp b/src/controller.cpp index 0643841..0df119b 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -155,7 +155,6 @@ void RcxController::connectEditor(RcxEditor* editor) { void RcxController::refresh() { m_lastResult = m_doc->compose(); - qDebug() << "refresh() called, text length:" << m_lastResult.text.size(); // Prune stale selections (nodes removed by undo/redo/delete) QSet valid; @@ -265,15 +264,36 @@ void RcxController::insertNode(uint64_t parentId, int offset, NodeKind kind, con void RcxController::removeNode(int nodeIdx) { if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; - uint64_t nodeId = m_doc->tree.nodes[nodeIdx].id; + const Node& node = m_doc->tree.nodes[nodeIdx]; + uint64_t nodeId = node.id; + uint64_t parentId = node.parentId; + // Compute size of deleted node/subtree + int deletedSize = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) + ? m_doc->tree.structSpan(node.id) : node.byteSize(); + int deletedEnd = node.offset + deletedSize; + + // Find siblings after this node and compute offset adjustments + QVector adjs; + if (parentId != 0) { // only adjust if not root-level + auto siblings = m_doc->tree.childrenOf(parentId); + for (int si : siblings) { + if (si == nodeIdx) continue; + auto& sib = m_doc->tree.nodes[si]; + if (sib.offset >= deletedEnd) { + adjs.append({sib.id, sib.offset, sib.offset - deletedSize}); + } + } + } + + // Collect subtree QVector indices = m_doc->tree.subtreeIndices(nodeId); QVector subtree; for (int i : indices) subtree.append(m_doc->tree.nodes[i]); m_doc->undoStack.push(new RcxCommand(this, - cmd::Remove{nodeId, subtree})); + cmd::Remove{nodeId, subtree, adjs})); } void RcxController::toggleCollapse(int nodeIdx) { @@ -316,13 +336,23 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { tree.addNode(c.node); } } else if constexpr (std::is_same_v) { - qDebug() << "applyCommand Remove, isUndo:" << isUndo << "nodeId:" << c.nodeId; if (isUndo) { + // Restore nodes first for (const Node& n : c.subtree) tree.addNode(n); + // Revert offset adjustments + for (const auto& adj : c.offAdjs) { + int ai = tree.indexOfId(adj.nodeId); + if (ai >= 0) tree.nodes[ai].offset = adj.oldOffset; + } } else { + // Apply offset adjustments first (before removing changes indices) + for (const auto& adj : c.offAdjs) { + int ai = tree.indexOfId(adj.nodeId); + if (ai >= 0) tree.nodes[ai].offset = adj.newOffset; + } + // Remove nodes QVector indices = tree.subtreeIndices(c.nodeId); - qDebug() << " Removing" << indices.size() << "nodes"; std::sort(indices.begin(), indices.end(), std::greater()); for (int idx : indices) tree.nodes.remove(idx); @@ -518,6 +548,11 @@ void RcxController::batchRemoveNodes(const QVector& nodeIndices) { } idSet = m_doc->tree.normalizePreferAncestors(idSet); if (idSet.isEmpty()) return; + + // Clear selection before delete (prevents stale highlight on shifted lines) + m_selIds.clear(); + m_anchorLine = -1; + m_doc->undoStack.beginMacro(QString("Delete %1 nodes").arg(idSet.size())); for (uint64_t id : idSet) { int idx = m_doc->tree.indexOfId(id); diff --git a/src/core.h b/src/core.h index ad9d62b..8a9338c 100644 --- a/src/core.h +++ b/src/core.h @@ -449,7 +449,8 @@ namespace cmd { struct Rename { uint64_t nodeId; QString oldName, newName; }; struct Collapse { uint64_t nodeId; bool oldState, newState; }; struct Insert { Node node; }; - struct Remove { uint64_t nodeId; QVector subtree; }; + struct Remove { uint64_t nodeId; QVector subtree; + QVector offAdjs; }; struct ChangeBase { uint64_t oldBase, newBase; }; struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; }; } @@ -470,10 +471,12 @@ struct ColumnSpan { enum class EditTarget { Name, Type, Value }; // Column layout constants (shared with format.cpp span computation) -inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line -inline constexpr int kColType = 10; -inline constexpr int kColName = 24; -inline constexpr int kSepWidth = 2; +inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line +inline constexpr int kColType = 10; +inline constexpr int kColName = 24; +inline constexpr int kColValue = 22; +inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits +inline constexpr int kSepWidth = 2; inline ColumnSpan typeSpanFor(const LineMeta& lm) { if (lm.lineKind != LineKind::Field || lm.isContinuation) return {}; @@ -494,25 +497,47 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm) { return {start, start + kColName, true}; } -inline ColumnSpan valueSpanFor(const LineMeta& lm, int lineLength) { +inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/) { if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; int ind = kFoldCol + lm.depth * 3; - // Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes...] + // Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)] bool isHexPad = isHexPreview(lm.nodeKind); + int valWidth = isHexPad ? 23 : kColValue; // hex bytes or value column if (lm.isContinuation) { int prefixW = isHexPad ? (kColType + kSepWidth + 8 + kSepWidth) : (kColType + kColName + 4); int start = ind + prefixW; - return {start, lineLength, start < lineLength}; + return {start, start + valWidth, true}; } if (lm.lineKind != LineKind::Field) return {}; int start = isHexPad ? (ind + kColType + kSepWidth + 8 + kSepWidth) : (ind + kColType + kSepWidth + kColName + kSepWidth); + return {start, start + valWidth, true}; +} + +inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength) { + if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; + int ind = kFoldCol + lm.depth * 3; + + bool isHexPad = isHexPreview(lm.nodeKind); + int valWidth = isHexPad ? 23 : kColValue; + + int start; + if (lm.isContinuation) { + int prefixW = isHexPad + ? (kColType + kSepWidth + 8 + kSepWidth) + : (kColType + kColName + 4); + start = ind + prefixW + valWidth; + } else { + start = isHexPad + ? (ind + kColType + kSepWidth + 8 + kSepWidth + valWidth) + : (ind + kColType + kSepWidth + kColName + kSepWidth + valWidth); + } return {start, lineLength, start < lineLength}; } @@ -544,7 +569,8 @@ namespace fmt { QString fmtPointer32(uint32_t v); QString fmtPointer64(uint64_t v); QString fmtNodeLine(const Node& node, const Provider& prov, - uint64_t addr, int depth, int subLine = 0); + uint64_t addr, int depth, int subLine = 0, + const QString& comment = {}); QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation); QString fmtStructHeader(const Node& node, int depth); QString fmtStructFooter(const Node& node, int depth, int totalSize = -1); @@ -555,6 +581,7 @@ namespace fmt { uint64_t addr, int subLine); QByteArray parseValue(NodeKind kind, const QString& text, bool* ok); QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok); + QString validateValue(NodeKind kind, const QString& text); } // namespace fmt // ── Compose function forward declaration ── diff --git a/src/editor.cpp b/src/editor.cpp index fe9cec7..ca263e4 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -22,7 +22,6 @@ static const QColor kFgMarginDim("#505050"); static constexpr int IND_EDITABLE = 8; static constexpr int IND_HEX_DIM = 9; -static constexpr int IND_HOVER_TOK = 10; static QString g_fontName = "Consolas"; @@ -85,7 +84,6 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { if (!m_editState.active) return; if (m_editState.target == EditTarget::Value) validateEditLive(); - updateEditTokenBox(); }); } @@ -129,15 +127,6 @@ void RcxEditor::setupScintilla() { m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_HEX_DIM, QColor("#505050")); - // Hovered editable token highlight (subtle background tint, no outline) - m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, - IND_HOVER_TOK, 8 /*INDIC_STRAIGHTBOX*/); - m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, - IND_HOVER_TOK, QColor("#569cd6")); - m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETALPHA, - IND_HOVER_TOK, (long)35); - m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETOUTLINEALPHA, - IND_HOVER_TOK, (long)0); } void RcxEditor::setupLexer() { @@ -226,14 +215,14 @@ void RcxEditor::setupMarkers() { m_sci->setMarkerBackgroundColor(QColor("#e5a00d"), M_CYCLE); m_sci->setMarkerForegroundColor(QColor("#e5a00d"), M_CYCLE); - // M_ERR (4): background (dark red) + // M_ERR (4): background (dark red - brightened for visibility) m_sci->markerDefine(QsciScintilla::Background, M_ERR); - m_sci->setMarkerBackgroundColor(QColor("#5c2020"), M_ERR); + m_sci->setMarkerBackgroundColor(QColor("#7a2e2e"), M_ERR); m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR); - // M_STRUCT_BG (5): background tint for struct header/footer + // 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("#1a2638"), 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 @@ -257,6 +246,7 @@ void RcxEditor::allocateMarginStyles() { 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); @@ -286,11 +276,8 @@ void RcxEditor::applyDocument(const ComposeResult& result) { applyFoldLevels(result.meta); applyHexDimming(result.meta); - // Re-apply editable indicators for current cursor line + // Reset hint line - applySelectionOverlay will repaint indicators m_hintLine = -1; - int line, col; - m_sci->getCursorPosition(&line, &col); - updateEditableIndicators(line); } void RcxEditor::applyMarginText(const QVector& meta) { @@ -376,7 +363,7 @@ void RcxEditor::applySelectionOverlay(const QSet& selIds) { m_currentSelIds = selIds; m_sci->markerDeleteAll(M_SELECTED); - // Clear all editable indicators, then repaint for selected + cursor line + // 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); @@ -388,13 +375,9 @@ void RcxEditor::applySelectionOverlay(const QSet& selIds) { } } - // Also paint cursor line (even if not selected) - if (!m_editState.active) { - int curLine, col; - m_sci->getCursorPosition(&curLine, &col); - paintEditableSpans(curLine); - m_hintLine = curLine; - } + // Reset hint line - updateEditableIndicators will handle cursor hints + // on actual user navigation (not stale restored positions) + m_hintLine = -1; applyHoverHighlight(); } @@ -486,19 +469,22 @@ static QString getLineText(QsciScintilla* sci, int line) { // ── Shared inline-edit shutdown ── RcxEditor::EndEditInfo RcxEditor::endInlineEdit() { - // Clear edit token box and reset indicator color - clearIndicatorLine(IND_HOVER_TOK, m_editState.line); - m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, - IND_HOVER_TOK, QColor("#569cd6")); + // 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::restoreOverrideCursor(); - m_cursorOverridden = false; + QApplication::changeOverrideCursor(Qt::ArrowCursor); + } else { + QApplication::setOverrideCursor(Qt::ArrowCursor); + m_cursorOverridden = true; } - 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); @@ -586,16 +572,28 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, 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) return h; - h.line = (int)m_sci->SendScintilla( - QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); - h.col = (int)m_sci->SendScintilla( - QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos); + 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 < kFoldCol && m_meta[h.line].foldHead); + h.inFoldCol = (h.col >= 0 && h.col < kFoldCol && m_meta[h.line].foldHead); } return h; } @@ -668,7 +666,16 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { && 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; @@ -687,6 +694,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { } m_dragging = true; + m_dragStarted = false; // require threshold before extending + m_dragStartPos = me->pos(); m_dragLastLine = h.line; m_dragInitMods = me->modifiers(); @@ -705,10 +714,19 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { } } // 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, @@ -722,16 +740,23 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { } } 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; } } + // Block double/triple-click during edit mode (prevents word/line selection) + if (obj == m_sci->viewport() && m_editState.active + && event->type() == QEvent::MouseButtonDblClick) { + return true; + } if (obj == m_sci->viewport() && !m_editState.active && event->type() == QEvent::MouseButtonDblClick) { auto* me = static_cast(event); @@ -875,15 +900,8 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { bool RcxEditor::beginInlineEdit(EditTarget target, int line) { if (m_editState.active) return false; - if (m_cursorOverridden) { - QApplication::restoreOverrideCursor(); - m_cursorOverridden = false; - } m_hoveredNodeId = 0; applyHoverHighlight(); - // Clear hover token box (will be repainted as edit token box below) - clearIndicatorLine(IND_HOVER_TOK, m_hoverTokLine); - m_hoverTokLine = -1; // Clear editable-token color hints (de-emphasize non-active tokens) clearIndicatorLine(IND_EDITABLE, m_hintLine); m_hintLine = -1; @@ -919,8 +937,13 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1); m_sci->setReadOnly(false); - QApplication::setOverrideCursor(Qt::IBeamCursor); - m_cursorOverridden = true; + // Switch to I-beam for editing + 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); @@ -932,7 +955,10 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { long posStart = lineStart + m_editState.spanStart; long posEnd = posStart + trimmed.toUtf8().size(); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posEnd, posEnd); - updateEditTokenBox(); + + // 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); @@ -940,15 +966,6 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { return true; } -void RcxEditor::updateEditTokenBox() { - clearIndicatorLine(IND_HOVER_TOK, m_editState.line); - - int endCol = editEndCol(); - if (endCol <= m_editState.spanStart) return; - - fillIndicatorCols(IND_HOVER_TOK, m_editState.line, m_editState.spanStart, endCol); -} - int RcxEditor::editEndCol() const { QString lineText = getLineText(m_sci, m_editState.line); int delta = lineText.size() - m_editState.linelenAfterReplace; @@ -1023,6 +1040,19 @@ void RcxEditor::updateEditableIndicators(int line) { if (m_editState.active) return; if (line == m_hintLine) 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); @@ -1034,20 +1064,20 @@ void RcxEditor::updateEditableIndicators(int line) { paintEditableSpans(line); } -// ── Hover cursor (coalesced) ── +// ── Hover cursor ── void RcxEditor::applyHoverCursor() { - auto clearHoverTok = [&]() { - clearIndicatorLine(IND_HOVER_TOK, m_hoverTokLine); - m_hoverTokLine = -1; - }; + // Edit mode handles its own cursor (I-beam) + if (m_editState.active) + return; - if (m_editState.active || !m_hoverInside - || !m_sci->viewport()->underMouse()) { - clearHoverTok(); - if (m_cursorOverridden) { - QApplication::restoreOverrideCursor(); - m_cursorOverridden = false; + // 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; } @@ -1062,44 +1092,56 @@ void RcxEditor::applyHoverCursor() { if (h.inFoldCol) interactive = true; } - // Token box highlight - if (!tokenHit) { - clearHoverTok(); - } else if (line != m_hoverTokLine || t != m_hoverTokTarget) { - clearHoverTok(); - m_hoverTokLine = line; - m_hoverTokTarget = t; - - NormalizedSpan norm; - if (resolvedSpanFor(line, t, norm)) - fillIndicatorCols(IND_HOVER_TOK, line, norm.start, norm.end); - } - - if (interactive && !m_cursorOverridden) { - QApplication::setOverrideCursor(Qt::PointingHandCursor); + // 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 if (!interactive && m_cursorOverridden) { - QApplication::restoreOverrideCursor(); - m_cursorOverridden = false; + } else { + QApplication::changeOverrideCursor(desired); } } // ── Live value validation ── +void RcxEditor::setEditComment(const QString& comment) { + const LineMeta* lm = metaForLine(m_editState.line); + if (!lm) return; + + QString lineText = getLineText(m_sci, m_editState.line); + ColumnSpan cs = commentSpanFor(*lm, lineText.size()); + if (!cs.valid) return; + + // Pad/truncate comment to fixed width + QString padded = comment.leftJustified(kColComment, ' ').left(kColComment); + + long posA = posFromCol(m_sci, m_editState.line, cs.start); + long posB = posFromCol(m_sci, m_editState.line, cs.start + kColComment); + if (posB <= posA) return; + + 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()); +} + 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(); - bool ok; - fmt::parseValue(m_editState.editKind, text, &ok); - showEditValidation(ok); -} + QString errorMsg = fmt::validateValue(m_editState.editKind, text); -void RcxEditor::showEditValidation(bool valid) { - QColor c = valid ? QColor("#569cd6") : QColor("#e05050"); - m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_HOVER_TOK, c); + // Show/hide error marker (red background) and update comment + if (errorMsg.isEmpty()) { + m_sci->markerDelete(m_editState.line, M_ERR); + setEditComment(QStringLiteral("// Enter=Save Esc=Cancel")); + } else { + m_sci->markerAdd(m_editState.line, M_ERR); + setEditComment(QStringLiteral("// ") + errorMsg); + } } void RcxEditor::setEditorFont(const QString& fontName) { diff --git a/src/editor.h b/src/editor.h index 6b94b7a..2843293 100644 --- a/src/editor.h +++ b/src/editor.h @@ -65,11 +65,11 @@ private: bool m_cursorOverridden = false; uint64_t m_hoveredNodeId = 0; QSet m_currentSelIds; - int m_hoverTokLine = -1; - EditTarget m_hoverTokTarget = EditTarget::Name; // ── Drag selection ── bool m_dragging = false; + bool m_dragStarted = false; // true once drag threshold exceeded int m_dragLastLine = -1; + QPoint m_dragStartPos; // viewport coords at press Qt::KeyboardModifiers m_dragInitMods = Qt::NoModifier; // ── Deferred click (protects multi-select on double-click) ── @@ -112,9 +112,8 @@ private: void updateEditableIndicators(int line); void applyHoverCursor(); void applyHoverHighlight(); - void updateEditTokenBox(); void validateEditLive(); - void showEditValidation(bool valid); + void setEditComment(const QString& comment); // ── Refactored helpers ── struct HitInfo { int line = -1; int col = -1; uint64_t nodeId = 0; bool inFoldCol = false; }; diff --git a/src/format.cpp b/src/format.cpp index 59bc8da..7d75d48 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -6,9 +6,10 @@ namespace rcx::fmt { // ── Column layout ── // COL_TYPE and COL_NAME use shared constants from core.h (kColType, kColName) -static constexpr int COL_TYPE = kColType; -static constexpr int COL_NAME = kColName; -static constexpr int COL_VALUE = 22; +static constexpr int COL_TYPE = kColType; +static constexpr int COL_NAME = kColName; +static constexpr int COL_VALUE = 22; +static constexpr int COL_COMMENT = 28; // "// Enter=Save Esc=Cancel" fits static const QString SEP = QStringLiteral(" "); static QString fit(QString s, int w) { @@ -203,26 +204,31 @@ QString readValue(const Node& node, const Provider& prov, // ── Full node line ── QString fmtNodeLine(const Node& node, const Provider& prov, - uint64_t addr, int depth, int subLine) { + uint64_t addr, int depth, int subLine, + const QString& comment) { QString ind = indent(depth); QString type = typeName(node.kind); QString name = fit(node.name, COL_NAME); // Blank prefix for continuation lines (same width as type+sep+name+sep) const int prefixW = COL_TYPE + COL_NAME + 4; // 2 seps × 2 chars + // Comment suffix (padded or empty) + QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ') + : fit(comment, COL_COMMENT); + // Mat4x4: subLine 0..3 = rows if (node.kind == NodeKind::Mat4x4) { - QString val = readValue(node, prov, addr, subLine); - if (subLine == 0) return ind + type + SEP + name + SEP + val; - return ind + QString(prefixW, ' ') + val; + QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE); + if (subLine == 0) return ind + type + SEP + name + SEP + val + cmtSuffix; + return ind + QString(prefixW, ' ') + val + cmtSuffix; } // For vector types, subLine selects component if (subLine > 0 && (node.kind == NodeKind::Vec2 || node.kind == NodeKind::Vec3 || node.kind == NodeKind::Vec4)) { - QString val = readValue(node, prov, addr, subLine); - return ind + QString(prefixW, ' ') + val; + QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE); + return ind + QString(prefixW, ' ') + val + cmtSuffix; } // Hex nodes and Padding: ASCII preview + hex bytes (compact) @@ -234,22 +240,22 @@ QString fmtNodeLine(const Node& node, const Provider& prov, QByteArray b = prov.isReadable(addr + lineOff, lineBytes) ? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0'); QString ascii = bytesToAscii(b, lineBytes); - QString hex = bytesToHex(b, lineBytes); + QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1 if (subLine == 0) - return ind + type + SEP + ascii + SEP + hex; - return ind + QString(COL_TYPE + (int)SEP.size(), ' ') + ascii + SEP + hex; + return ind + type + SEP + ascii + SEP + hex + cmtSuffix; + return ind + QString(COL_TYPE + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix; } // Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns const int sz = sizeForKind(node.kind); QByteArray b = prov.isReadable(addr, sz) ? prov.readBytes(addr, sz) : QByteArray(sz, '\0'); QString ascii = bytesToAscii(b, sz).leftJustified(8, ' '); - QString hex = bytesToHex(b, sz); - return ind + type + SEP + ascii + SEP + hex; + QString hex = bytesToHex(b, sz).leftJustified(23, ' '); + return ind + type + SEP + ascii + SEP + hex + cmtSuffix; } - QString val = readValue(node, prov, addr, subLine); - return ind + type + SEP + name + SEP + val; + QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE); + return ind + type + SEP + name + SEP + val + cmtSuffix; } // ── Editable value (parse-friendly form for edit dialog) ── @@ -389,4 +395,23 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) { } } +// ── Value validation (returns error message or empty string if valid) ── + +QString validateValue(NodeKind kind, const QString& text) { + QString s = text.trimmed(); + if (s.isEmpty()) return {}; + + bool ok; + parseValue(kind, text, &ok); + if (ok) return {}; + + // Return byte-capacity max based on type size + const auto* m = kindMeta(kind); + if (m && m->size > 0 && m->size <= 8) { + uint64_t maxVal = (m->size == 8) ? ~0ULL : ((1ULL << (m->size * 8)) - 1); + return QStringLiteral("0x%1 max").arg(maxVal, m->size * 2, 16, QChar('0')); + } + return QStringLiteral("invalid"); +} + } // namespace rcx::fmt