mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
IChooseYou
This commit is contained in:
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 63 KiB |
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user