From 9ec06d9658af9dba5910dfadf8482343334f8324 Mon Sep 17 00:00:00 2001 From: MegaBlocksTM Date: Sat, 7 Feb 2026 10:40:04 -0700 Subject: [PATCH] Move alignas alignment to context menu, fix hover state across refreshes Replace inline alignas() editing with a proper "Align Members" submenu in the right-click context menu. Remove alignas display from command row and all related span/hit-test/edit machinery. Preserve hover highlight state across document refreshes. --- src/compose.cpp | 2 +- src/controller.cpp | 72 ++++++++++++++++++++++++++++-------------- src/core.h | 16 ++-------- src/editor.cpp | 56 +++++++++++--------------------- src/resources.qrc | 2 ++ tests/test_compose.cpp | 25 ++------------- tests/test_editor.cpp | 12 +++---- 7 files changed, 81 insertions(+), 104 deletions(-) diff --git a/src/compose.cpp b/src/compose.cpp index 6fbf093..46b7258 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -495,7 +495,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { lm.markerMask = 0; lm.effectiveTypeW = state.typeW; lm.effectiveNameW = state.nameW; - state.emitLine(QStringLiteral("struct alignas(1)"), lm); + state.emitLine(QStringLiteral("struct "), lm); } QVector roots = state.childMap.value(0); diff --git a/src/controller.cpp b/src/controller.cpp index 6a8894c..ff25482 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -177,8 +177,7 @@ void RcxController::connectEditor(RcxEditor* editor) { this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) { // CommandRow BaseAddress/Source edit has nodeIdx=-1; CommandRow2 edits too if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source - && target != EditTarget::RootClassType && target != EditTarget::RootClassName - && target != EditTarget::Alignas) { refresh(); return; } + && target != EditTarget::RootClassType && target != EditTarget::RootClassName) { refresh(); return; } switch (target) { case EditTarget::Name: { if (text.isEmpty()) break; @@ -462,22 +461,6 @@ void RcxController::connectEditor(RcxEditor* editor) { case EditTarget::ArrayCount: // Array navigation removed - these cases are unreachable break; - case EditTarget::Alignas: { - // Parse "alignas(N)" → N - int paren = text.indexOf('('); - int close = text.indexOf(')'); - if (paren < 0 || close < 0) break; - int newAlign = text.mid(paren + 1, close - paren - 1).toInt(); - if (newAlign <= 0) break; - for (int i = 0; i < m_doc->tree.nodes.size(); i++) { - const auto& n = m_doc->tree.nodes[i]; - if (n.parentId == 0 && n.kind == NodeKind::Struct) { - performRealignment(n.id, newAlign); - break; - } - } - break; - } } // Always refresh to restore canonical text (handles parse failures, no-ops, etc.) refresh(); @@ -1000,6 +983,23 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, if (ni >= 0) toggleCollapse(ni); }); } + + // Align Members submenu + if (node.kind == NodeKind::Struct) { + int curAlign = m_doc->tree.computeStructAlignment(nodeId); + auto* alignMenu = menu.addMenu(icon("symbol-ruler.svg"), "Align &Members"); + static const int alignValues[] = {1, 2, 4, 8, 16, 32, 64, 128}; + for (int av : alignValues) { + QString label = (av == 1) + ? QStringLiteral("1 (packed)") + : QString::number(av); + auto* act = alignMenu->addAction(label, [this, nodeId, av]() { + performRealignment(nodeId, av); + }); + act->setCheckable(true); + act->setChecked(av == curAlign); + } + } } menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() { @@ -1034,6 +1034,33 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, // ── Always-available actions ── + // Root struct alignment (always available if a root struct exists) + { + uint64_t rootStructId = 0; + for (const auto& n : m_doc->tree.nodes) { + if (n.parentId == 0 && n.kind == NodeKind::Struct) { + rootStructId = n.id; + break; + } + } + if (rootStructId != 0) { + int curAlign = m_doc->tree.computeStructAlignment(rootStructId); + auto* alignMenu = menu.addMenu(icon("symbol-ruler.svg"), "Align &Members"); + static const int alignValues[] = {1, 2, 4, 8, 16, 32, 64, 128}; + for (int av : alignValues) { + QString label = (av == 1) + ? QStringLiteral("1 (packed)") + : QString::number(av); + auto* act = alignMenu->addAction(label, [this, rootStructId, av]() { + performRealignment(rootStructId, av); + }); + act->setCheckable(true); + act->setChecked(av == curAlign); + } + menu.addSeparator(); + } + } + menu.addAction(icon("add.svg"), "Add Hex64 at Root", [this]() { insertNode(0, -1, NodeKind::Hex64, "newField"); }); @@ -1324,21 +1351,20 @@ void RcxController::updateCommandRow() { .arg(elide(src, 40), elide(addr, 24), elide(sym, 40)); } - // Build row 2: root class type + name + alignment + // Build row 2: root class type + name QString row2; for (int i = 0; i < m_doc->tree.nodes.size(); i++) { const auto& n = m_doc->tree.nodes[i]; if (n.parentId == 0 && n.kind == NodeKind::Struct) { QString keyword = n.resolvedClassKeyword(); QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName; - int alignment = m_doc->tree.computeStructAlignment(n.id); - row2 = QStringLiteral("%1 %2 alignas(%3)") - .arg(keyword, className).arg(alignment); + row2 = QStringLiteral("%1 %2") + .arg(keyword, className); break; } } if (row2.isEmpty()) - row2 = QStringLiteral("struct alignas(1)"); + row2 = QStringLiteral("struct "); for (auto* ed : m_editors) { ed->setCommandRowText(row); diff --git a/src/core.h b/src/core.h index cc54d22..0a43b99 100644 --- a/src/core.h +++ b/src/core.h @@ -484,7 +484,7 @@ struct ColumnSpan { enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount, ArrayElementType, ArrayElementCount, PointerTarget, - RootClassType, RootClassName, Alignas }; + RootClassType, RootClassName }; // Column layout constants (shared with format.cpp span computation) inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line @@ -587,7 +587,7 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) { } // ── CommandRow2 spans ── -// Line format: "struct ClassName alignas(8)" +// Line format: "struct ClassName" inline ColumnSpan commandRow2TypeSpan(const QString& lineText) { int start = 0; @@ -606,22 +606,12 @@ inline ColumnSpan commandRow2NameSpan(const QString& lineText) { int nameStart = space + 1; while (nameStart < lineText.size() && lineText[nameStart].isSpace()) nameStart++; if (nameStart >= lineText.size()) return {}; - // Name ends before "alignas(" if present, otherwise at line end - int nameEnd = lineText.indexOf(QStringLiteral(" alignas("), nameStart); - if (nameEnd < 0) nameEnd = lineText.size(); + int nameEnd = lineText.size(); while (nameEnd > nameStart && lineText[nameEnd - 1].isSpace()) nameEnd--; if (nameEnd <= nameStart) return {}; return {nameStart, nameEnd, true}; } -inline ColumnSpan commandRow2AlignasSpan(const QString& lineText) { - int idx = lineText.indexOf(QStringLiteral("alignas(")); - if (idx < 0) return {}; - int end = lineText.indexOf(')', idx); - if (end < 0) return {}; - return {idx, end + 1, true}; -} - // ── Array element type/count spans (within type column of array headers) ── // Line format: " int32_t[10] name {" // arrayElemTypeSpan covers "int32_t", arrayElemCountSpan covers "10" diff --git a/src/editor.cpp b/src/editor.cpp index b7ecee0..6d9866d 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -81,8 +81,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { if (id == 1 && (m_editState.target == EditTarget::Type || m_editState.target == EditTarget::ArrayElementType || m_editState.target == EditTarget::PointerTarget - || m_editState.target == EditTarget::RootClassType - || m_editState.target == EditTarget::Alignas)) { + || m_editState.target == EditTarget::RootClassType)) { auto info = endInlineEdit(); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text); } @@ -314,6 +313,11 @@ void RcxEditor::applyDocument(const ComposeResult& result) { if (m_editState.active) endInlineEdit(); + // 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; @@ -333,6 +337,11 @@ void RcxEditor::applyDocument(const ComposeResult& result) { // Reset hint line - applySelectionOverlay will repaint indicators m_hintLine = -1; + + // Restore hover state + m_hoveredNodeId = savedHoverId; + m_hoveredLine = savedHoverLine; + m_hoverInside = savedHoverInside; } void RcxEditor::applyMarginText(const QVector& meta) { @@ -448,6 +457,7 @@ void RcxEditor::applySelectionOverlay(const QSet& selIds) { m_hintLine = -1; applyHoverHighlight(); + applyHoverCursor(); } void RcxEditor::applyHoverHighlight() { @@ -651,10 +661,6 @@ void RcxEditor::applyCommandRowPills() { ColumnSpan nameSpan = commandRow2NameSpan(t2); fillPadded2(nameSpan); - ColumnSpan alignasSpan = commandRow2AlignasSpan(t2); - fillPadded2(alignasSpan); - if (alignasSpan.valid) - fillIndicatorCols(IND_HEX_DIM, line2, alignasSpan.start, alignasSpan.end); } } @@ -811,15 +817,13 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, return out.valid; } - // CommandRow2: root class type, name, and alignas + // CommandRow2: root class type and name if (lm->lineKind == LineKind::CommandRow2) { - if (t != EditTarget::RootClassType && t != EditTarget::RootClassName - && t != EditTarget::Alignas) return false; + if (t != EditTarget::RootClassType && t != EditTarget::RootClassName) return false; QString lineText = getLineText(m_sci, line); ColumnSpan s; if (t == EditTarget::RootClassType) s = commandRow2TypeSpan(lineText); - else if (t == EditTarget::RootClassName) s = commandRow2NameSpan(lineText); - else s = commandRow2AlignasSpan(lineText); + else s = commandRow2NameSpan(lineText); out = normalizeSpan(s, lineText, t, false); if (lineTextOut) *lineTextOut = lineText; return out.valid; @@ -943,14 +947,12 @@ static bool hitTestTarget(QsciScintilla* sci, return false; } - // CommandRow2: root class type, name, and alignas + // CommandRow2: root class type and name if (lm.lineKind == LineKind::CommandRow2) { ColumnSpan ts = commandRow2TypeSpan(lineText); if (inSpan(ts)) { outTarget = EditTarget::RootClassType; outLine = line; return true; } ColumnSpan ns = commandRow2NameSpan(lineText); if (inSpan(ns)) { outTarget = EditTarget::RootClassName; outLine = line; return true; } - ColumnSpan as = commandRow2AlignasSpan(lineText); - if (inSpan(as)) { outTarget = EditTarget::Alignas; outLine = line; return true; } return false; } @@ -1369,8 +1371,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { if (lm->nodeIdx < 0 && !(lm->lineKind == LineKind::CommandRow && (target == EditTarget::BaseAddress || target == EditTarget::Source)) && !(lm->lineKind == LineKind::CommandRow2 && - (target == EditTarget::RootClassType || target == EditTarget::RootClassName - || target == EditTarget::Alignas))) + (target == EditTarget::RootClassType || target == EditTarget::RootClassName))) return false; // Padding: reject value editing (display-only hex bytes) if (target == EditTarget::Value && lm->nodeKind == NodeKind::Padding) @@ -1462,24 +1463,6 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { m_sci->viewport()->setCursor(Qt::ArrowCursor); }); } - if (target == EditTarget::Alignas) { - QTimer::singleShot(0, this, [this]() { - if (!m_editState.active || m_editState.target != EditTarget::Alignas) return; - 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); - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n'); - m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW, - (uintptr_t)1, - "alignas(1)\nalignas(4)\nalignas(8)\nalignas(16)"); - m_sci->viewport()->setCursor(Qt::ArrowCursor); - }); - } - return true; } @@ -1747,15 +1730,13 @@ void RcxEditor::paintEditableSpans(int line) { fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); return; } - // CommandRow2: paint RootClassType, RootClassName, and Alignas spans + // CommandRow2: paint RootClassType and RootClassName spans if (lm->lineKind == LineKind::CommandRow2) { NormalizedSpan norm; if (resolvedSpanFor(line, EditTarget::RootClassType, norm)) fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); if (resolvedSpanFor(line, EditTarget::RootClassName, norm)) fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); - if (resolvedSpanFor(line, EditTarget::Alignas, norm)) - fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); return; } if (isSyntheticLine(*lm)) return; @@ -1890,7 +1871,6 @@ void RcxEditor::applyHoverCursor() { case EditTarget::ArrayElementType: case EditTarget::PointerTarget: case EditTarget::RootClassType: - case EditTarget::Alignas: desired = Qt::PointingHandCursor; break; default: diff --git a/src/resources.qrc b/src/resources.qrc index 320a0fa..21d7922 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -40,5 +40,7 @@ vsicons/rename.svg vsicons/whole-word.svg vsicons/list-selection.svg + vsicons/symbol-numeric.svg + vsicons/symbol-ruler.svg diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index 6976946..1729fd6 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -1799,34 +1799,13 @@ private slots: QCOMPARE(tree.computeStructAlignment(rootId), 1); } - void testCommandRow2AlignasSpan() { - // Test span detection for alignas(N) in CommandRow2 text - QString text = "struct MyClass alignas(8)"; - ColumnSpan span = commandRow2AlignasSpan(text); - QVERIFY(span.valid); - QVERIFY(span.start >= 0); - QVERIFY(span.end > span.start); - - QString spanText = text.mid(span.start, span.end - span.start); - QCOMPARE(spanText, QString("alignas(8)")); - } - - void testCommandRow2AlignasSpanNoMatch() { - // Text without alignas should return invalid span + void testCommandRow2NameSpan() { + // Name span should cover the class name QString text = "struct MyClass"; - ColumnSpan span = commandRow2AlignasSpan(text); - QVERIFY(!span.valid); - } - - void testCommandRow2NameSpanStopsBeforeAlignas() { - // Name span should NOT include the alignas part - QString text = "struct MyClass alignas(4)"; ColumnSpan nameSpan = commandRow2NameSpan(text); QVERIFY(nameSpan.valid); QString nameText = text.mid(nameSpan.start, nameSpan.end - nameSpan.start); - QVERIFY2(!nameText.contains("alignas"), - qPrintable("Name span should not include alignas: " + nameText)); QVERIFY2(nameText.trimmed() == "MyClass", qPrintable("Name span should be 'MyClass', got: '" + nameText.trimmed() + "'")); } diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index d5814e3..6ece00c 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -1058,20 +1058,20 @@ private slots: m_editor->cancelInlineEdit(); } - // ── Test: alignas span detection on CommandRow2 ── - void testAlignasSpanOnCommandRow2() { + // ── Test: CommandRow2 has class type and name but no alignas ── + void testCommandRow2NoAlignas() { m_editor->applyDocument(m_result); - // Set CommandRow2 with alignas - m_editor->setCommandRow2Text(QStringLiteral("struct _PEB64 alignas(8)")); + // Set CommandRow2 without alignas + m_editor->setCommandRow2Text(QStringLiteral("struct _PEB64")); // Line 1 is CommandRow2 const LineMeta* lm = m_editor->metaForLine(1); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::CommandRow2); - // Alignas IS allowed as inline edit (picker-based) - QVERIFY(m_editor->beginInlineEdit(EditTarget::Alignas, 1)); + // RootClassName should work + QVERIFY(m_editor->beginInlineEdit(EditTarget::RootClassName, 1)); QVERIFY(m_editor->isEditing()); m_editor->cancelInlineEdit();