IChooseYou

This commit is contained in:
sysadmin
2026-02-15 09:23:17 -07:00
parent 4192a4dad3
commit 1473a58742
16 changed files with 191 additions and 416 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -14,8 +14,9 @@ constexpr uint64_t kGoldenRatio = 0x9E3779B97F4A7C15ULL;
struct ComposeState { struct ComposeState {
QString text; QString text;
QVector<LineMeta> meta; QVector<LineMeta> meta;
QSet<uint64_t> visiting; // cycle detection for struct recursion QSet<uint64_t> visiting; // cycle detection for struct recursion
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
QSet<uint64_t> virtualPtrRefs; // refIds currently being virtually expanded via pointer deref
int currentLine = 0; int currentLine = 0;
int typeW = kColType; // global type column width (fallback) int typeW = kColType; // global type column width (fallback)
int nameW = kColName; // global name 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*/) { uint64_t /*addr*/, bool isCont, int /*depth*/) {
uint32_t mask = 0; uint32_t mask = 0;
if (isCont) mask |= (1u << M_CONT); 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. // No ambient validation markers — errors only shown during inline editing.
return mask; return mask;
} }
@@ -118,14 +118,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
int typeW = state.effectiveTypeW(scopeId); int typeW = state.effectiveTypeW(scopeId);
int nameW = state.effectiveNameW(scopeId); int nameW = state.effectiveNameW(scopeId);
// Line count: padding wraps at 8 bytes per line int numLines = linesForKind(node.kind);
int numLines;
if (node.kind == NodeKind::Padding) {
int totalBytes = qMax(1, node.arrayLen);
numLines = (totalBytes + 7) / 8;
} else {
numLines = linesForKind(node.kind);
}
// Resolve pointer target name for display // Resolve pointer target name for display
QString ptrTypeOverride; 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) // Set byte count for hex preview lines (used for per-byte change highlighting)
if (isHexPreview(node.kind)) { if (isHexPreview(node.kind)) {
if (node.kind == NodeKind::Padding) { lm.lineByteCount = sizeForKind(node.kind);
int totalSz = qMax(1, node.arrayLen);
lm.lineByteCount = qMin(8, totalSz - sub * 8);
} else {
lm.lineByteCount = sizeForKind(node.kind);
}
} }
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub, 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 ptrTargetName = resolvePointerTarget(tree, node.refId);
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName); QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
// Check if this pointer has materialized children (from materializeRefChildren)
QVector<int> 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) // Emit merged fold header: "Type* Name {" (expanded) or "Type* Name -> val" (collapsed)
{ {
LineMeta lm; LineMeta lm;
lm.nodeIdx = nodeIdx; lm.nodeIdx = nodeIdx;
lm.nodeId = node.id; lm.nodeId = node.id;
lm.depth = depth; 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.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr; lm.offsetAddr = tree.baseAddress + absAddr;
lm.nodeKind = node.kind; lm.nodeKind = node.kind;
lm.foldHead = true; lm.foldHead = true;
lm.foldCollapsed = node.collapsed; lm.foldCollapsed = effectiveCollapsed;
lm.foldLevel = computeFoldLevel(depth, true); lm.foldLevel = computeFoldLevel(depth, true);
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth); lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
lm.effectiveTypeW = typeW; lm.effectiveTypeW = typeW;
lm.effectiveNameW = nameW; lm.effectiveNameW = nameW;
lm.pointerTargetName = ptrTargetName; lm.pointerTargetName = ptrTargetName;
state.emitLine(fmt::fmtPointerHeader(node, depth, node.collapsed, state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
prov, absAddr, ptrTypeOverride, prov, absAddr, ptrTypeOverride,
typeW, nameW), lm); typeW, nameW), lm);
} }
if (!node.collapsed) { if (!effectiveCollapsed) {
int sz = node.byteSize(); int sz = node.byteSize();
uint64_t ptrVal = 0; uint64_t ptrVal = 0;
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) { if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
@@ -480,18 +481,42 @@ void composeNode(ComposeState& state, const NodeTree& tree,
if (!ptrReadable) if (!ptrReadable)
pBase = (uint64_t)0 - tree.baseAddress; pBase = (uint64_t)0 - tree.baseAddress;
qulonglong key = pBase ^ (node.refId * kGoldenRatio); if (hasMaterialized) {
if (!state.ptrVisiting.contains(key)) { // Render materialized children at the pointer target address.
state.ptrVisiting.insert(key); // These are real tree nodes with independent state — use rootId
int refIdx = tree.indexOfId(node.refId); // so resolveAddr computes offsets relative to the pointer target.
if (refIdx >= 0) { std::sort(ptrChildren.begin(), ptrChildren.end(), [&](int a, int b) {
const Node& ref = tree.nodes[refIdx]; return tree.nodes[a].offset < tree.nodes[b].offset;
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) });
composeParent(state, tree, childProv, refIdx, for (int childIdx : ptrChildren) {
depth, pBase, ref.id, composeNode(state, tree, childProv, childIdx, depth + 1,
/*isArrayChild=*/true); 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 // 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 // Include struct/array names - they now use columnar layout too
int maxNameLen = kMinNameW; int maxNameLen = kMinNameW;
for (const Node& node : tree.nodes) { 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; if (isHexPreview(node.kind)) continue;
maxNameLen = qMax(maxNameLen, (int)node.name.size()); 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]; const Node& child = tree.nodes[childIdx];
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size()); 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)) { if (!isHexPreview(child.kind)) {
scopeMaxName = qMax(scopeMaxName, (int)child.name.size()); 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]; const Node& child = tree.nodes[childIdx];
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size()); rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
// Name width (skip hex/padding, include containers) // Name width (skip hex, include containers)
if (!isHexPreview(child.kind)) { if (!isHexPreview(child.kind)) {
rootMaxName = qMax(rootMaxName, (int)child.name.size()); rootMaxName = qMax(rootMaxName, (int)child.name.size());
} }

View File

@@ -2078,7 +2078,7 @@ void RcxController::onRefreshTick() {
uint64_t rootId = m_viewRootId; uint64_t rootId = m_viewRootId;
if (rootId == 0 && !m_doc->tree.nodes.isEmpty()) if (rootId == 0 && !m_doc->tree.nodes.isEmpty())
rootId = m_doc->tree.nodes[0].id; rootId = m_doc->tree.nodes[0].id;
collectPointerRanges(rootId, 0, 0, 4, visited, ranges); collectPointerRanges(rootId, 0, 0, 99, visited, ranges);
} }
m_readInFlight = true; m_readInFlight = true;

View File

@@ -141,7 +141,7 @@ void RcxEditor::setupScintilla() {
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_EDITABLE, 5 /*INDIC_HIDDEN*/); 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, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/); IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/);
@@ -241,9 +241,6 @@ void RcxEditor::setupMarkers() {
// M_CONT (0): continuation line (metadata only, no visual) // M_CONT (0): continuation line (metadata only, no visual)
m_sci->markerDefine(QsciScintilla::Invisible, M_CONT); 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_PTR0 (2): right triangle
m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0); m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0);
@@ -1038,9 +1035,6 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
if (lm->nodeIdx < 0) return false; 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) // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind)) if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind))
return false; return false;
@@ -1221,9 +1215,6 @@ static bool hitTestTarget(QsciScintilla* sci,
} }
return false; 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) // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
if ((outTarget == EditTarget::Name || outTarget == EditTarget::Value) && isHexNode(lm.nodeKind)) if ((outTarget == EditTarget::Name || outTarget == EditTarget::Value) && isHexNode(lm.nodeKind))
return false; return false;
@@ -1681,9 +1672,6 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
(target == EditTarget::BaseAddress || target == EditTarget::Source (target == EditTarget::BaseAddress || target == EditTarget::Source
|| target == EditTarget::RootClassType || target == EditTarget::RootClassName))) || target == EditTarget::RootClassType || target == EditTarget::RootClassName)))
return false; 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) // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind)) if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind))
return false; return false;

