From b7b0cbf2d2063da64dd463f99d3b54ad68dd6e08 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Wed, 4 Feb 2026 09:03:18 -0700 Subject: [PATCH] Dynamic name column width + click-in-padding keeps edit active - Compute effective name column width from longest field name (8-22 chars) - Store layout in ComposeResult, cache in editor for span calculations - Parameterize span functions to use runtime nameW instead of fixed constant - Click within column padding during edit moves cursor to end instead of committing Co-Authored-By: Claude Opus 4.5 --- src/compose.cpp | 17 +++++++++++++-- src/core.h | 27 ++++++++++++++++-------- src/editor.cpp | 55 +++++++++++++++++++++++++++++++++++-------------- src/editor.h | 5 +++-- src/format.cpp | 6 +++--- 5 files changed, 78 insertions(+), 32 deletions(-) diff --git a/src/compose.cpp b/src/compose.cpp index 766cd2e..d01ff87 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -16,6 +16,7 @@ struct ComposeState { QSet visiting; // cycle detection for struct recursion QSet ptrVisiting; // cycle guard for pointer expansions int currentLine = 0; + int nameW = kColName; // effective name column width // Precomputed for O(1) lookups QHash> childMap; @@ -119,7 +120,8 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth); lm.foldLevel = computeFoldLevel(depth, false); - QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub); + QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub, + /*comment=*/{}, state.nameW); state.emitLine(lineText, lm); } } @@ -275,6 +277,17 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { for (int i = 0; i < tree.nodes.size(); i++) state.absOffsets[i] = tree.computeOffset(i); + // Compute effective name column width from longest name + int maxNameLen = kMinNameW; + for (const Node& node : tree.nodes) { + // Skip hex/padding (they show ASCII preview, not name column) + if (isHexPreview(node.kind)) continue; + // Skip containers (struct/array headers have different layout) + if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue; + maxNameLen = qMax(maxNameLen, node.name.size()); + } + state.nameW = qBound(kMinNameW, maxNameLen + 1, kMaxNameW); + QVector roots = state.childMap.value(0); std::sort(roots.begin(), roots.end(), [&](int a, int b) { return tree.nodes[a].offset < tree.nodes[b].offset; @@ -284,7 +297,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { composeNode(state, tree, prov, idx, 0); } - return { state.text, state.meta }; + return { state.text, state.meta, LayoutInfo{state.nameW} }; } QSet NodeTree::normalizePreferAncestors(const QSet& ids) const { diff --git a/src/core.h b/src/core.h index 1d8619c..ecdacb7 100644 --- a/src/core.h +++ b/src/core.h @@ -434,11 +434,18 @@ struct LineMeta { uint32_t markerMask = 0; }; +// ── Layout Info ── + +struct LayoutInfo { + int nameW = 22; // Effective name column width (default = kColName) +}; + // ── ComposeResult ── struct ComposeResult { QString text; QVector meta; + LayoutInfo layout; }; // ── Command ── @@ -479,6 +486,8 @@ inline constexpr int kColValue = 32; inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address) inline constexpr int kSepWidth = 2; +inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview) +inline constexpr int kMaxNameW = 22; // Maximum name column width (= kColName) inline ColumnSpan typeSpanFor(const LineMeta& lm) { if (lm.lineKind != LineKind::Field || lm.isContinuation) return {}; @@ -486,7 +495,7 @@ inline ColumnSpan typeSpanFor(const LineMeta& lm) { return {ind, ind + kColType, true}; } -inline ColumnSpan nameSpanFor(const LineMeta& lm) { +inline ColumnSpan nameSpanFor(const LineMeta& lm, int nameW = kColName) { if (lm.isContinuation || lm.lineKind != LineKind::Field) return {}; int ind = kFoldCol + lm.depth * 3; @@ -496,10 +505,10 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm) { if (isHexPreview(lm.nodeKind)) return {start, start + 8, true}; - return {start, start + kColName, true}; + return {start, start + nameW, true}; } -inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/) { +inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int nameW = kColName) { if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; int ind = kFoldCol + lm.depth * 3; @@ -510,7 +519,7 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/) { if (lm.isContinuation) { int prefixW = isHexPad ? (kColType + kSepWidth + 8 + kSepWidth) - : (kColType + kColName + 4); + : (kColType + nameW + 4); int start = ind + prefixW; return {start, start + valWidth, true}; } @@ -518,11 +527,11 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/) { int start = isHexPad ? (ind + kColType + kSepWidth + 8 + kSepWidth) - : (ind + kColType + kSepWidth + kColName + kSepWidth); + : (ind + kColType + kSepWidth + nameW + kSepWidth); return {start, start + valWidth, true}; } -inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength) { +inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int nameW = kColName) { if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; int ind = kFoldCol + lm.depth * 3; @@ -533,12 +542,12 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength) { if (lm.isContinuation) { int prefixW = isHexPad ? (kColType + kSepWidth + 8 + kSepWidth) - : (kColType + kColName + 4); + : (kColType + nameW + 4); start = ind + prefixW + valWidth; } else { start = isHexPad ? (ind + kColType + kSepWidth + 8 + kSepWidth + valWidth) - : (ind + kColType + kSepWidth + kColName + kSepWidth + valWidth); + : (ind + kColType + kSepWidth + nameW + kSepWidth + valWidth); } return {start, lineLength, start < lineLength}; } @@ -599,7 +608,7 @@ namespace fmt { QString fmtPointer64(uint64_t v); QString fmtNodeLine(const Node& node, const Provider& prov, uint64_t addr, int depth, int subLine = 0, - const QString& comment = {}); + const QString& comment = {}, int colName = kColName); QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation); QString fmtStructHeader(const Node& node, int depth); QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress); diff --git a/src/editor.cpp b/src/editor.cpp index 0f1ecae..61985da 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -153,7 +153,7 @@ void RcxEditor::setupLexer() { // Dark theme colors m_lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword); - m_lexer->setColor(QColor("#4ec9b0"), QsciLexerCPP::KeywordSet2); + 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); @@ -280,6 +280,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) { endInlineEdit(); m_meta = result.meta; + m_layout = result.layout; m_sci->setReadOnly(false); m_sci->setText(result.text); @@ -449,8 +450,8 @@ int RcxEditor::currentNodeIndex() const { // ── Column span computation ── ColumnSpan RcxEditor::typeSpan(const LineMeta& lm) { return typeSpanFor(lm); } -ColumnSpan RcxEditor::nameSpan(const LineMeta& lm) { return nameSpanFor(lm); } -ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength) { return valueSpanFor(lm, lineLength); } +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 ── @@ -591,8 +592,8 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, ColumnSpan s; switch (t) { case EditTarget::Type: s = typeSpan(*lm); break; - case EditTarget::Name: s = nameSpan(*lm); break; - case EditTarget::Value: s = valueSpan(*lm, textLen); 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; } @@ -639,7 +640,8 @@ RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const { static bool hitTestTarget(QsciScintilla* sci, const QVector& meta, const QPoint& viewportPos, - int& outLine, EditTarget& outTarget) + int& outLine, EditTarget& outTarget, + int nameW = kColName) { long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, (unsigned long)viewportPos.x(), (long)viewportPos.y()); @@ -655,8 +657,8 @@ static bool hitTestTarget(QsciScintilla* sci, const LineMeta& lm = meta[line]; ColumnSpan ts = RcxEditor::typeSpan(lm); - ColumnSpan ns = RcxEditor::nameSpan(lm); - ColumnSpan vs = RcxEditor::valueSpan(lm, textLen); + 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) @@ -687,13 +689,34 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { && m_editState.active) { auto* me = static_cast(event); auto h = hitTest(me->pos()); - bool insideEdit = false; + if (h.line == m_editState.line) { int editEnd = editEndCol(); - insideEdit = (h.col >= m_editState.spanStart && h.col <= editEnd); + 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 + } + } } - if (insideEdit) - return false; // inside edit span: let Scintilla position cursor + commitInlineEdit(); m_currentSelIds.clear(); // stale — normal handler will re-establish // Fall through to normal click handler below @@ -725,7 +748,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { // 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)) { + if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t, m_layout.nameW)) { m_pendingClickNodeId = 0; return beginInlineEdit(t, tLine); } @@ -801,7 +824,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { && event->type() == QEvent::MouseButtonDblClick) { auto* me = static_cast(event); int line; EditTarget t; - if (hitTestTarget(m_sci, m_meta, me->pos(), line, 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); } @@ -959,7 +982,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { // Store fixed comment column position for value editing if (target == EditTarget::Value) { - ColumnSpan cs = commentSpanFor(*lm, lineText.size()); + 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 { @@ -1214,7 +1237,7 @@ void RcxEditor::applyHoverCursor() { } int line; EditTarget t; - bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, 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) { diff --git a/src/editor.h b/src/editor.h index 7665582..4531b9b 100644 --- a/src/editor.h +++ b/src/editor.h @@ -25,8 +25,8 @@ public: // ── Column span computation ── static ColumnSpan typeSpan(const LineMeta& lm); - static ColumnSpan nameSpan(const LineMeta& lm); - static ColumnSpan valueSpan(const LineMeta& lm, int lineLength); + static ColumnSpan nameSpan(const LineMeta& lm, int nameW = kColName); + static ColumnSpan valueSpan(const LineMeta& lm, int lineLength, int nameW = kColName); // ── Multi-selection ── QSet selectedNodeIndices() const; @@ -55,6 +55,7 @@ private: QsciScintilla* m_sci = nullptr; QsciLexerCPP* m_lexer = nullptr; QVector m_meta; + LayoutInfo m_layout; // cached from ComposeResult int m_marginStyleBase = -1; int m_hintLine = -1; diff --git a/src/format.cpp b/src/format.cpp index 3d41182..32eb2d1 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -213,12 +213,12 @@ QString readValue(const Node& node, const Provider& prov, QString fmtNodeLine(const Node& node, const Provider& prov, uint64_t addr, int depth, int subLine, - const QString& comment) { + const QString& comment, int colName) { QString ind = indent(depth); QString type = typeName(node.kind); - QString name = fit(node.name, COL_NAME); + QString name = fit(node.name, colName); // Blank prefix for continuation lines (same width as type+sep+name+sep) - const int prefixW = COL_TYPE + COL_NAME + 4; // 2 seps × 2 chars + const int prefixW = COL_TYPE + colName + 4; // 2 seps × 2 chars // Comment suffix (padded or empty) QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ')