diff --git a/screenshot.png b/screenshot.png index 790d0a5..9ec25b3 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/src/compose.cpp b/src/compose.cpp index 3b1352d..d97078c 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -14,8 +14,9 @@ constexpr uint64_t kGoldenRatio = 0x9E3779B97F4A7C15ULL; struct ComposeState { QString text; QVector meta; - QSet visiting; // cycle detection for struct recursion - QSet ptrVisiting; // cycle guard for pointer expansions + QSet visiting; // cycle detection for struct recursion + QSet ptrVisiting; // cycle guard for pointer expansions + QSet virtualPtrRefs; // refIds currently being virtually expanded via pointer deref int currentLine = 0; int typeW = kColType; // global type column width (fallback) int nameW = kColName; // global name column width (fallback) @@ -64,7 +65,6 @@ uint32_t computeMarkers(const Node& node, const Provider& /*prov*/, uint64_t /*addr*/, bool isCont, int /*depth*/) { uint32_t mask = 0; if (isCont) mask |= (1u << M_CONT); - if (node.kind == NodeKind::Padding) mask |= (1u << M_PAD); // No ambient validation markers — errors only shown during inline editing. return mask; } @@ -118,14 +118,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, int typeW = state.effectiveTypeW(scopeId); int nameW = state.effectiveNameW(scopeId); - // Line count: padding wraps at 8 bytes per line - int numLines; - if (node.kind == NodeKind::Padding) { - int totalBytes = qMax(1, node.arrayLen); - numLines = (totalBytes + 7) / 8; - } else { - numLines = linesForKind(node.kind); - } + int numLines = linesForKind(node.kind); // Resolve pointer target name for display QString ptrTypeOverride; @@ -156,12 +149,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, // Set byte count for hex preview lines (used for per-byte change highlighting) if (isHexPreview(node.kind)) { - if (node.kind == NodeKind::Padding) { - int totalSz = qMax(1, node.arrayLen); - lm.lineByteCount = qMin(8, totalSz - sub * 8); - } else { - lm.lineByteCount = sizeForKind(node.kind); - } + lm.lineByteCount = sizeForKind(node.kind); } QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub, @@ -430,29 +418,42 @@ void composeNode(ComposeState& state, const NodeTree& tree, QString ptrTargetName = resolvePointerTarget(tree, node.refId); QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName); + // Check if this pointer has materialized children (from materializeRefChildren) + QVector ptrChildren = state.childMap.value(node.id); + bool hasMaterialized = !ptrChildren.isEmpty(); + + // Force collapsed if this refId is already being virtually expanded + // (prevents infinite recursion in virtual expansion mode). + // Materialized children bypass this — they are real tree nodes with + // independent collapsed state, so recursion is bounded by the tree. + bool forceCollapsed = !hasMaterialized + && state.virtualPtrRefs.contains(node.refId); + bool effectiveCollapsed = node.collapsed || forceCollapsed; + // Emit merged fold header: "Type* Name {" (expanded) or "Type* Name -> val" (collapsed) { LineMeta lm; lm.nodeIdx = nodeIdx; lm.nodeId = node.id; lm.depth = depth; - lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header; + lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header; lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); lm.offsetAddr = tree.baseAddress + absAddr; lm.nodeKind = node.kind; lm.foldHead = true; - lm.foldCollapsed = node.collapsed; + lm.foldCollapsed = effectiveCollapsed; lm.foldLevel = computeFoldLevel(depth, true); lm.markerMask = computeMarkers(node, prov, absAddr, false, depth); + if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE); lm.effectiveTypeW = typeW; lm.effectiveNameW = nameW; lm.pointerTargetName = ptrTargetName; - state.emitLine(fmt::fmtPointerHeader(node, depth, node.collapsed, + state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed, prov, absAddr, ptrTypeOverride, typeW, nameW), lm); } - if (!node.collapsed) { + if (!effectiveCollapsed) { int sz = node.byteSize(); uint64_t ptrVal = 0; if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) { @@ -480,18 +481,42 @@ void composeNode(ComposeState& state, const NodeTree& tree, if (!ptrReadable) pBase = (uint64_t)0 - tree.baseAddress; - qulonglong key = pBase ^ (node.refId * kGoldenRatio); - if (!state.ptrVisiting.contains(key)) { - state.ptrVisiting.insert(key); - int refIdx = tree.indexOfId(node.refId); - if (refIdx >= 0) { - const Node& ref = tree.nodes[refIdx]; - if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) - composeParent(state, tree, childProv, refIdx, - depth, pBase, ref.id, - /*isArrayChild=*/true); + if (hasMaterialized) { + // Render materialized children at the pointer target address. + // These are real tree nodes with independent state — use rootId + // so resolveAddr computes offsets relative to the pointer target. + std::sort(ptrChildren.begin(), ptrChildren.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + for (int childIdx : ptrChildren) { + composeNode(state, tree, childProv, childIdx, depth + 1, + pBase, node.id, false, node.id); + } + } else { + // Virtual expansion via ref struct definition. + // Temporarily remove the ref struct from visiting so composeParent + // doesn't hit the struct-level cycle guard. The ptrVisiting mechanism + // handles actual address-level pointer cycles, and virtualPtrRefs + // prevents infinite virtual recursion (inner self-referential pointers + // are force-collapsed with M_CYCLE for the user to materialize). + qulonglong key = pBase ^ (node.refId * kGoldenRatio); + if (!state.ptrVisiting.contains(key)) { + state.ptrVisiting.insert(key); + int refIdx = tree.indexOfId(node.refId); + if (refIdx >= 0) { + const Node& ref = tree.nodes[refIdx]; + if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) { + bool wasVisiting = state.visiting.remove(node.refId); + state.virtualPtrRefs.insert(node.refId); + composeParent(state, tree, childProv, refIdx, + depth, pBase, ref.id, + /*isArrayChild=*/true); + state.virtualPtrRefs.remove(node.refId); + if (wasVisiting) state.visiting.insert(node.refId); + } + } + state.ptrVisiting.remove(key); } - state.ptrVisiting.remove(key); } // Footer for pointer fold @@ -571,7 +596,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR // Include struct/array names - they now use columnar layout too int maxNameLen = kMinNameW; for (const Node& node : tree.nodes) { - // Skip hex/padding (they show ASCII preview, not name column) + // Skip hex (they show ASCII preview, not name column) if (isHexPreview(node.kind)) continue; maxNameLen = qMax(maxNameLen, (int)node.name.size()); } @@ -590,7 +615,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR const Node& child = tree.nodes[childIdx]; scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size()); - // Name width (skip hex/padding, but include containers) + // Name width (skip hex, but include containers) if (!isHexPreview(child.kind)) { scopeMaxName = qMax(scopeMaxName, (int)child.name.size()); } @@ -622,7 +647,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR const Node& child = tree.nodes[childIdx]; rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size()); - // Name width (skip hex/padding, include containers) + // Name width (skip hex, include containers) if (!isHexPreview(child.kind)) { rootMaxName = qMax(rootMaxName, (int)child.name.size()); } diff --git a/src/controller.cpp b/src/controller.cpp index 4a472b2..f1df75a 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -2078,7 +2078,7 @@ void RcxController::onRefreshTick() { uint64_t rootId = m_viewRootId; if (rootId == 0 && !m_doc->tree.nodes.isEmpty()) rootId = m_doc->tree.nodes[0].id; - collectPointerRanges(rootId, 0, 0, 4, visited, ranges); + collectPointerRanges(rootId, 0, 0, 99, visited, ranges); } m_readInFlight = true; diff --git a/src/editor.cpp b/src/editor.cpp index b3593e1..ddf4a45 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -141,7 +141,7 @@ void RcxEditor::setupScintilla() { m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_EDITABLE, 5 /*INDIC_HIDDEN*/); - // Hex/Padding node dim indicator — overrides text color + // Hex node dim indicator — overrides text color m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/); @@ -241,9 +241,6 @@ void RcxEditor::setupMarkers() { // M_CONT (0): continuation line (metadata only, no visual) m_sci->markerDefine(QsciScintilla::Invisible, M_CONT); - // M_PAD (1): padding line (metadata only, no visual) - m_sci->markerDefine(QsciScintilla::Invisible, M_PAD); - // M_PTR0 (2): right triangle m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0); @@ -1038,9 +1035,6 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, if (lm->nodeIdx < 0) return false; - // Padding: reject value editing (hex bytes are display-only) - if (t == EditTarget::Value && lm->nodeKind == NodeKind::Padding) - return false; // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only) if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind)) return false; @@ -1221,9 +1215,6 @@ static bool hitTestTarget(QsciScintilla* sci, } return false; } - // Padding nodes: hex bytes are display-only, not editable - if (outTarget == EditTarget::Value && lm.nodeKind == NodeKind::Padding) - return false; // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only) if ((outTarget == EditTarget::Name || outTarget == EditTarget::Value) && isHexNode(lm.nodeKind)) return false; @@ -1681,9 +1672,6 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { (target == EditTarget::BaseAddress || target == EditTarget::Source || target == EditTarget::RootClassType || target == EditTarget::RootClassName))) return false; - // Padding: reject value editing (display-only hex bytes) - if (target == EditTarget::Value && lm->nodeKind == NodeKind::Padding) - return false; // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only) if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind)) return false; diff --git a/src/format.cpp b/src/format.cpp index 4003a18..a59ebd1 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -293,7 +293,6 @@ static QString readValueImpl(const Node& node, const Provider& prov, line += QStringLiteral("]"); return line; } - case NodeKind::Padding: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2); case NodeKind::UTF8: { QByteArray bytes = prov.readBytes(addr, node.strLen); int end = bytes.indexOf('\0'); @@ -344,21 +343,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov, return ind + QString(prefixW, ' ') + val + cmtSuffix; } - // Hex nodes and Padding: hex byte preview (ASCII padded to colName to align with value column) + // Hex nodes: hex byte preview (ASCII padded to colName to align with value column) if (isHexPreview(node.kind)) { - if (node.kind == NodeKind::Padding) { - const int totalSz = qMax(1, node.arrayLen); - const int lineOff = subLine * 8; - const int lineBytes = qMin(8, totalSz - lineOff); - QByteArray b = prov.isReadable(addr + lineOff, lineBytes) - ? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0'); - QString ascii = bytesToAscii(b, lineBytes).leftJustified(colName, ' '); - QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1 - if (subLine == 0) - return ind + type + SEP + ascii + SEP + hex + cmtSuffix; - return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix; - } - // Hex8..Hex64: single line, ASCII padded to colName so hex column aligns with value column const int sz = sizeForKind(node.kind); QByteArray b = prov.isReadable(addr, sz) ? prov.readBytes(addr, sz) : QByteArray(sz, '\0'); diff --git a/src/generator.cpp b/src/generator.cpp index f65f9d5..ec2fa6a 100644 --- a/src/generator.cpp +++ b/src/generator.cpp @@ -50,7 +50,6 @@ static QString cTypeName(NodeKind kind) { case NodeKind::Mat4x4: return QStringLiteral("float"); case NodeKind::UTF8: return QStringLiteral("char"); case NodeKind::UTF16: return QStringLiteral("wchar_t"); - case NodeKind::Padding: return QStringLiteral("uint8_t"); default: return QStringLiteral("uint8_t"); } } @@ -123,8 +122,6 @@ static QString emitField(GenContext& ctx, const Node& node) { return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc; case NodeKind::UTF16: return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc; - case NodeKind::Padding: - return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::Padding), name).arg(qMax(1, node.arrayLen)) + oc; case NodeKind::Pointer32: { if (node.refId != 0) { int refIdx = tree.indexOfId(node.refId); @@ -169,7 +166,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) { auto emitPadRun = [&](int offset, int size) { if (size <= 0) return; ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n") - .arg(ctx.cType(NodeKind::Padding)) + .arg(QStringLiteral("uint8_t")) .arg(ctx.uniquePadName()) .arg(QString::number(size, 16).toUpper()) .arg(offsetComment(offset)); diff --git a/src/mcp/mcp_bridge.cpp b/src/mcp/mcp_bridge.cpp index 97b6fb1..e819b9b 100644 --- a/src/mcp/mcp_bridge.cpp +++ b/src/mcp/mcp_bridge.cpp @@ -248,7 +248,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { "collapse: {op:'collapse', nodeId:'ID', collapsed:true}. " "Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. " "Kinds: Hex8 Hex16 Hex32 Hex64 Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 " - "Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Padding Struct Array"}, + "Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Struct Array"}, {"inputSchema", QJsonObject{ {"type", "object"}, {"properties", QJsonObject{ diff --git a/src/resources.qrc b/src/resources.qrc index 32d6fb1..6db63dc 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -49,5 +49,7 @@ vsicons/symbol-ruler.svg vsicons/settings-gear.svg vsicons/chevron-down.svg + vsicons/folder.svg + vsicons/symbol-enum.svg diff --git a/src/typeselectorpopup.cpp b/src/typeselectorpopup.cpp index d1e25dd..fb3359c 100644 --- a/src/typeselectorpopup.cpp +++ b/src/typeselectorpopup.cpp @@ -122,7 +122,7 @@ public: return; } - // 18px gutter: side triangle if current + // Gutter: side triangle if current if (m_hasCurrent && m_filtered && row >= 0 && row < m_filtered->size()) { const TypeEntry& entry = (*m_filtered)[row]; bool isCurrent = false; @@ -131,13 +131,13 @@ public: else if (m_current->entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite) isCurrent = (entry.structId == m_current->structId); if (isCurrent) { - painter->setPen(t.syntaxType); + painter->setPen(t.text); painter->setFont(m_font); - painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter, + painter->drawText(QRect(x, y, 10, h), Qt::AlignCenter, QString(QChar(0x25B8))); } } - x += 18; + x += 10; // Icon 16x16 — only for composite entries bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size() @@ -369,6 +369,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) m_listView->setFrameShape(QFrame::NoFrame); m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_listView->setMouseTracking(true); + m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers); m_listView->viewport()->setAttribute(Qt::WA_Hover, true); m_listView->installEventFilter(this); @@ -491,7 +492,7 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) { QString text = t.classKeyword.isEmpty() ? t.displayName : (t.classKeyword + QStringLiteral(" ") + t.displayName); - int w = 18 + 20 + fm.horizontalAdvance(text) + 16; + int w = 10 + 20 + fm.horizontalAdvance(text) + 16; if (w > maxTextW) maxTextW = w; } int popupW = qBound(280, maxTextW + 24, 500); diff --git a/src/workspace_model.h b/src/workspace_model.h index d5b7c80..9838299 100644 --- a/src/workspace_model.h +++ b/src/workspace_model.h @@ -1,62 +1,76 @@ #pragma once #include "core.h" +#include #include #include #include namespace rcx { -// Recursively add children of parentId as tree items under parentItem. -inline void addWorkspaceChildren(QStandardItem* parentItem, - const NodeTree& tree, - uint64_t parentId, - void* subPtr) { - QVector children = tree.childrenOf(parentId); - std::sort(children.begin(), children.end(), [&](int a, int b) { - return tree.nodes[a].offset < tree.nodes[b].offset; - }); +struct TabInfo { + const NodeTree* tree; + QString name; + void* subPtr; // QMdiSubWindow* as void* +}; - for (int idx : children) { - const Node& node = tree.nodes[idx]; +// Sentinel value stored in UserRole+1 to mark the Project group node. +static constexpr uint64_t kGroupSentinel = ~uint64_t(0); - // Skip hex preview nodes — they are padding/filler, not meaningful fields - if (isHexNode(node.kind)) continue; - - QString display; - if (node.kind == NodeKind::Struct) { - QString typeName = node.structTypeName.isEmpty() - ? node.name : node.structTypeName; - display = QStringLiteral("%1 (%2)") - .arg(typeName, node.resolvedClassKeyword()); - } else { - display = QStringLiteral("%1 (%2)") - .arg(node.name, QString::fromLatin1(kindToString(node.kind))); - } - - auto* item = new QStandardItem(display); - item->setData(QVariant::fromValue(subPtr), Qt::UserRole); - if (node.kind == NodeKind::Struct) - item->setData(QVariant::fromValue(node.id), Qt::UserRole + 1); - item->setData(QVariant::fromValue(node.id), Qt::UserRole + 2); // nodeId for scroll - - if (node.kind == NodeKind::Struct) - addWorkspaceChildren(item, tree, node.id, subPtr); - - parentItem->appendRow(item); - } -} - -inline void buildWorkspaceModel(QStandardItemModel* model, - const NodeTree& tree, - const QString& projectName, - void* subPtr = nullptr) { +inline void buildProjectExplorer(QStandardItemModel* model, + const QVector& tabs) { model->clear(); model->setHorizontalHeaderLabels({QStringLiteral("Name")}); - auto* projectItem = new QStandardItem(projectName); - projectItem->setData(QVariant::fromValue(subPtr), Qt::UserRole); + // Single "Project" root with folder icon + void* firstSub = tabs.isEmpty() ? nullptr : tabs[0].subPtr; + auto* projectItem = new QStandardItem(QIcon(":/vsicons/folder.svg"), + QStringLiteral("Project")); + projectItem->setData(QVariant::fromValue(firstSub), Qt::UserRole); + projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1); - addWorkspaceChildren(projectItem, tree, 0, subPtr); + // Collect all top-level structs/enums across all tabs + QVector> types, enums; + for (const auto& tab : tabs) { + QVector topLevel = tab.tree->childrenOf(0); + for (int idx : topLevel) { + const Node& n = tab.tree->nodes[idx]; + if (n.kind != NodeKind::Struct) continue; + if (n.resolvedClassKeyword() == QStringLiteral("enum")) + enums.append({&n, tab.subPtr}); + else + types.append({&n, tab.subPtr}); + } + } + + auto nameOf = [](const Node* n) { + return n->structTypeName.isEmpty() ? n->name : n->structTypeName; + }; + auto cmpName = [&](const std::pair& a, + const std::pair& b) { + return nameOf(a.first).compare(nameOf(b.first), Qt::CaseInsensitive) < 0; + }; + std::sort(types.begin(), types.end(), cmpName); + std::sort(enums.begin(), enums.end(), cmpName); + + for (const auto& [n, subPtr] : types) { + QString display = QStringLiteral("%1 (%2)") + .arg(nameOf(n), n->resolvedClassKeyword()); + auto* item = new QStandardItem( + QIcon(":/vsicons/symbol-structure.svg"), display); + item->setData(QVariant::fromValue(subPtr), Qt::UserRole); + item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1); + projectItem->appendRow(item); + } + + for (const auto& [n, subPtr] : enums) { + QString display = QStringLiteral("%1 (%2)") + .arg(nameOf(n), n->resolvedClassKeyword()); + auto* item = new QStandardItem( + QIcon(":/vsicons/symbol-enum.svg"), display); + item->setData(QVariant::fromValue(subPtr), Qt::UserRole); + item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1); + projectItem->appendRow(item); + } model->appendRow(projectItem); } diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index 3294b31..e28d4b6 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -89,7 +89,7 @@ private slots: QCOMPARE(result.meta[2].lineKind, LineKind::Footer); } - void testPaddingMarker() { + void testHexNodeCompose() { NodeTree tree; tree.baseAddress = 0; @@ -100,19 +100,18 @@ private slots: int ri = tree.addNode(root); uint64_t rootId = tree.nodes[ri].id; - Node pad; - pad.kind = NodeKind::Padding; - pad.name = "pad"; - pad.parentId = rootId; - pad.offset = 0; - tree.addNode(pad); + Node hex; + hex.kind = NodeKind::Hex8; + hex.name = "pad"; + hex.parentId = rootId; + hex.offset = 0; + tree.addNode(hex); NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + padding + root footer = 3 + // CommandRow + hex node + root footer = 3 QCOMPARE(result.meta.size(), 3); - QVERIFY(result.meta[1].markerMask & (1u << M_PAD)); QCOMPARE(result.meta[1].depth, 1); // Line 2 is root footer diff --git a/tests/test_controller.cpp b/tests/test_controller.cpp index 4ff6789..e1a1310 100644 --- a/tests/test_controller.cpp +++ b/tests/test_controller.cpp @@ -34,9 +34,8 @@ static void buildSmallTree(NodeTree& tree) { field(0, NodeKind::UInt32, "field_u32"); // 4 bytes field(4, NodeKind::Float, "field_float"); // 4 bytes field(8, NodeKind::UInt8, "field_u8"); // 1 byte - field(9, NodeKind::Padding, "pad0"); // 3 bytes padding - // Set padding arrayLen = 3 for 3-byte padding - tree.nodes.last().arrayLen = 3; + field(9, NodeKind::Hex16, "pad0"); // 2 bytes + field(11, NodeKind::Hex8, "pad1"); // 1 byte field(12, NodeKind::Hex32, "field_hex"); // 4 bytes } @@ -282,47 +281,6 @@ private slots: QVERIFY(newIdx >= 0); } - // ── Test: Padding value edit is effectively blocked at controller level ── - void testPaddingValueEditIsBlocked() { - // Find the padding node - int padIdx = -1; - for (int i = 0; i < m_doc->tree.nodes.size(); i++) { - if (m_doc->tree.nodes[i].kind == NodeKind::Padding) { padIdx = i; break; } - } - QVERIFY(padIdx >= 0); - uint64_t addr = m_doc->tree.computeOffset(padIdx); - - // Read original data at padding offset - int padSize = m_doc->tree.nodes[padIdx].byteSize(); - QByteArray origData = m_doc->provider->readBytes(addr, padSize); - - // The context menu blocks Padding editing, so the controller's setNodeValue - // would only be called if the editing UI somehow allows it. But let's verify - // the editor correctly blocks it. - // Find padding line in composed output - ComposeResult result = m_doc->compose(); - int paddingLine = -1; - for (int i = 0; i < result.meta.size(); i++) { - if (result.meta[i].nodeKind == NodeKind::Padding && - result.meta[i].lineKind == LineKind::Field) { - paddingLine = i; - break; - } - } - QVERIFY(paddingLine >= 0); - - m_editor->applyDocument(result); - QApplication::processEvents(); - - // beginInlineEdit(Value) on Padding line must be rejected - QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine)); - QVERIFY(!m_editor->isEditing()); - - // Data must be unchanged - QByteArray afterData = m_doc->provider->readBytes(addr, padSize); - QCOMPARE(afterData, origData); - } - // ── Test: setNodeValue with Hex32 (space-separated hex bytes) ── void testSetNodeValueHex() { int idx = -1; diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index 5b276c2..a646c73 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -170,9 +170,10 @@ static NodeTree makeTestTree() { n.parentId = rootId; n.offset = off; tree.addNode(n); }; - auto pad = [&](int off, int len, const char* name) { - Node n; n.kind = NodeKind::Padding; n.name = name; - n.parentId = rootId; n.offset = off; n.arrayLen = len; + auto pad = [&](int off, int /*len*/, const char* name) { + // 4-byte padding → Hex32 (all usages in this test pass len=4) + Node n; n.kind = NodeKind::Hex32; n.name = name; + n.parentId = rootId; n.offset = off; tree.addNode(n); }; auto arr = [&](int off, NodeKind ek, int len, const char* name) { @@ -278,8 +279,8 @@ static NodeTree makeTestTree() { n.kind = NodeKind::UInt16; n.name = "Length"; n.offset = 0; tree.addNode(n); n.kind = NodeKind::UInt16; n.name = "MaximumLength"; n.offset = 2; tree.addNode(n); - n.kind = NodeKind::Padding; n.name = "Pad"; - n.offset = 4; n.arrayLen = 4; tree.addNode(n); + n.kind = NodeKind::Hex32; n.name = "Pad"; + n.offset = 4; n.arrayLen = 1; tree.addNode(n); n.kind = NodeKind::Pointer64; n.name = "Buffer"; n.offset = 8; n.arrayLen = 1; tree.addNode(n); } @@ -751,70 +752,6 @@ private slots: m_editor->applyDocument(m_result); } - // ── Test: Padding line rejects value editing ── - void testPaddingLineRejectsValueEdit() { - m_editor->applyDocument(m_result); - - // Find a Padding line in the composed output - int paddingLine = -1; - for (int i = 0; i < m_result.meta.size(); i++) { - if (m_result.meta[i].nodeKind == NodeKind::Padding && - m_result.meta[i].lineKind == LineKind::Field) { - paddingLine = i; - break; - } - } - QVERIFY2(paddingLine >= 0, "Should have at least one Padding line in test tree"); - - const LineMeta* lm = m_editor->metaForLine(paddingLine); - QVERIFY(lm); - QCOMPARE(lm->nodeKind, NodeKind::Padding); - - // Value edit on Padding MUST be rejected (the bug fix) - QVERIFY2(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine), - "Value edit should be rejected on Padding lines"); - QVERIFY(!m_editor->isEditing()); - - // Name edit on Padding SHOULD succeed (ASCII preview column is editable) - bool ok = m_editor->beginInlineEdit(EditTarget::Name, paddingLine); - QVERIFY2(ok, "Name edit should be allowed on Padding lines (ASCII preview)"); - QVERIFY(m_editor->isEditing()); - m_editor->cancelInlineEdit(); - - // Type edit on Padding SHOULD succeed (emits popup signal) - QSignalSpy typeSpy(m_editor, &RcxEditor::typePickerRequested); - ok = m_editor->beginInlineEdit(EditTarget::Type, paddingLine); - QVERIFY2(ok, "Type edit should be allowed on Padding lines"); - QCOMPARE(typeSpy.count(), 1); - } - - // ── Test: resolvedSpanFor rejects Value on Padding (defense-in-depth) ── - void testPaddingLineRejectsValueSpan() { - m_editor->applyDocument(m_result); - - // Find a Padding line - int paddingLine = -1; - for (int i = 0; i < m_result.meta.size(); i++) { - if (m_result.meta[i].nodeKind == NodeKind::Padding && - m_result.meta[i].lineKind == LineKind::Field) { - paddingLine = i; - break; - } - } - QVERIFY(paddingLine >= 0); - - const LineMeta* lm = m_editor->metaForLine(paddingLine); - QVERIFY(lm); - - // valueSpanFor returns valid (shared with Hex via KF_HexPreview) - ColumnSpan vs = RcxEditor::valueSpan(*lm, 200); - QVERIFY2(vs.valid, "valueSpanFor should return valid for Padding (shared HexPreview flag)"); - - // But beginInlineEdit should still reject it - QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine)); - QVERIFY(!m_editor->isEditing()); - } - // ── Test: value edit commit fires signal with typed text ── void testValueEditCommitUpdatesSignal() { m_editor->applyDocument(m_result); @@ -823,8 +760,6 @@ private slots: const LineMeta* lm = m_editor->metaForLine(kFirstDataLine); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::Field); - QVERIFY(lm->nodeKind != NodeKind::Padding); - // Begin value edit bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine); QVERIFY(ok); diff --git a/tests/test_generator.cpp b/tests/test_generator.cpp index b933982..6e2e84e 100644 --- a/tests/test_generator.cpp +++ b/tests/test_generator.cpp @@ -418,30 +418,6 @@ private slots: QVERIFY(result.contains("wchar_t wname[32];")); } - // ── Padding node ── - - void testPaddingNode() { - rcx::NodeTree tree; - rcx::Node root; - root.kind = rcx::NodeKind::Struct; - root.name = "PadTest"; - root.structTypeName = "PadTest"; - root.parentId = 0; - int ri = tree.addNode(root); - uint64_t rootId = tree.nodes[ri].id; - - rcx::Node pad; - pad.kind = rcx::NodeKind::Padding; - pad.name = "reserved"; - pad.parentId = rootId; - pad.offset = 0; - pad.arrayLen = 16; - tree.addNode(pad); - - QString result = rcx::renderCpp(tree, rootId); - QVERIFY(result.contains("uint8_t reserved[16];")); - } - // ── Full SDK export (multiple root structs) ── void testFullSdkExport() { diff --git a/tests/test_new_features.cpp b/tests/test_new_features.cpp index df89b16..ecb81a7 100644 --- a/tests/test_new_features.cpp +++ b/tests/test_new_features.cpp @@ -304,39 +304,6 @@ private slots: QVERIFY(result.contains("float speed;")); } - void testGenerator_typeAliases_padding() { - // Padding gap and tail padding should use aliased uint8_t - NodeTree tree; - Node root; - root.kind = NodeKind::Struct; - root.name = "PadTest"; - root.structTypeName = "PadTest"; - root.parentId = 0; - int ri = tree.addNode(root); - uint64_t rootId = tree.nodes[ri].id; - - Node f1; - f1.kind = NodeKind::UInt32; - f1.name = "a"; - f1.parentId = rootId; - f1.offset = 0; - tree.addNode(f1); - - Node f2; - f2.kind = NodeKind::UInt32; - f2.name = "b"; - f2.parentId = rootId; - f2.offset = 8; // gap of 4 bytes at offset 4 - tree.addNode(f2); - - QHash aliases; - aliases[NodeKind::Padding] = "BYTE"; - - QString result = renderCpp(tree, rootId, &aliases); - // Padding gap should use the alias - QVERIFY(result.contains("BYTE _pad")); - } - void testGenerator_typeAliases_array() { // Array element type should use alias NodeTree tree; @@ -547,134 +514,92 @@ private slots: void testWorkspace_simpleTree() { auto tree = makeSimpleTree(); QStandardItemModel model; - buildWorkspaceModel(&model, tree, "TestProject.rcx"); + QVector tabs = {{ &tree, "TestProject.rcx", nullptr }}; + buildProjectExplorer(&model, tabs); - // 1 top-level item (the project) + // Single "Project" root QCOMPARE(model.rowCount(), 1); QStandardItem* project = model.item(0); - QCOMPARE(project->text(), QString("TestProject.rcx")); + QCOMPARE(project->text(), QString("Project")); - // Project has 1 child: the Player struct + // 1 type directly under Project: Player (no member fields) QCOMPARE(project->rowCount(), 1); - QStandardItem* player = project->child(0); - QVERIFY(player->text().contains("Player")); - QVERIFY(player->text().contains("struct")); - - // Player struct has 2 children: health, speed - QCOMPARE(player->rowCount(), 2); - QVERIFY(player->child(0)->text().contains("health")); - QVERIFY(player->child(1)->text().contains("speed")); + QVERIFY(project->child(0)->text().contains("Player")); + QVERIFY(project->child(0)->text().contains("struct")); + QCOMPARE(project->child(0)->rowCount(), 0); } void testWorkspace_twoRootTree() { auto tree = makeTwoRootTree(); QStandardItemModel model; - buildWorkspaceModel(&model, tree, "TwoRoot.rcx"); + QVector tabs = {{ &tree, "TwoRoot.rcx", nullptr }}; + buildProjectExplorer(&model, tabs); QCOMPARE(model.rowCount(), 1); QStandardItem* project = model.item(0); - // 2 root struct children: Alpha and Bravo + // 2 types sorted alphabetically: Alpha, Bravo (no field children) QCOMPARE(project->rowCount(), 2); QVERIFY(project->child(0)->text().contains("Alpha")); QVERIFY(project->child(1)->text().contains("Bravo")); - - // Each has 1 field child - QCOMPARE(project->child(0)->rowCount(), 1); - QVERIFY(project->child(0)->child(0)->text().contains("flagsA")); - QCOMPARE(project->child(1)->rowCount(), 1); - QVERIFY(project->child(1)->child(0)->text().contains("flagsB")); + QCOMPARE(project->child(0)->rowCount(), 0); + QCOMPARE(project->child(1)->rowCount(), 0); } void testWorkspace_richTree_rootCount() { auto tree = makeRichTree(); QStandardItemModel model; - buildWorkspaceModel(&model, tree, "Rich.rcx"); + QVector tabs = {{ &tree, "Rich.rcx", nullptr }}; + buildProjectExplorer(&model, tabs); QStandardItem* project = model.item(0); - QCOMPARE(project->rowCount(), 3); // Pet, Cat, Ball + QCOMPARE(project->rowCount(), 3); // Ball, Cat, Pet (sorted) } - void testWorkspace_richTree_petChildren() { + void testWorkspace_richTree_sorted() { auto tree = makeRichTree(); QStandardItemModel model; - buildWorkspaceModel(&model, tree, "Rich.rcx"); + QVector tabs = {{ &tree, "Rich.rcx", nullptr }}; + buildProjectExplorer(&model, tabs); - QStandardItem* pet = model.item(0)->child(0); - QVERIFY(pet->text().contains("Pet")); - // Pet has 2 non-hex children: name (UTF8), owner (Pointer64) - QCOMPARE(pet->rowCount(), 2); - QVERIFY(pet->child(0)->text().contains("name")); - QVERIFY(pet->child(1)->text().contains("owner")); - } - - void testWorkspace_richTree_catNesting() { - auto tree = makeRichTree(); - QStandardItemModel model; - buildWorkspaceModel(&model, tree, "Rich.rcx"); - - QStandardItem* cat = model.item(0)->child(1); - QVERIFY(cat->text().contains("Cat")); - - // Find the nested "Pet" struct child (base) - QStandardItem* base = nullptr; - for (int i = 0; i < cat->rowCount(); i++) { - if (cat->child(i)->text().contains("Pet") && - cat->child(i)->text().contains("struct")) { - base = cat->child(i); - break; - } - } - QVERIFY2(base != nullptr, "Cat should have a nested Pet struct child"); - - // base has structId set - QVERIFY(base->data(Qt::UserRole + 1).isValid()); - - // base should have its own children (name + owner) - QCOMPARE(base->rowCount(), 2); - } - - void testWorkspace_richTree_ballChildren() { - auto tree = makeRichTree(); - QStandardItemModel model; - buildWorkspaceModel(&model, tree, "Rich.rcx"); - - QStandardItem* ball = model.item(0)->child(2); - QVERIFY(ball->text().contains("Ball")); - - // Ball has 3 non-hex children: speed, position, color - QCOMPARE(ball->rowCount(), 3); - QVERIFY(ball->child(0)->text().contains("speed")); - QVERIFY(ball->child(1)->text().contains("position")); - QVERIFY(ball->child(2)->text().contains("color")); + QStandardItem* project = model.item(0); + // Sorted alphabetically: Ball, Cat, Pet + QVERIFY(project->child(0)->text().contains("Ball")); + QVERIFY(project->child(1)->text().contains("Cat")); + QVERIFY(project->child(2)->text().contains("Pet")); + // No member fields under type nodes + QCOMPARE(project->child(0)->rowCount(), 0); + QCOMPARE(project->child(1)->rowCount(), 0); + QCOMPARE(project->child(2)->rowCount(), 0); } void testWorkspace_emptyTree() { NodeTree tree; QStandardItemModel model; - buildWorkspaceModel(&model, tree, "Empty.rcx"); + QVector tabs = {{ &tree, "Empty.rcx", nullptr }}; + buildProjectExplorer(&model, tabs); + // Still has the "Project" root, just no children QCOMPARE(model.rowCount(), 1); + QCOMPARE(model.item(0)->text(), QString("Project")); QCOMPARE(model.item(0)->rowCount(), 0); } void testWorkspace_structIdRole() { auto tree = makeSimpleTree(); QStandardItemModel model; - buildWorkspaceModel(&model, tree, "Test.rcx"); + QVector tabs = {{ &tree, "Test.rcx", nullptr }}; + buildProjectExplorer(&model, tabs); QStandardItem* project = model.item(0); - // Project item should NOT have structId - QVERIFY(!project->data(Qt::UserRole + 1).isValid()); + // Project root has kGroupSentinel + QCOMPARE(project->data(Qt::UserRole + 1).toULongLong(), kGroupSentinel); - // Player struct should have structId + // Player type item should have structId QStandardItem* player = project->child(0); QVERIFY(player->data(Qt::UserRole + 1).isValid()); QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0); - - // health field should NOT have structId - QStandardItem* health = player->child(0); - QVERIFY(!health->data(Qt::UserRole + 1).isValid()); + QVERIFY(player->data(Qt::UserRole + 1).toULongLong() != kGroupSentinel); } // ═══════════════════════════════════════════════════ diff --git a/tests/test_validation.cpp b/tests/test_validation.cpp index 9236741..588e8f4 100644 --- a/tests/test_validation.cpp +++ b/tests/test_validation.cpp @@ -57,8 +57,8 @@ static void buildValidationTree(NodeTree& tree) { field(46, NodeKind::Hex32, "field_h32"); field(50, NodeKind::Hex64, "field_h64"); field(58, NodeKind::Pointer64, "field_ptr"); - field(66, NodeKind::Padding, "pad0"); - tree.nodes.last().arrayLen = 6; + field(66, NodeKind::Hex32, "pad0"); + field(70, NodeKind::Hex16, "pad1"); fieldArr(72, NodeKind::UInt32, 4, "field_arr"); } @@ -725,9 +725,9 @@ private slots: QCOMPARE(m_doc->undoStack.count(), 0); } - // ── changeNodeKind size transitions: shrink inserts padding ── + // ── changeNodeKind size transitions: shrink inserts hex nodes ── - void testChangeKindShrinkInsertsPadding() { + void testChangeKindShrinkInsertsHexNodes() { int idx = findNode(m_doc->tree, "field_u32"); QVERIFY(idx >= 0); QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); // 4 bytes @@ -737,7 +737,7 @@ private slots: QApplication::processEvents(); QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt8); - // Should have inserted padding nodes (Hex16 + Hex8 = 3 bytes, or similar) + // Should have inserted hex nodes (Hex16 + Hex8 = 3 bytes, or similar) QVERIFY(m_doc->tree.nodes.size() > origCount); // Undo restores everything @@ -985,37 +985,6 @@ private slots: QVERIFY(!m_editor->isEditing()); } - // ── Editor: padding value edit blocked, name/type still work ── - - void testPaddingEditRestrictions() { - m_ctrl->refresh(); - QApplication::processEvents(); - - ComposeResult result = m_doc->compose(); - m_editor->applyDocument(result); - QApplication::processEvents(); - - // Find padding line - int padLine = -1; - for (int i = 0; i < result.meta.size(); i++) { - if (result.meta[i].nodeKind == NodeKind::Padding && - result.meta[i].lineKind == LineKind::Field) { - padLine = i; - break; - } - } - QVERIFY(padLine >= 0); - - // Value edit rejected - QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, padLine)); - - // Type edit accepted - bool ok = m_editor->beginInlineEdit(EditTarget::Type, padLine); - QVERIFY(ok); - m_editor->cancelInlineEdit(); - QApplication::processEvents(); - } - // ── Editor: struct header rejects value edit ── void testStructHeaderRejectsValueEdit() {