View File

@@ -293,7 +293,6 @@ static QString readValueImpl(const Node& node, const Provider& prov,
line += QStringLiteral("]"); line += QStringLiteral("]");
return line; return line;
} }
case NodeKind::Padding: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
case NodeKind::UTF8: { case NodeKind::UTF8: {
QByteArray bytes = prov.readBytes(addr, node.strLen); QByteArray bytes = prov.readBytes(addr, node.strLen);
int end = bytes.indexOf('\0'); int end = bytes.indexOf('\0');
@@ -344,21 +343,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
return ind + QString(prefixW, ' ') + val + cmtSuffix; 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 (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); const int sz = sizeForKind(node.kind);
QByteArray b = prov.isReadable(addr, sz) QByteArray b = prov.isReadable(addr, sz)
? prov.readBytes(addr, sz) : QByteArray(sz, '\0'); ? prov.readBytes(addr, sz) : QByteArray(sz, '\0');

View File

@@ -50,7 +50,6 @@ static QString cTypeName(NodeKind kind) {
case NodeKind::Mat4x4: return QStringLiteral("float"); case NodeKind::Mat4x4: return QStringLiteral("float");
case NodeKind::UTF8: return QStringLiteral("char"); case NodeKind::UTF8: return QStringLiteral("char");
case NodeKind::UTF16: return QStringLiteral("wchar_t"); case NodeKind::UTF16: return QStringLiteral("wchar_t");
case NodeKind::Padding: return QStringLiteral("uint8_t");
default: 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; return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
case NodeKind::UTF16: case NodeKind::UTF16:
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc; 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: { case NodeKind::Pointer32: {
if (node.refId != 0) { if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId); int refIdx = tree.indexOfId(node.refId);
@@ -169,7 +166,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
auto emitPadRun = [&](int offset, int size) { auto emitPadRun = [&](int offset, int size) {
if (size <= 0) return; if (size <= 0) return;
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n") ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
.arg(ctx.cType(NodeKind::Padding)) .arg(QStringLiteral("uint8_t"))
.arg(ctx.uniquePadName()) .arg(ctx.uniquePadName())
.arg(QString::number(size, 16).toUpper()) .arg(QString::number(size, 16).toUpper())
.arg(offsetComment(offset)); .arg(offsetComment(offset));

View File

@@ -248,7 +248,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
"collapse: {op:'collapse', nodeId:'ID', collapsed:true}. " "collapse: {op:'collapse', nodeId:'ID', collapsed:true}. "
"Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. " "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 " "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{ {"inputSchema", QJsonObject{
{"type", "object"}, {"type", "object"},
{"properties", QJsonObject{ {"properties", QJsonObject{

View File

@@ -49,5 +49,7 @@
<file alias="symbol-ruler.svg">vsicons/symbol-ruler.svg</file> <file alias="symbol-ruler.svg">vsicons/symbol-ruler.svg</file>
<file alias="settings-gear.svg">vsicons/settings-gear.svg</file> <file alias="settings-gear.svg">vsicons/settings-gear.svg</file>
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file> <file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
<file alias="folder.svg">vsicons/folder.svg</file>
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@@ -122,7 +122,7 @@ public:
return; return;
} }
// 18px gutter: side triangle if current // Gutter: side triangle if current
if (m_hasCurrent && m_filtered && row >= 0 && row < m_filtered->size()) { if (m_hasCurrent && m_filtered && row >= 0 && row < m_filtered->size()) {
const TypeEntry& entry = (*m_filtered)[row]; const TypeEntry& entry = (*m_filtered)[row];
bool isCurrent = false; bool isCurrent = false;
@@ -131,13 +131,13 @@ public:
else if (m_current->entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite) else if (m_current->entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite)
isCurrent = (entry.structId == m_current->structId); isCurrent = (entry.structId == m_current->structId);
if (isCurrent) { if (isCurrent) {
painter->setPen(t.syntaxType); painter->setPen(t.text);
painter->setFont(m_font); 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))); QString(QChar(0x25B8)));
} }
} }
x += 18; x += 10;
// Icon 16x16 — only for composite entries // Icon 16x16 — only for composite entries
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size() 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->setFrameShape(QFrame::NoFrame);
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_listView->setMouseTracking(true); m_listView->setMouseTracking(true);
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_listView->viewport()->setAttribute(Qt::WA_Hover, true); m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
m_listView->installEventFilter(this); m_listView->installEventFilter(this);
@@ -491,7 +492,7 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) {
QString text = t.classKeyword.isEmpty() QString text = t.classKeyword.isEmpty()
? t.displayName ? t.displayName
: (t.classKeyword + QStringLiteral(" ") + 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; if (w > maxTextW) maxTextW = w;
} }
int popupW = qBound(280, maxTextW + 24, 500); int popupW = qBound(280, maxTextW + 24, 500);

View File

@@ -1,62 +1,76 @@
#pragma once #pragma once
#include "core.h" #include "core.h"
#include <QIcon>
#include <QStandardItemModel> #include <QStandardItemModel>
#include <QStandardItem> #include <QStandardItem>
#include <algorithm> #include <algorithm>
namespace rcx { namespace rcx {
// Recursively add children of parentId as tree items under parentItem. struct TabInfo {
inline void addWorkspaceChildren(QStandardItem* parentItem, const NodeTree* tree;
const NodeTree& tree, QString name;
uint64_t parentId, void* subPtr; // QMdiSubWindow* as void*
void* subPtr) { };
QVector<int> children = tree.childrenOf(parentId);
std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
for (int idx : children) { // Sentinel value stored in UserRole+1 to mark the Project group node.
const Node& node = tree.nodes[idx]; static constexpr uint64_t kGroupSentinel = ~uint64_t(0);
// Skip hex preview nodes — they are padding/filler, not meaningful fields inline void buildProjectExplorer(QStandardItemModel* model,
if (isHexNode(node.kind)) continue; const QVector<TabInfo>& tabs) {
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) {
model->clear(); model->clear();
model->setHorizontalHeaderLabels({QStringLiteral("Name")}); model->setHorizontalHeaderLabels({QStringLiteral("Name")});
auto* projectItem = new QStandardItem(projectName); // Single "Project" root with folder icon
projectItem->setData(QVariant::fromValue(subPtr), Qt::UserRole); 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<std::pair<const Node*, void*>> types, enums;
for (const auto& tab : tabs) {
QVector<int> 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<const Node*, void*>& a,
const std::pair<const Node*, void*>& 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); model->appendRow(projectItem);
} }

View File

@@ -89,7 +89,7 @@ private slots:
QCOMPARE(result.meta[2].lineKind, LineKind::Footer); QCOMPARE(result.meta[2].lineKind, LineKind::Footer);
} }
void testPaddingMarker() { void testHexNodeCompose() {
NodeTree tree; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -100,19 +100,18 @@ private slots:
int ri = tree.addNode(root); int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id; uint64_t rootId = tree.nodes[ri].id;
Node pad; Node hex;
pad.kind = NodeKind::Padding; hex.kind = NodeKind::Hex8;
pad.name = "pad"; hex.name = "pad";
pad.parentId = rootId; hex.parentId = rootId;
pad.offset = 0; hex.offset = 0;
tree.addNode(pad); tree.addNode(hex);
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + padding + root footer = 3 // CommandRow + hex node + root footer = 3
QCOMPARE(result.meta.size(), 3); QCOMPARE(result.meta.size(), 3);
QVERIFY(result.meta[1].markerMask & (1u << M_PAD));
QCOMPARE(result.meta[1].depth, 1); QCOMPARE(result.meta[1].depth, 1);
// Line 2 is root footer // Line 2 is root footer

View File

@@ -34,9 +34,8 @@ static void buildSmallTree(NodeTree& tree) {
field(0, NodeKind::UInt32, "field_u32"); // 4 bytes field(0, NodeKind::UInt32, "field_u32"); // 4 bytes
field(4, NodeKind::Float, "field_float"); // 4 bytes field(4, NodeKind::Float, "field_float"); // 4 bytes
field(8, NodeKind::UInt8, "field_u8"); // 1 byte field(8, NodeKind::UInt8, "field_u8"); // 1 byte
field(9, NodeKind::Padding, "pad0"); // 3 bytes padding field(9, NodeKind::Hex16, "pad0"); // 2 bytes
// Set padding arrayLen = 3 for 3-byte padding field(11, NodeKind::Hex8, "pad1"); // 1 byte
tree.nodes.last().arrayLen = 3;
field(12, NodeKind::Hex32, "field_hex"); // 4 bytes field(12, NodeKind::Hex32, "field_hex"); // 4 bytes
} }
@@ -282,47 +281,6 @@ private slots:
QVERIFY(newIdx >= 0); 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) ── // ── Test: setNodeValue with Hex32 (space-separated hex bytes) ──
void testSetNodeValueHex() { void testSetNodeValueHex() {
int idx = -1; int idx = -1;

View File

@@ -170,9 +170,10 @@ static NodeTree makeTestTree() {
n.parentId = rootId; n.offset = off; n.parentId = rootId; n.offset = off;
tree.addNode(n); tree.addNode(n);
}; };
auto pad = [&](int off, int len, const char* name) { auto pad = [&](int off, int /*len*/, const char* name) {
Node n; n.kind = NodeKind::Padding; n.name = name; // 4-byte padding → Hex32 (all usages in this test pass len=4)
n.parentId = rootId; n.offset = off; n.arrayLen = len; Node n; n.kind = NodeKind::Hex32; n.name = name;
n.parentId = rootId; n.offset = off;
tree.addNode(n); tree.addNode(n);
}; };
auto arr = [&](int off, NodeKind ek, int len, const char* name) { 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 = "Length"; n.offset = 0; tree.addNode(n);
n.kind = NodeKind::UInt16; n.name = "MaximumLength"; n.offset = 2; tree.addNode(n); n.kind = NodeKind::UInt16; n.name = "MaximumLength"; n.offset = 2; tree.addNode(n);
n.kind = NodeKind::Padding; n.name = "Pad"; n.kind = NodeKind::Hex32; n.name = "Pad";
n.offset = 4; n.arrayLen = 4; tree.addNode(n); n.offset = 4; n.arrayLen = 1; tree.addNode(n);
n.kind = NodeKind::Pointer64; n.name = "Buffer"; n.offset = 8; n.arrayLen = 1; n.kind = NodeKind::Pointer64; n.name = "Buffer"; n.offset = 8; n.arrayLen = 1;
tree.addNode(n); tree.addNode(n);
} }
@@ -751,70 +752,6 @@ private slots:
m_editor->applyDocument(m_result); 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 ── // ── Test: value edit commit fires signal with typed text ──
void testValueEditCommitUpdatesSignal() { void testValueEditCommitUpdatesSignal() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
@@ -823,8 +760,6 @@ private slots:
const LineMeta* lm = m_editor->metaForLine(kFirstDataLine); const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
QVERIFY(lm); QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::Field); QCOMPARE(lm->lineKind, LineKind::Field);
QVERIFY(lm->nodeKind != NodeKind::Padding);
// Begin value edit // Begin value edit
bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine); bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine);
QVERIFY(ok); QVERIFY(ok);

View File

@@ -418,30 +418,6 @@ private slots:
QVERIFY(result.contains("wchar_t wname[32];")); 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) ── // ── Full SDK export (multiple root structs) ──
void testFullSdkExport() { void testFullSdkExport() {

View File

@@ -304,39 +304,6 @@ private slots:
QVERIFY(result.contains("float speed;")); 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<NodeKind, QString> 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() { void testGenerator_typeAliases_array() {
// Array element type should use alias // Array element type should use alias
NodeTree tree; NodeTree tree;
@@ -547,134 +514,92 @@ private slots:
void testWorkspace_simpleTree() { void testWorkspace_simpleTree() {
auto tree = makeSimpleTree(); auto tree = makeSimpleTree();
QStandardItemModel model; QStandardItemModel model;
buildWorkspaceModel(&model, tree, "TestProject.rcx"); QVector<TabInfo> tabs = {{ &tree, "TestProject.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
// 1 top-level item (the project) // Single "Project" root
QCOMPARE(model.rowCount(), 1); QCOMPARE(model.rowCount(), 1);
QStandardItem* project = model.item(0); 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); QCOMPARE(project->rowCount(), 1);
QStandardItem* player = project->child(0); QVERIFY(project->child(0)->text().contains("Player"));
QVERIFY(player->text().contains("Player")); QVERIFY(project->child(0)->text().contains("struct"));
QVERIFY(player->text().contains("struct")); QCOMPARE(project->child(0)->rowCount(), 0);
// 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"));
} }
void testWorkspace_twoRootTree() { void testWorkspace_twoRootTree() {
auto tree = makeTwoRootTree(); auto tree = makeTwoRootTree();
QStandardItemModel model; QStandardItemModel model;
buildWorkspaceModel(&model, tree, "TwoRoot.rcx"); QVector<TabInfo> tabs = {{ &tree, "TwoRoot.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
QCOMPARE(model.rowCount(), 1); QCOMPARE(model.rowCount(), 1);
QStandardItem* project = model.item(0); 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); QCOMPARE(project->rowCount(), 2);
QVERIFY(project->child(0)->text().contains("Alpha")); QVERIFY(project->child(0)->text().contains("Alpha"));
QVERIFY(project->child(1)->text().contains("Bravo")); QVERIFY(project->child(1)->text().contains("Bravo"));
QCOMPARE(project->child(0)->rowCount(), 0);
// Each has 1 field child QCOMPARE(project->child(1)->rowCount(), 0);
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"));
} }
void testWorkspace_richTree_rootCount() { void testWorkspace_richTree_rootCount() {
auto tree = makeRichTree(); auto tree = makeRichTree();
QStandardItemModel model; QStandardItemModel model;
buildWorkspaceModel(&model, tree, "Rich.rcx"); QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
QStandardItem* project = model.item(0); 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(); auto tree = makeRichTree();
QStandardItemModel model; QStandardItemModel model;
buildWorkspaceModel(&model, tree, "Rich.rcx"); QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
QStandardItem* pet = model.item(0)->child(0); QStandardItem* project = model.item(0);
QVERIFY(pet->text().contains("Pet")); // Sorted alphabetically: Ball, Cat, Pet
// Pet has 2 non-hex children: name (UTF8), owner (Pointer64) QVERIFY(project->child(0)->text().contains("Ball"));
QCOMPARE(pet->rowCount(), 2); QVERIFY(project->child(1)->text().contains("Cat"));
QVERIFY(pet->child(0)->text().contains("name")); QVERIFY(project->child(2)->text().contains("Pet"));
QVERIFY(pet->child(1)->text().contains("owner")); // No member fields under type nodes
} QCOMPARE(project->child(0)->rowCount(), 0);
QCOMPARE(project->child(1)->rowCount(), 0);
void testWorkspace_richTree_catNesting() { QCOMPARE(project->child(2)->rowCount(), 0);
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"));
} }
void testWorkspace_emptyTree() { void testWorkspace_emptyTree() {
NodeTree tree; NodeTree tree;
QStandardItemModel model; QStandardItemModel model;
buildWorkspaceModel(&model, tree, "Empty.rcx"); QVector<TabInfo> tabs = {{ &tree, "Empty.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
// Still has the "Project" root, just no children
QCOMPARE(model.rowCount(), 1); QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.item(0)->text(), QString("Project"));
QCOMPARE(model.item(0)->rowCount(), 0); QCOMPARE(model.item(0)->rowCount(), 0);
} }
void testWorkspace_structIdRole() { void testWorkspace_structIdRole() {
auto tree = makeSimpleTree(); auto tree = makeSimpleTree();
QStandardItemModel model; QStandardItemModel model;
buildWorkspaceModel(&model, tree, "Test.rcx"); QVector<TabInfo> tabs = {{ &tree, "Test.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
QStandardItem* project = model.item(0); QStandardItem* project = model.item(0);
// Project item should NOT have structId // Project root has kGroupSentinel
QVERIFY(!project->data(Qt::UserRole + 1).isValid()); QCOMPARE(project->data(Qt::UserRole + 1).toULongLong(), kGroupSentinel);
// Player struct should have structId // Player type item should have structId
QStandardItem* player = project->child(0); QStandardItem* player = project->child(0);
QVERIFY(player->data(Qt::UserRole + 1).isValid()); QVERIFY(player->data(Qt::UserRole + 1).isValid());
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0); QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() != kGroupSentinel);
// health field should NOT have structId
QStandardItem* health = player->child(0);
QVERIFY(!health->data(Qt::UserRole + 1).isValid());
} }
// ═══════════════════════════════════════════════════ // ═══════════════════════════════════════════════════

View File

@@ -57,8 +57,8 @@ static void buildValidationTree(NodeTree& tree) {
field(46, NodeKind::Hex32, "field_h32"); field(46, NodeKind::Hex32, "field_h32");
field(50, NodeKind::Hex64, "field_h64"); field(50, NodeKind::Hex64, "field_h64");
field(58, NodeKind::Pointer64, "field_ptr"); field(58, NodeKind::Pointer64, "field_ptr");
field(66, NodeKind::Padding, "pad0"); field(66, NodeKind::Hex32, "pad0");
tree.nodes.last().arrayLen = 6; field(70, NodeKind::Hex16, "pad1");
fieldArr(72, NodeKind::UInt32, 4, "field_arr"); fieldArr(72, NodeKind::UInt32, 4, "field_arr");
} }
@@ -725,9 +725,9 @@ private slots:
QCOMPARE(m_doc->undoStack.count(), 0); 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"); int idx = findNode(m_doc->tree, "field_u32");
QVERIFY(idx >= 0); QVERIFY(idx >= 0);
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); // 4 bytes QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); // 4 bytes
@@ -737,7 +737,7 @@ private slots:
QApplication::processEvents(); QApplication::processEvents();
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt8); 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); QVERIFY(m_doc->tree.nodes.size() > origCount);
// Undo restores everything // Undo restores everything
@@ -985,37 +985,6 @@ private slots:
QVERIFY(!m_editor->isEditing()); 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 ── // ── Editor: struct header rejects value edit ──
void testStructHeaderRejectsValueEdit() { void testStructHeaderRejectsValueEdit() {