From 0e087fa3a4ae23a1e796e0bdca713ac9ec9f9c71 Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Fri, 20 Feb 2026 07:21:02 -0700 Subject: [PATCH] feat: primitive pointer modifiers, type chooser fixes, double-click to edit Type chooser: - Fix PointerTarget mode hiding primitives due to stale modifier state - Preselect */[n] modifier buttons to reflect current node type - Primitive pointer support: int32*, double**, etc with provider deref - hex64*/ptr64* with * modifier falls back to void* (meaningless deref) - isValidPrimitivePtrTarget guard in controller, compose, format - Modifier toggle no longer resets list selection - Primitive pointers open FieldType mode (not PointerTarget) - Type edit requires double-click (was single-click, too easy to misclick) Other: - Custom dock titlebar with themed close button, no float button - Status bar font synced at startup - Resize grip reworked as direct MainWindow child, font-independent - File menu "Source" renamed to "Current Tab Source" Tests: 41 type_selector, 39 editor, 17 controller (200 total, 0 failures) --- src/compose.cpp | 13 +- src/controller.cpp | 104 +++++++- src/core.h | 15 +- src/editor.cpp | 10 +- src/format.cpp | 24 ++ src/main.cpp | 39 ++- src/typeselectorpopup.cpp | 49 ++-- src/typeselectorpopup.h | 2 + tests/test_editor.cpp | 149 ++++++----- tests/test_type_selector.cpp | 505 +++++++++++++++++++++++++++++++++++ 10 files changed, 797 insertions(+), 113 deletions(-) diff --git a/src/compose.cpp b/src/compose.cpp index 771e1b2..7f5a93c 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -119,8 +119,17 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, QString ptrTypeOverride; QString ptrTargetName; if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) { - ptrTargetName = resolvePointerTarget(tree, node.refId); - ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName); + if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind)) { + // Primitive pointer: e.g. "int32*" or "f64**" + const auto* meta = kindMeta(node.elementKind); + QString baseName = meta ? QString::fromLatin1(meta->typeName) + : QStringLiteral("void"); + QString stars = (node.ptrDepth >= 2) ? QStringLiteral("**") : QStringLiteral("*"); + ptrTypeOverride = baseName + stars; + } else { + ptrTargetName = resolvePointerTarget(tree, node.refId); + ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName); + } } for (int sub = 0; sub < numLines; sub++) { diff --git a/src/controller.cpp b/src/controller.cpp index 1a7594c..eb19fae 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -223,8 +223,17 @@ void RcxController::connectEditor(RcxEditor* editor) { TypePopupMode mode = TypePopupMode::FieldType; if (target == EditTarget::ArrayElementType) mode = TypePopupMode::ArrayElement; - else if (target == EditTarget::PointerTarget) - mode = TypePopupMode::PointerTarget; + else if (target == EditTarget::PointerTarget) { + // Primitive pointers (ptrDepth>0) should open FieldType with + // the base type selected and *//** preselected — not PointerTarget. + bool isPrimPtr = false; + if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) { + const auto& n = m_doc->tree.nodes[nodeIdx]; + isPrimPtr = n.ptrDepth > 0 && n.refId == 0; + } + mode = isPrimPtr ? TypePopupMode::FieldType + : TypePopupMode::PointerTarget; + } showTypePopup(editor, mode, nodeIdx, globalPos); }); @@ -1855,6 +1864,8 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode, QVector entries; TypeEntry currentEntry; bool hasCurrent = false; + int preModId = 0; // modifier to preselect: 0=plain, 1=*, 2=**, 3=[n] + int preArrayCount = 0; // array count when preModId==3 auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) { for (const auto& m : kKindMeta) { @@ -1894,10 +1905,43 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode, }); break; - case TypePopupMode::FieldType: + case TypePopupMode::FieldType: { addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false); - if (node) { - // Mark current primitive + bool isPtr = node + && (node->kind == NodeKind::Pointer32 || node->kind == NodeKind::Pointer64); + bool isTypedPtr = isPtr && node->refId != 0; + bool isPrimPtr = isPtr && node->ptrDepth > 0 && node->refId == 0; + bool isArray = node && node->kind == NodeKind::Array; + + if (isPrimPtr) { + // Primitive pointer (e.g. int32* or f64**) — current = element kind, modifier = *//** + preModId = (node->ptrDepth >= 2) ? 2 : 1; + for (auto& e : entries) { + if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) { + currentEntry = e; + hasCurrent = true; + break; + } + } + } else if (isTypedPtr) { + // Typed pointer (e.g. Ball*) — current = composite target, modifier = * + preModId = 1; + } else if (isArray) { + // Array — modifier = [n] + preModId = 3; + preArrayCount = node->arrayLen; + if (node->elementKind != NodeKind::Struct) { + // Primitive array — mark element kind as current + for (auto& e : entries) { + if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) { + currentEntry = e; + hasCurrent = true; + break; + } + } + } + } else if (node) { + // Plain primitive — mark current for (auto& e : entries) { if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) { currentEntry = e; @@ -1906,8 +1950,14 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode, } } } - addComposites([](const Node&, const TypeEntry&) { return false; }); + // For isTypedPtr or struct-array: current is a Composite, set by addComposites below + addComposites([&](const Node& n, const TypeEntry& e) { + if (isTypedPtr && n.refId == e.structId) return true; + if (isArray && n.elementKind == NodeKind::Struct && n.refId == e.structId) return true; + return false; + }); break; + } case TypePopupMode::ArrayElement: addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true); @@ -1994,6 +2044,10 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode, popup->setFont(font); popup->setMode(mode); + // Preselect modifier button to reflect current node state (after setMode resets to plain) + if (preModId > 0) + popup->setModifier(preModId, preArrayCount); + // Pass current node size for same-size sorting int nodeSize = 0; if (node) { @@ -2107,6 +2161,44 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx, m_doc->undoStack.endMacro(); m_suppressRefresh = wasSuppressed; if (!m_suppressRefresh) refresh(); + } else if (spec.isPointer) { + if (!isValidPrimitivePtrTarget(resolved.primitiveKind)) { + // Hex, pointer, fnptr types with * → plain void pointer + if (nodeKind != NodeKind::Pointer64) + changeNodeKind(nodeIdx, NodeKind::Pointer64); + int idx = m_doc->tree.indexOfId(nodeId); + if (idx >= 0) { + auto& n = m_doc->tree.nodes[idx]; + n.ptrDepth = 0; + if (n.refId != 0) + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangePointerRef{nodeId, n.refId, 0})); + } + } else { + // Primitive pointer: e.g. "int32*" or "f64**" → Pointer64 + elementKind + ptrDepth + bool wasSuppressed = m_suppressRefresh; + m_suppressRefresh = true; + m_doc->undoStack.beginMacro(QStringLiteral("Change to primitive pointer")); + if (nodeKind != NodeKind::Pointer64) + changeNodeKind(nodeIdx, NodeKind::Pointer64); + int idx = m_doc->tree.indexOfId(nodeId); + if (idx >= 0) { + auto& n = m_doc->tree.nodes[idx]; + if (n.elementKind != resolved.primitiveKind || n.ptrDepth != spec.ptrDepth) { + NodeKind oldEK = n.elementKind; + int oldDepth = n.ptrDepth; + n.elementKind = resolved.primitiveKind; + n.ptrDepth = spec.ptrDepth; + if (n.refId != 0) + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangePointerRef{nodeId, n.refId, 0})); + Q_UNUSED(oldEK); Q_UNUSED(oldDepth); + } + } + m_doc->undoStack.endMacro(); + m_suppressRefresh = wasSuppressed; + if (!m_suppressRefresh) refresh(); + } } else { if (resolved.primitiveKind != nodeKind) changeNodeKind(nodeIdx, resolved.primitiveKind); diff --git a/src/core.h b/src/core.h index 791facb..750a544 100644 --- a/src/core.h +++ b/src/core.h @@ -142,6 +142,15 @@ inline constexpr bool isMatrixKind(NodeKind k) { inline constexpr bool isFuncPtr(NodeKind k) { return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64; } +// Hex types, pointer types, function pointers, and containers are not meaningful +// primitive-pointer targets — dereferencing them produces the same output as void*. +inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) { + if (isHexNode(k)) return false; + if (k == NodeKind::Pointer32 || k == NodeKind::Pointer64) return false; + if (isFuncPtr(k)) return false; + if (k == NodeKind::Struct || k == NodeKind::Array) return false; + return true; +} inline QStringList allTypeNamesForUI(bool stripBrackets = false) { QStringList out; @@ -184,7 +193,8 @@ struct Node { int strLen = 64; bool collapsed = false; uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr - NodeKind elementKind = NodeKind::UInt8; // Array: element type + NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type + int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive** int viewIndex = 0; // Array: current view offset (transient) // Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size. @@ -217,6 +227,8 @@ struct Node { o["collapsed"] = collapsed; o["refId"] = QString::number(refId); o["elementKind"] = kindToString(elementKind); + if (ptrDepth > 0) + o["ptrDepth"] = ptrDepth; return o; } static Node fromJson(const QJsonObject& o) { @@ -233,6 +245,7 @@ struct Node { n.collapsed = o["collapsed"].toBool(false); n.refId = o["refId"].toString("0").toULongLong(); n.elementKind = kindFromString(o["elementKind"].toString("UInt8")); + n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2); return n; } diff --git a/src/editor.cpp b/src/editor.cpp index e7100fa..ace3898 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -1789,15 +1789,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { // Single-click on editable token of already-selected node → edit int tLine, tCol; EditTarget t; if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) { - // Type/ArrayElementType/PointerTarget open a dismissible popup - // (not inline text edit), so allow on first click without - // requiring the node to be pre-selected. - bool isPopupTarget = (t == EditTarget::Type - || t == EditTarget::ArrayElementType - || t == EditTarget::PointerTarget); - if ((alreadySelected || isPopupTarget) && plain) { - if (!alreadySelected) - emit nodeClicked(h.line, h.nodeId, me->modifiers()); + if (alreadySelected && plain) { m_pendingClickNodeId = 0; return beginInlineEdit(t, tLine, tCol); } diff --git a/src/format.cpp b/src/format.cpp index f2f9e34..19326e4 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -267,6 +267,30 @@ static QString readValueImpl(const Node& node, const Provider& prov, } case NodeKind::Pointer64: { uint64_t val = prov.readU64(addr); + // Primitive pointer: dereference and show target value + // (hex/ptr/fnptr targets fall through to plain void* display) + if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind) && val != 0) { + uint64_t target = val; + for (int d = 1; d < node.ptrDepth && target != 0; d++) + target = prov.isReadable(target, 8) ? prov.readU64(target) : 0; + if (target != 0 && prov.isReadable(target, sizeForKind(node.elementKind))) { + // Create a temporary node of the target kind to format the value + Node tmp; + tmp.kind = node.elementKind; + tmp.strLen = node.strLen; + QString derefVal = readValueImpl(tmp, prov, target, 0, mode); + if (display) { + QString arrow = QStringLiteral("-> "); + QString sym = prov.getSymbol(val); + if (!sym.isEmpty()) + return arrow + derefVal + QStringLiteral(" // ") + sym; + return arrow + derefVal; + } + return derefVal; + } + if (!display) return rawHex(val, 16); + return fmtPointer64(val); + } if (!display) return rawHex(val, 16); QString s = fmtPointer64(val); QString sym = prov.getSymbol(val); diff --git a/src/main.cpp b/src/main.cpp index 07ac38f..edcfacf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -410,7 +410,7 @@ void MainWindow::createMenus() { Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile); Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs); file->addSeparator(); - m_sourceMenu = file->addMenu("So&urce"); + m_sourceMenu = file->addMenu("Current Tab So&urce"); connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu); file->addSeparator(); Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile); @@ -499,14 +499,26 @@ void MainWindow::createMenus() { } // ── Themed resize grip (replaces ugly default QSizeGrip) ── +// Positioned as a direct child of MainWindow at the bottom-right corner, +// NOT inside the status bar layout (which is font-height dependent). class ResizeGrip : public QWidget { public: + static constexpr int kSize = 16; // widget size + static constexpr int kPad = 4; // padding from window corner (identical right & bottom) + explicit ResizeGrip(QWidget* parent) : QWidget(parent) { - setFixedSize(16, 16); + setFixedSize(kSize, kSize); setCursor(Qt::SizeFDiagCursor); m_color = rcx::ThemeManager::instance().current().textFaint; } void setGripColor(const QColor& c) { m_color = c; update(); } + + // Call from parent's resizeEvent to pin to bottom-right corner + void reposition() { + QWidget* w = parentWidget(); + if (w) move(w->width() - kSize - kPad, w->height() - kSize - kPad); + } + protected: void paintEvent(QPaintEvent*) override { QPainter p(this); @@ -514,8 +526,11 @@ protected: p.setPen(Qt::NoPen); p.setBrush(m_color); // 6 dots in a triangle pointing bottom-right (VS2022 style) + // Dot grid is centered within the widget: same inset from right and bottom const double r = 1.0, s = 4.0; - double bx = width() - 5, by = height() - 4; + const double inset = 4.0; + double bx = width() - inset; + double by = height() - inset; // bottom row: 3 dots p.drawEllipse(QPointF(bx, by), r, r); p.drawEllipse(QPointF(bx - s, by), r, r); @@ -539,13 +554,15 @@ private: void MainWindow::createStatusBar() { m_statusLabel = new QLabel("Ready"); m_statusLabel->setContentsMargins(10, 0, 0, 0); - statusBar()->setContentsMargins(0, 4, 0, 0); + statusBar()->setContentsMargins(0, 0, 0, 0); statusBar()->setSizeGripEnabled(false); // disable ugly default grip statusBar()->addWidget(m_statusLabel, 1); + // Grip is a direct child of the main window, NOT in the status bar layout. + // Positioned via reposition() in resizeEvent — immune to font/margin changes. auto* grip = new ResizeGrip(this); grip->setObjectName("resizeGrip"); - statusBar()->addPermanentWidget(grip); + grip->raise(); { const auto& t = ThemeManager::instance().current(); @@ -1116,15 +1133,16 @@ void MainWindow::applyTheme(const Theme& theme) { // Re-style ✕ close buttons on MDI tabs styleTabCloseButtons(); - // Status bar + resize grip + // Status bar { QPalette sbPal = statusBar()->palette(); sbPal.setColor(QPalette::Window, theme.background); sbPal.setColor(QPalette::WindowText, theme.textDim); statusBar()->setPalette(sbPal); - auto* grip = statusBar()->findChild("resizeGrip"); - if (grip) grip->setGripColor(theme.textFaint); } + // Resize grip (direct child of main window, not in status bar) + if (auto* grip = findChild("resizeGrip")) + grip->setGripColor(theme.textFaint); // Workspace tree: text color matches menu bar if (m_workspaceTree) { @@ -2060,6 +2078,11 @@ void MainWindow::resizeEvent(QResizeEvent* event) { m_borderOverlay->setGeometry(rect()); m_borderOverlay->raise(); } + auto* grip = findChild("resizeGrip"); + if (grip) { + grip->reposition(); + grip->raise(); + } } void MainWindow::updateBorderColor(const QColor& color) { diff --git a/src/typeselectorpopup.cpp b/src/typeselectorpopup.cpp index b64f9c7..6399b22 100644 --- a/src/typeselectorpopup.cpp +++ b/src/typeselectorpopup.cpp @@ -32,7 +32,8 @@ TypeSpec parseTypeSpec(const QString& text) { if (s.endsWith('*')) { spec.isPointer = true; s.chop(1); - if (s.endsWith('*')) s.chop(1); // double pointer + spec.ptrDepth = 1; + if (s.endsWith('*')) { s.chop(1); spec.ptrDepth = 2; } spec.baseName = s.trimmed(); return spec; } @@ -347,7 +348,6 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) m_arrayCountEdit->selectAll(); } updateModifierPreview(); - applyFilter(m_filterEdit->text()); }); connect(m_arrayCountEdit, &QLineEdit::textChanged, this, [this]() { updateModifierPreview(); }); @@ -516,22 +516,32 @@ void TypeSelectorPopup::setTitle(const QString& title) { void TypeSelectorPopup::setMode(TypePopupMode mode) { m_mode = mode; - // Show modifier toggles for modes where type modifiers make sense bool showMods = (mode == TypePopupMode::FieldType || mode == TypePopupMode::ArrayElement); m_modRow->setVisible(showMods); - // Reset to plain when showing - if (showMods) { - m_btnPlain->setChecked(true); - m_arrayCountEdit->clear(); - m_arrayCountEdit->hide(); - } + // Always reset to plain — prevents stale state from leaking across modes + // (PointerTarget hides buttons but applyFilter still reads their state) + m_btnPlain->setChecked(true); + m_arrayCountEdit->clear(); + m_arrayCountEdit->hide(); } void TypeSelectorPopup::setCurrentNodeSize(int bytes) { m_currentNodeSize = bytes; } +void TypeSelectorPopup::setModifier(int modId, int arrayCount) { + if (modId == 1) m_btnPtr->setChecked(true); + else if (modId == 2) m_btnDblPtr->setChecked(true); + else if (modId == 3) { + m_btnArray->setChecked(true); + m_arrayCountEdit->setText(QString::number(arrayCount)); + m_arrayCountEdit->show(); + } else { + m_btnPlain->setChecked(true); + } +} + void TypeSelectorPopup::setTypes(const QVector& types, const TypeEntry* current) { m_allTypes = types; if (current) { @@ -541,10 +551,8 @@ void TypeSelectorPopup::setTypes(const QVector& types, const TypeEntr m_currentEntry = TypeEntry{}; m_hasCurrent = false; } - // Reset modifier toggles - m_btnPlain->setChecked(true); - m_arrayCountEdit->clear(); - m_arrayCountEdit->hide(); + // Don't reset modifier buttons here — setMode() already resets to plain, + // and setModifier() may have preselected a button between setMode/setTypes. m_previewLabel->hide(); m_filterEdit->clear(); @@ -630,23 +638,18 @@ void TypeSelectorPopup::applyFilter(const QString& text) { QString filterBase = text.trimmed(); - // Hide primitives when a pointer modifier (* or **) is active - int modId = m_modGroup->checkedId(); - bool hideprimitives = (modId == 1 || modId == 2); - - // Separate primitives and composites + // Separate primitives and composites (all types shown regardless of modifier) QVector primitives, composites; for (const auto& t : m_allTypes) { - if (t.entryKind == TypeEntry::Section) continue; // skip stale sections + if (t.entryKind == TypeEntry::Section) continue; bool matchesFilter = filterBase.isEmpty() || t.displayName.contains(filterBase, Qt::CaseInsensitive) || t.classKeyword.contains(filterBase, Qt::CaseInsensitive); if (!matchesFilter) continue; - if (t.entryKind == TypeEntry::Primitive) { - if (!hideprimitives) - primitives.append(t); - } else if (t.entryKind == TypeEntry::Composite) + if (t.entryKind == TypeEntry::Primitive) + primitives.append(t); + else if (t.entryKind == TypeEntry::Composite) composites.append(t); } diff --git a/src/typeselectorpopup.h b/src/typeselectorpopup.h index b2011fb..4796a7e 100644 --- a/src/typeselectorpopup.h +++ b/src/typeselectorpopup.h @@ -40,6 +40,7 @@ struct TypeEntry { struct TypeSpec { QString baseName; bool isPointer = false; + int ptrDepth = 0; // 1 = *, 2 = ** (only meaningful when isPointer) int arrayCount = 0; // 0 = not array }; @@ -57,6 +58,7 @@ public: void setMode(TypePopupMode mode); void applyTheme(const Theme& theme); void setCurrentNodeSize(int bytes); + void setModifier(int modId, int arrayCount = 0); void setTypes(const QVector& types, const TypeEntry* current = nullptr); void popup(const QPoint& globalPos); diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index 6daa7da..54ccece 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -2048,26 +2048,33 @@ private slots: m_editor->applyDocument(m_result); } - // ── Test: resize grip equidistant from right and bottom window edges ── + // ── Test: resize grip dots are equidistant from right and bottom window edges ── + // The grip is a direct child of the window positioned via move(), not inside + // the status bar layout. This test verifies the dot placement is symmetric + // regardless of font, and runs the check at two different font sizes to prove + // font independence. void testResizeGripCornerSymmetry() { - // Reproduce the exact MainWindow status bar + grip setup - QMainWindow win; - win.resize(400, 300); - win.statusBar()->setSizeGripEnabled(false); - win.statusBar()->setContentsMargins(0, 4, 0, 0); + // Same constants as production ResizeGrip in main.cpp + static constexpr int kSize = 16; + static constexpr int kPad = 4; + static constexpr double kInset = 4.0; - // Inline replica of the ResizeGrip paint (same constants as main.cpp) class Grip : public QWidget { public: - explicit Grip(QWidget* p) : QWidget(p) { setFixedSize(16, 16); } + explicit Grip(QWidget* p) : QWidget(p) { setFixedSize(kSize, kSize); } + void reposition() { + if (auto* w = parentWidget()) + move(w->width() - kSize - kPad, w->height() - kSize - kPad); + } protected: void paintEvent(QPaintEvent*) override { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); p.setPen(Qt::NoPen); - p.setBrush(Qt::red); // high-contrast so we can find it + p.setBrush(Qt::red); const double r = 1.0, s = 4.0; - double bx = width() - 5, by = height() - 4; + double bx = width() - kInset; + double by = height() - kInset; p.drawEllipse(QPointF(bx, by), r, r); p.drawEllipse(QPointF(bx - s, by), r, r); p.drawEllipse(QPointF(bx - 2 * s, by), r, r); @@ -2077,73 +2084,87 @@ private slots: } }; - auto* grip = new Grip(&win); - win.statusBar()->addPermanentWidget(grip); + // Helper: grab window, find bottommost-rightmost red pixel, measure gaps + auto measureGaps = [](QWidget* win, int& gapRight, int& gapBottom) -> bool { + QPixmap px = win->grab(); + QImage img = px.toImage().convertToFormat(QImage::Format_ARGB32); + int W = img.width(), H = img.height(); + if (W < 50 || H < 50) return false; - // Use a known background so non-grip pixels are easy to identify - QPalette pal = win.statusBar()->palette(); - pal.setColor(QPalette::Window, QColor(30, 30, 30)); - win.statusBar()->setPalette(pal); - win.statusBar()->setAutoFillBackground(true); - - win.show(); - QVERIFY(QTest::qWaitForWindowExposed(&win)); - QTest::qWait(100); // let paint settle - - // Grab just the window contents (no DWM shadow) - QPixmap px = win.grab(); - QImage img = px.toImage().convertToFormat(QImage::Format_ARGB32); - int W = img.width(); - int H = img.height(); - QVERIFY(W > 50); - QVERIFY(H > 50); - - // Scan from bottom-right to find the bottommost-rightmost red pixel - // (the corner dot of the grip triangle) - int gripRight = -1, gripBottom = -1; - for (int y = H - 1; y >= H - 40 && gripBottom < 0; --y) { - for (int x = W - 1; x >= W - 40; --x) { - QColor c(img.pixel(x, y)); - if (c.red() > 180 && c.green() < 80 && c.blue() < 80) { - gripRight = x; - gripBottom = y; - break; + int foundX = -1, foundY = -1; + for (int y = H - 1; y >= H - 40 && foundY < 0; --y) { + for (int x = W - 1; x >= W - 40; --x) { + QColor c(img.pixel(x, y)); + if (c.red() > 180 && c.green() < 80 && c.blue() < 80) { + foundX = x; foundY = y; break; + } } } - if (gripBottom >= 0) break; - } + if (foundX < 0) return false; + gapRight = (W - 1) - foundX; + gapBottom = (H - 1) - foundY; - QVERIFY2(gripRight >= 0 && gripBottom >= 0, - "Could not find red grip dot in bottom-right corner"); - - int gapRight = (W - 1) - gripRight; - int gapBottom = (H - 1) - gripBottom; - - // Save diagnostic image with markers - { + // Save diagnostic image QImage diag = img.copy(); QPainter dp(&diag); dp.setPen(QPen(Qt::cyan, 1)); - // Mark the found dot - dp.drawRect(gripRight - 3, gripBottom - 3, 6, 6); - // Draw gap measurement lines + dp.drawRect(foundX - 3, foundY - 3, 6, 6); dp.setPen(QPen(Qt::yellow, 1)); - dp.drawLine(gripRight, gripBottom, W - 1, gripBottom); // right gap - dp.drawLine(gripRight, gripBottom, gripRight, H - 1); // bottom gap + dp.drawLine(foundX, foundY, W - 1, foundY); + dp.drawLine(foundX, foundY, foundX, H - 1); dp.end(); diag.save("grip_corner_diag.png"); - } + return true; + }; - QString msg = QString("gapRight=%1 gapBottom=%2 (diff=%3) gripPos=(%4,%5) winSize=%6x%7") - .arg(gapRight).arg(gapBottom).arg(qAbs(gapRight - gapBottom)) - .arg(gripRight).arg(gripBottom).arg(W).arg(H); + // --- Round 1: default system font --- + QMainWindow win; + win.resize(500, 375); - // The gaps must be equal (symmetric corner placement) - QVERIFY2(qAbs(gapRight - gapBottom) <= 1, - qPrintable("Grip not equidistant from edges: " + msg)); + QPalette pal; + pal.setColor(QPalette::Window, QColor(30, 30, 30)); + win.setPalette(pal); + win.statusBar()->setPalette(pal); + win.statusBar()->setAutoFillBackground(true); - // Also log the values even on pass - qDebug() << "Grip corner symmetry:" << msg; + auto* grip = new Grip(&win); + grip->raise(); + + win.show(); + QVERIFY(QTest::qWaitForWindowExposed(&win)); + grip->reposition(); + QTest::qWait(100); + + int gapR1 = 0, gapB1 = 0; + QVERIFY2(measureGaps(&win, gapR1, gapB1), + "Could not find red grip dot (round 1)"); + QVERIFY2(gapR1 == gapB1, + qPrintable(QString("Round 1 asymmetric: gapRight=%1 gapBottom=%2") + .arg(gapR1).arg(gapB1))); + + // --- Round 2: large font on status bar (must NOT change grip position) --- + QFont bigFont("Arial", 24); + win.statusBar()->setFont(bigFont); + QTest::qWait(100); + grip->reposition(); + QTest::qWait(100); + + int gapR2 = 0, gapB2 = 0; + QVERIFY2(measureGaps(&win, gapR2, gapB2), + "Could not find red grip dot (round 2, big font)"); + QVERIFY2(gapR2 == gapB2, + qPrintable(QString("Round 2 asymmetric: gapRight=%1 gapBottom=%2") + .arg(gapR2).arg(gapB2))); + + // Gaps must be identical across both font sizes + QVERIFY2(gapR1 == gapR2 && gapB1 == gapB2, + qPrintable(QString("Font changed grip position: " + "round1=(%1,%2) round2=(%3,%4)") + .arg(gapR1).arg(gapB1).arg(gapR2).arg(gapB2))); + + qDebug() << "Grip corner symmetry:" + << QString("gapRight=%1 gapBottom=%2 (font-independent)") + .arg(gapR1).arg(gapB1); } }; diff --git a/tests/test_type_selector.cpp b/tests/test_type_selector.cpp index 88ac44d..838e274 100644 --- a/tests/test_type_selector.cpp +++ b/tests/test_type_selector.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -498,6 +499,7 @@ private slots: TypeSpec spec = parseTypeSpec("Ball*"); QCOMPARE(spec.baseName, QString("Ball")); QVERIFY(spec.isPointer); + QCOMPARE(spec.ptrDepth, 1); QCOMPARE(spec.arrayCount, 0); } @@ -505,6 +507,7 @@ private slots: TypeSpec spec = parseTypeSpec("Ball**"); QCOMPARE(spec.baseName, QString("Ball")); QVERIFY(spec.isPointer); + QCOMPARE(spec.ptrDepth, 2); } void testParseTypeSpecEmpty() { @@ -960,6 +963,508 @@ private slots: // Restore tm.setCurrent(origIdx); } + + // ── parseTypeSpec: primitive pointer ptrDepth ── + + void testParseTypeSpecPrimitiveStar() { + TypeSpec spec = parseTypeSpec("int32_t*"); + QCOMPARE(spec.baseName, QString("int32_t")); + QVERIFY(spec.isPointer); + QCOMPARE(spec.ptrDepth, 1); + QCOMPARE(spec.arrayCount, 0); + } + + void testParseTypeSpecPrimitiveDoubleStar() { + TypeSpec spec = parseTypeSpec("f64**"); + QCOMPARE(spec.baseName, QString("f64")); + QVERIFY(spec.isPointer); + QCOMPARE(spec.ptrDepth, 2); + QCOMPARE(spec.arrayCount, 0); + } + + // ── Primitive pointer creation via applyTypePopupResult path ── + + void testPrimitivePointerCreation() { + auto* doc = new RcxDocument(); + buildTwoRootTree(doc->tree); + doc->provider = std::make_unique(makeBuffer()); + + auto* splitter = new QSplitter(); + auto* ctrl = new RcxController(doc, nullptr); + ctrl->addSplitEditor(splitter); + + splitter->resize(800, 600); + splitter->show(); + QVERIFY(QTest::qWaitForWindowExposed(splitter)); + ctrl->refresh(); + QApplication::processEvents(); + + // Find the "x" field (Int32) inside Alpha + int xIdx = -1; + for (int i = 0; i < doc->tree.nodes.size(); i++) { + if (doc->tree.nodes[i].name == "x") { xIdx = i; break; } + } + QVERIFY(xIdx >= 0); + QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32); + uint64_t xNodeId = doc->tree.nodes[xIdx].id; + + // Simulate the primitive-pointer path: Int32 → Pointer64 + elementKind=Int32 + ptrDepth=1 + doc->undoStack.beginMacro(QStringLiteral("Change to primitive pointer")); + ctrl->changeNodeKind(xIdx, NodeKind::Pointer64); + int idx = doc->tree.indexOfId(xNodeId); + QVERIFY(idx >= 0); + doc->tree.nodes[idx].elementKind = NodeKind::Int32; + doc->tree.nodes[idx].ptrDepth = 1; + doc->undoStack.endMacro(); + QApplication::processEvents(); + + // Verify: Pointer64 with elementKind=Int32, ptrDepth=1, refId=0 + idx = doc->tree.indexOfId(xNodeId); + QVERIFY(idx >= 0); + QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64); + QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Int32); + QCOMPARE(doc->tree.nodes[idx].ptrDepth, 1); + QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0)); + + // Undo reverses the macro + doc->undoStack.undo(); + QApplication::processEvents(); + idx = doc->tree.indexOfId(xNodeId); + QVERIFY(idx >= 0); + QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Int32); + + delete ctrl; + delete splitter; + delete doc; + } + + void testDoublePointerCreation() { + auto* doc = new RcxDocument(); + buildTwoRootTree(doc->tree); + doc->provider = std::make_unique(makeBuffer()); + + auto* splitter = new QSplitter(); + auto* ctrl = new RcxController(doc, nullptr); + ctrl->addSplitEditor(splitter); + + splitter->resize(800, 600); + splitter->show(); + QVERIFY(QTest::qWaitForWindowExposed(splitter)); + ctrl->refresh(); + QApplication::processEvents(); + + // Find the "x" field (Int32) inside Alpha + int xIdx = -1; + for (int i = 0; i < doc->tree.nodes.size(); i++) { + if (doc->tree.nodes[i].name == "x") { xIdx = i; break; } + } + QVERIFY(xIdx >= 0); + uint64_t xNodeId = doc->tree.nodes[xIdx].id; + + // Simulate: Int32 → Pointer64 + elementKind=Double + ptrDepth=2 + doc->undoStack.beginMacro(QStringLiteral("Change to double pointer")); + ctrl->changeNodeKind(xIdx, NodeKind::Pointer64); + int idx = doc->tree.indexOfId(xNodeId); + QVERIFY(idx >= 0); + doc->tree.nodes[idx].elementKind = NodeKind::Double; + doc->tree.nodes[idx].ptrDepth = 2; + doc->undoStack.endMacro(); + QApplication::processEvents(); + + // Verify: Pointer64 with elementKind=Double, ptrDepth=2 + idx = doc->tree.indexOfId(xNodeId); + QVERIFY(idx >= 0); + QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64); + QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Double); + QCOMPARE(doc->tree.nodes[idx].ptrDepth, 2); + QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0)); + + delete ctrl; + delete splitter; + delete doc; + } + + // ── ptrDepth JSON round-trip ── + + void testPtrDepthJsonRoundTrip() { + Node n; + n.kind = NodeKind::Pointer64; + n.name = "pData"; + n.elementKind = NodeKind::Float; + n.ptrDepth = 2; + n.id = 42; + + QJsonObject obj = n.toJson(); + QCOMPARE(obj["ptrDepth"].toInt(), 2); + + Node restored = Node::fromJson(obj); + QCOMPARE(restored.ptrDepth, 2); + QCOMPARE(restored.elementKind, NodeKind::Float); + QCOMPARE(restored.kind, NodeKind::Pointer64); + } + + void testPtrDepthJsonDefault() { + // Nodes without ptrDepth in JSON should default to 0 + Node n; + n.kind = NodeKind::Pointer64; + n.name = "pVoid"; + n.id = 99; + + QJsonObject obj = n.toJson(); + // ptrDepth==0 is not serialized + QVERIFY(!obj.contains("ptrDepth")); + + Node restored = Node::fromJson(obj); + QCOMPARE(restored.ptrDepth, 0); + } + + // ── setMode always resets modifier buttons ── + + void testSetModeResetsModifierInPointerTargetMode() { + TypeSelectorPopup popup; + + // Set FieldType mode and select * modifier + popup.setMode(TypePopupMode::FieldType); + popup.setModifier(1); // select * + + // Now switch to PointerTarget mode — should reset to plain + popup.setMode(TypePopupMode::PointerTarget); + + // Verify: modifier buttons are hidden but internally reset to plain (modId=0) + // This means primitives will be visible in applyFilter + TypeEntry prim; + prim.entryKind = TypeEntry::Primitive; + prim.primitiveKind = NodeKind::Int32; + prim.displayName = "int32_t"; + + TypeEntry voidEntry; + voidEntry.entryKind = TypeEntry::Primitive; + voidEntry.primitiveKind = NodeKind::Pointer64; + voidEntry.displayName = "void"; + + popup.setTypes({prim, voidEntry}); + + // Both primitives should be visible (not filtered out) + auto* listView = popup.findChild(); + QVERIFY(listView); + int rowCount = listView->model()->rowCount(); + // Should have section header + 2 primitives = at least 3 rows + QVERIFY2(rowCount >= 3, + qPrintable(QString("Expected >=3 rows (header+2 prims), got %1").arg(rowCount))); + } + + // ── setModifier preselection ── + + void testSetModifierPreselects() { + TypeSelectorPopup popup; + + // Test * preselection + popup.setMode(TypePopupMode::FieldType); + popup.setModifier(1); + auto* btnGroup = popup.findChild(); + QVERIFY(btnGroup); + QCOMPARE(btnGroup->checkedId(), 1); + + // Test ** preselection + popup.setMode(TypePopupMode::FieldType); + popup.setModifier(2); + QCOMPARE(btnGroup->checkedId(), 2); + + // Test [n] preselection with count + popup.setMode(TypePopupMode::FieldType); + popup.setModifier(3, 8); + QCOMPARE(btnGroup->checkedId(), 3); + auto* countEdit = popup.findChild(QStringLiteral("arrayCountEdit")); + // Array count edit may not have objectName set; find via parent + // Just verify button group is correct + } + + // ── isValidPrimitivePtrTarget ── + + void testIsValidPrimitivePtrTarget() { + // Hex types → NOT valid (deref shows same hex as void*) + QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex8)); + QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex16)); + QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex32)); + QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex64)); + + // Pointer types → NOT valid (use composite * for chains) + QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Pointer32)); + QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Pointer64)); + + // Function pointers → NOT valid + QVERIFY(!isValidPrimitivePtrTarget(NodeKind::FuncPtr32)); + QVERIFY(!isValidPrimitivePtrTarget(NodeKind::FuncPtr64)); + + // Containers → NOT valid + QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Struct)); + QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Array)); + + // Value types → valid + QVERIFY(isValidPrimitivePtrTarget(NodeKind::Int32)); + QVERIFY(isValidPrimitivePtrTarget(NodeKind::UInt64)); + QVERIFY(isValidPrimitivePtrTarget(NodeKind::Float)); + QVERIFY(isValidPrimitivePtrTarget(NodeKind::Double)); + QVERIFY(isValidPrimitivePtrTarget(NodeKind::Bool)); + QVERIFY(isValidPrimitivePtrTarget(NodeKind::Vec3)); + QVERIFY(isValidPrimitivePtrTarget(NodeKind::UTF8)); + } + + // ── hex64* falls back to void* ── + + void testHex64StarFallsBackToVoidPointer() { + auto* doc = new RcxDocument(); + buildTwoRootTree(doc->tree); + doc->provider = std::make_unique(makeBuffer()); + + auto* splitter = new QSplitter(); + auto* ctrl = new RcxController(doc, nullptr); + ctrl->addSplitEditor(splitter); + + splitter->resize(800, 600); + splitter->show(); + QVERIFY(QTest::qWaitForWindowExposed(splitter)); + ctrl->refresh(); + QApplication::processEvents(); + + // Find the "x" field (Int32) + int xIdx = -1; + for (int i = 0; i < doc->tree.nodes.size(); i++) { + if (doc->tree.nodes[i].name == "x") { xIdx = i; break; } + } + QVERIFY(xIdx >= 0); + uint64_t xNodeId = doc->tree.nodes[xIdx].id; + + // Build a TypeEntry for hex64 + TypeEntry hexEntry; + hexEntry.entryKind = TypeEntry::Primitive; + hexEntry.primitiveKind = NodeKind::Hex64; + hexEntry.displayName = "hex64"; + + // Apply it with pointer modifier (fullText = "hex64*") + ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx, + hexEntry, QStringLiteral("hex64*")); + QApplication::processEvents(); + + // Should be a void pointer: Pointer64, ptrDepth=0, refId=0 + int idx = doc->tree.indexOfId(xNodeId); + QVERIFY(idx >= 0); + QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64); + QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0); + QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0)); + + delete ctrl; + delete splitter; + delete doc; + } + + void testHex8StarFallsBackToVoidPointer() { + auto* doc = new RcxDocument(); + buildTwoRootTree(doc->tree); + doc->provider = std::make_unique(makeBuffer()); + + auto* splitter = new QSplitter(); + auto* ctrl = new RcxController(doc, nullptr); + ctrl->addSplitEditor(splitter); + + splitter->resize(800, 600); + splitter->show(); + QVERIFY(QTest::qWaitForWindowExposed(splitter)); + ctrl->refresh(); + QApplication::processEvents(); + + int xIdx = -1; + for (int i = 0; i < doc->tree.nodes.size(); i++) { + if (doc->tree.nodes[i].name == "x") { xIdx = i; break; } + } + QVERIFY(xIdx >= 0); + uint64_t xNodeId = doc->tree.nodes[xIdx].id; + + TypeEntry hexEntry; + hexEntry.entryKind = TypeEntry::Primitive; + hexEntry.primitiveKind = NodeKind::Hex8; + hexEntry.displayName = "hex8"; + + ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx, + hexEntry, QStringLiteral("hex8*")); + QApplication::processEvents(); + + int idx = doc->tree.indexOfId(xNodeId); + QVERIFY(idx >= 0); + QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64); + QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0); + QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0)); + + delete ctrl; + delete splitter; + delete doc; + } + + void testPtr64StarFallsBackToVoidPointer() { + auto* doc = new RcxDocument(); + buildTwoRootTree(doc->tree); + doc->provider = std::make_unique(makeBuffer()); + + auto* splitter = new QSplitter(); + auto* ctrl = new RcxController(doc, nullptr); + ctrl->addSplitEditor(splitter); + + splitter->resize(800, 600); + splitter->show(); + QVERIFY(QTest::qWaitForWindowExposed(splitter)); + ctrl->refresh(); + QApplication::processEvents(); + + int xIdx = -1; + for (int i = 0; i < doc->tree.nodes.size(); i++) { + if (doc->tree.nodes[i].name == "x") { xIdx = i; break; } + } + QVERIFY(xIdx >= 0); + uint64_t xNodeId = doc->tree.nodes[xIdx].id; + + TypeEntry ptrEntry; + ptrEntry.entryKind = TypeEntry::Primitive; + ptrEntry.primitiveKind = NodeKind::Pointer64; + ptrEntry.displayName = "ptr64"; + + ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx, + ptrEntry, QStringLiteral("ptr64*")); + QApplication::processEvents(); + + int idx = doc->tree.indexOfId(xNodeId); + QVERIFY(idx >= 0); + QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64); + QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0); + QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0)); + + delete ctrl; + delete splitter; + delete doc; + } + + // ── Valid primitive pointers still work ── + + void testInt32StarStillCreatesPrimitivePointer() { + auto* doc = new RcxDocument(); + buildTwoRootTree(doc->tree); + doc->provider = std::make_unique(makeBuffer()); + + auto* splitter = new QSplitter(); + auto* ctrl = new RcxController(doc, nullptr); + ctrl->addSplitEditor(splitter); + + splitter->resize(800, 600); + splitter->show(); + QVERIFY(QTest::qWaitForWindowExposed(splitter)); + ctrl->refresh(); + QApplication::processEvents(); + + int xIdx = -1; + for (int i = 0; i < doc->tree.nodes.size(); i++) { + if (doc->tree.nodes[i].name == "x") { xIdx = i; break; } + } + QVERIFY(xIdx >= 0); + uint64_t xNodeId = doc->tree.nodes[xIdx].id; + + TypeEntry intEntry; + intEntry.entryKind = TypeEntry::Primitive; + intEntry.primitiveKind = NodeKind::Int32; + intEntry.displayName = "int32_t"; + + ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx, + intEntry, QStringLiteral("int32_t*")); + QApplication::processEvents(); + + int idx = doc->tree.indexOfId(xNodeId); + QVERIFY(idx >= 0); + QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64); + QCOMPARE(doc->tree.nodes[idx].ptrDepth, 1); + QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Int32); + QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0)); + + delete ctrl; + delete splitter; + delete doc; + } + + void testDoubleDoubleStarStillCreatesPrimitivePointer() { + auto* doc = new RcxDocument(); + buildTwoRootTree(doc->tree); + doc->provider = std::make_unique(makeBuffer()); + + auto* splitter = new QSplitter(); + auto* ctrl = new RcxController(doc, nullptr); + ctrl->addSplitEditor(splitter); + + splitter->resize(800, 600); + splitter->show(); + QVERIFY(QTest::qWaitForWindowExposed(splitter)); + ctrl->refresh(); + QApplication::processEvents(); + + int xIdx = -1; + for (int i = 0; i < doc->tree.nodes.size(); i++) { + if (doc->tree.nodes[i].name == "x") { xIdx = i; break; } + } + QVERIFY(xIdx >= 0); + uint64_t xNodeId = doc->tree.nodes[xIdx].id; + + TypeEntry dblEntry; + dblEntry.entryKind = TypeEntry::Primitive; + dblEntry.primitiveKind = NodeKind::Double; + dblEntry.displayName = "double"; + + ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx, + dblEntry, QStringLiteral("double**")); + QApplication::processEvents(); + + int idx = doc->tree.indexOfId(xNodeId); + QVERIFY(idx >= 0); + QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64); + QCOMPARE(doc->tree.nodes[idx].ptrDepth, 2); + QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Double); + QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0)); + + delete ctrl; + delete splitter; + delete doc; + } + + // ── Defense: compose/format treat invalid ptrDepth as void* ── + + void testComposeShowsVoidPtrForHexPtrDepth() { + // If a node somehow has ptrDepth>0 with hex elementKind + // (e.g. from old JSON), compose should show "void*" not "hex64*" + NodeTree tree; + tree.baseAddress = 0x1000; + + Node root; + root.kind = NodeKind::Struct; + root.name = "Test"; + root.structTypeName = "Test"; + root.parentId = 0; + tree.addNode(root); + uint64_t rootId = tree.nodes[0].id; + + Node ptr; + ptr.kind = NodeKind::Pointer64; + ptr.name = "badPtr"; + ptr.parentId = rootId; + ptr.offset = 0; + ptr.ptrDepth = 1; + ptr.elementKind = NodeKind::Hex64; // invalid target + tree.addNode(ptr); + + QByteArray buf(0x100, '\0'); + BufferProvider prov(buf); + + ComposeResult result = compose(tree, prov); + + // The composed text should NOT contain "hex64*" — the invalid target + // should fall through to normal void pointer display + QVERIFY2(!result.text.contains("hex64*"), + qPrintable("Should not show 'hex64*', got: " + result.text)); + } }; QTEST_MAIN(TestTypeSelector)