diff --git a/src/compose.cpp b/src/compose.cpp index c07607c..1dceb46 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -336,6 +336,25 @@ void composeParent(ComposeState& state, const NodeTree& tree, } } + // Embedded struct with refId but no child nodes: expand referenced struct's + // children at this node's offset (single instance, like array with count=1) + if (node.kind == NodeKind::Struct && children.isEmpty() && node.refId != 0) { + int refIdx = tree.indexOfId(node.refId); + if (refIdx >= 0) { + QVector refChildren = state.childMap.value(node.refId); + std::sort(refChildren.begin(), refChildren.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + for (int childIdx : refChildren) { + // Skip self-referential children (e.g. struct Ball has a field of type Ball) + if (state.visiting.contains(tree.nodes[childIdx].id)) + continue; + composeNode(state, tree, prov, childIdx, childDepth, + absAddr, node.refId, false, node.id); + } + } + } + // For arrays, render children as condensed (no header/footer for struct elements) bool childrenAreArrayElements = (node.kind == NodeKind::Array); int elementIdx = 0; diff --git a/src/controller.cpp b/src/controller.cpp index a9c0e85..4a1c63c 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1703,6 +1703,63 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx, if (entry.entryKind == TypeEntry::Primitive) { if (entry.primitiveKind != node.kind) changeNodeKind(nodeIdx, entry.primitiveKind); + } else if (entry.entryKind == TypeEntry::Composite) { + bool wasSuppressed = m_suppressRefresh; + m_suppressRefresh = true; + m_doc->undoStack.beginMacro(QStringLiteral("Change to composite type")); + + if (spec.isPointer) { + // Pointer modifier: e.g. "Material*" → Pointer64 + refId + if (node.kind != NodeKind::Pointer64) + changeNodeKind(nodeIdx, NodeKind::Pointer64); + int idx = m_doc->tree.indexOfId(node.id); + if (idx >= 0 && m_doc->tree.nodes[idx].refId != entry.structId) + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangePointerRef{node.id, m_doc->tree.nodes[idx].refId, entry.structId})); + + } else if (spec.arrayCount > 0) { + // Array modifier: e.g. "Material[10]" → Array + Struct element + if (node.kind != NodeKind::Array) + changeNodeKind(nodeIdx, NodeKind::Array); + int idx = m_doc->tree.indexOfId(node.id); + if (idx >= 0) { + auto& n = m_doc->tree.nodes[idx]; + if (n.elementKind != NodeKind::Struct || n.arrayLen != spec.arrayCount) + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangeArrayMeta{node.id, n.elementKind, NodeKind::Struct, + n.arrayLen, spec.arrayCount})); + if (n.refId != entry.structId) + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangePointerRef{node.id, n.refId, entry.structId})); + } + + } else { + // Plain struct: e.g. "Material" → Struct + structTypeName + refId + collapsed + if (node.kind != NodeKind::Struct) + changeNodeKind(nodeIdx, NodeKind::Struct); + int idx = m_doc->tree.indexOfId(node.id); + if (idx >= 0) { + int refIdx = m_doc->tree.indexOfId(entry.structId); + QString targetName; + if (refIdx >= 0) { + const Node& ref = m_doc->tree.nodes[refIdx]; + targetName = ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName; + } + QString oldTypeName = m_doc->tree.nodes[idx].structTypeName; + if (oldTypeName != targetName) + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangeStructTypeName{node.id, oldTypeName, targetName})); + // Set refId so compose can expand the referenced struct's children + if (m_doc->tree.nodes[idx].refId != entry.structId) + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangePointerRef{node.id, m_doc->tree.nodes[idx].refId, entry.structId})); + // ChangePointerRef auto-sets collapsed=true when refId != 0 + } + } + + m_doc->undoStack.endMacro(); + m_suppressRefresh = wasSuppressed; + if (!m_suppressRefresh) refresh(); } } else if (mode == TypePopupMode::ArrayElement) { if (entry.entryKind == TypeEntry::Primitive) { diff --git a/src/core.h b/src/core.h index 294eca6..26e61a7 100644 --- a/src/core.h +++ b/src/core.h @@ -366,6 +366,10 @@ struct NodeTree { if (end > maxEnd) maxEnd = end; } + // Embedded struct reference: no own children but refId points to a struct definition + if (kids.isEmpty() && node.kind == NodeKind::Struct && node.refId != 0) + maxEnd = qMax(maxEnd, structSpan(node.refId, childMap, visited)); + return qMax(declaredSize, maxEnd); } diff --git a/tests/test_type_selector.cpp b/tests/test_type_selector.cpp index 338e278..954b89a 100644 --- a/tests/test_type_selector.cpp +++ b/tests/test_type_selector.cpp @@ -405,6 +405,186 @@ private slots: QCOMPARE(spec.arrayCount, 0); } + // ── FieldType popup: selecting a composite (struct) type changes node kind + structTypeName + collapsed ── + + void testFieldTypeCompositeChangesNodeToStruct() { + 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 struct, and Bravo struct id + int xIdx = -1; + uint64_t bravoId = 0; + for (int i = 0; i < doc->tree.nodes.size(); i++) { + const auto& n = doc->tree.nodes[i]; + if (n.name == "x" && n.kind == NodeKind::Int32) xIdx = i; + if (n.name == "Bravo" && n.kind == NodeKind::Struct) bravoId = n.id; + } + QVERIFY(xIdx >= 0); + QVERIFY(bravoId != 0); + + QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32); + QVERIFY(!doc->tree.nodes[xIdx].collapsed); + uint64_t xNodeId = doc->tree.nodes[xIdx].id; + + // Simulate the plain-struct path of applyTypePopupResult: + // beginMacro → changeNodeKind(Struct) → ChangeStructTypeName → ChangePointerRef → endMacro + doc->undoStack.beginMacro(QStringLiteral("Change to composite type")); + ctrl->changeNodeKind(xIdx, NodeKind::Struct); + + xIdx = doc->tree.indexOfId(xNodeId); + QVERIFY(xIdx >= 0); + + int bravoIdx = doc->tree.indexOfId(bravoId); + QVERIFY(bravoIdx >= 0); + QString targetName = doc->tree.nodes[bravoIdx].structTypeName; + + doc->undoStack.push(new RcxCommand(ctrl, + cmd::ChangeStructTypeName{xNodeId, doc->tree.nodes[xIdx].structTypeName, targetName})); + + // Set refId so compose can expand referenced struct children (auto-collapses) + doc->undoStack.push(new RcxCommand(ctrl, + cmd::ChangePointerRef{xNodeId, 0, bravoId})); + + doc->undoStack.endMacro(); + QApplication::processEvents(); + + // Verify: Struct with correct name, refId, AND collapsed + xIdx = doc->tree.indexOfId(xNodeId); + QVERIFY(xIdx >= 0); + QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Struct); + QCOMPARE(doc->tree.nodes[xIdx].structTypeName, QString("Bravo")); + QCOMPARE(doc->tree.nodes[xIdx].refId, bravoId); + QVERIFY(doc->tree.nodes[xIdx].collapsed); + + // Single undo reverses the entire macro + doc->undoStack.undo(); + QApplication::processEvents(); + xIdx = doc->tree.indexOfId(xNodeId); + QVERIFY(xIdx >= 0); + QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32); + QCOMPARE(doc->tree.nodes[xIdx].refId, uint64_t(0)); + QVERIFY(doc->tree.nodes[xIdx].structTypeName.isEmpty()); + + delete ctrl; + delete splitter; + delete doc; + } + + // ── FieldType popup: selecting a composite with * modifier creates Pointer64 + refId ── + + void testFieldTypeCompositeWithPointerModifier() { + 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) and Bravo struct + int xIdx = -1; + uint64_t bravoId = 0; + for (int i = 0; i < doc->tree.nodes.size(); i++) { + const auto& n = doc->tree.nodes[i]; + if (n.name == "x" && n.kind == NodeKind::Int32) xIdx = i; + if (n.name == "Bravo" && n.kind == NodeKind::Struct) bravoId = n.id; + } + QVERIFY(xIdx >= 0); + QVERIFY(bravoId != 0); + + uint64_t xNodeId = doc->tree.nodes[xIdx].id; + + // Simulate the pointer path of applyTypePopupResult: + // beginMacro → changeNodeKind(Pointer64) → ChangePointerRef → endMacro + doc->undoStack.beginMacro(QStringLiteral("Change to composite type")); + ctrl->changeNodeKind(xIdx, NodeKind::Pointer64); + + xIdx = doc->tree.indexOfId(xNodeId); + QVERIFY(xIdx >= 0); + QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Pointer64); + + doc->undoStack.push(new RcxCommand(ctrl, + cmd::ChangePointerRef{xNodeId, 0, bravoId})); + doc->undoStack.endMacro(); + QApplication::processEvents(); + + // Verify: Pointer64 with refId pointing to Bravo, auto-collapsed + xIdx = doc->tree.indexOfId(xNodeId); + QVERIFY(xIdx >= 0); + QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Pointer64); + QCOMPARE(doc->tree.nodes[xIdx].refId, bravoId); + QVERIFY(doc->tree.nodes[xIdx].collapsed); + + // Single undo reverses the entire macro + doc->undoStack.undo(); + QApplication::processEvents(); + xIdx = doc->tree.indexOfId(xNodeId); + QVERIFY(xIdx >= 0); + QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32); + QCOMPARE(doc->tree.nodes[xIdx].refId, uint64_t(0)); + + delete ctrl; + delete splitter; + delete doc; + } + + // ── FieldType popup: selecting a primitive type still works ── + + void testFieldTypePrimitiveStillWorks() { + 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); + QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32); + + // Change to Float via changeNodeKind (same path as primitive TypeEntry) + ctrl->changeNodeKind(xIdx, NodeKind::Float); + QApplication::processEvents(); + QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Float); + + // Undo + doc->undoStack.undo(); + QApplication::processEvents(); + QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32); + + delete ctrl; + delete splitter; + delete doc; + } + // ── Section headers in filtered list ── void testSectionHeadersPresent() {