mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Condensed array display + per-scope column widths + MIT license
- Array element structs render without { } braces (condensed display)
- [N] separators show element indices within arrays
- Per-scope column width calculation (nested elements use tighter spacing)
- Array headers show struct[N] for struct arrays
- [N] separators are not interactive (no hover/click highlight)
- Dynamic type column width (min 8, max 14)
- PE32+ sample data with full headers, DataDirectory[16], SectionHeaders[4]
- Added MIT license
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 IChooChoose
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
151
src/compose.cpp
151
src/compose.cpp
@@ -16,12 +16,24 @@ struct ComposeState {
|
|||||||
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
|
||||||
int currentLine = 0;
|
int currentLine = 0;
|
||||||
int nameW = kColName; // effective name column width
|
int typeW = kColType; // global type column width (fallback)
|
||||||
|
int nameW = kColName; // global name column width (fallback)
|
||||||
|
|
||||||
// Precomputed for O(1) lookups
|
// Precomputed for O(1) lookups
|
||||||
QHash<uint64_t, QVector<int>> childMap;
|
QHash<uint64_t, QVector<int>> childMap;
|
||||||
QVector<int64_t> absOffsets; // indexed by node index
|
QVector<int64_t> absOffsets; // indexed by node index
|
||||||
|
|
||||||
|
// Per-scope column widths (containerId -> width for direct children)
|
||||||
|
QHash<uint64_t, int> scopeTypeW;
|
||||||
|
QHash<uint64_t, int> scopeNameW;
|
||||||
|
|
||||||
|
int effectiveTypeW(uint64_t scopeId) const {
|
||||||
|
return scopeTypeW.value(scopeId, typeW);
|
||||||
|
}
|
||||||
|
int effectiveNameW(uint64_t scopeId) const {
|
||||||
|
return scopeNameW.value(scopeId, nameW);
|
||||||
|
}
|
||||||
|
|
||||||
void emitLine(const QString& lineText, LineMeta lm) {
|
void emitLine(const QString& lineText, LineMeta lm) {
|
||||||
if (currentLine > 0) text += '\n';
|
if (currentLine > 0) text += '\n';
|
||||||
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
|
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
|
||||||
@@ -93,9 +105,13 @@ static inline uint64_t resolveAddr(const ComposeState& state,
|
|||||||
|
|
||||||
void composeLeaf(ComposeState& state, const NodeTree& tree,
|
void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||||
const Provider& prov, int nodeIdx,
|
const Provider& prov, int nodeIdx,
|
||||||
int depth, uint64_t absAddr) {
|
int depth, uint64_t absAddr, uint64_t scopeId) {
|
||||||
const Node& node = tree.nodes[nodeIdx];
|
const Node& node = tree.nodes[nodeIdx];
|
||||||
|
|
||||||
|
// Get per-scope widths (falls back to global if no scope entry)
|
||||||
|
int typeW = state.effectiveTypeW(scopeId);
|
||||||
|
int nameW = state.effectiveNameW(scopeId);
|
||||||
|
|
||||||
// Line count: padding wraps at 8 bytes per line
|
// Line count: padding wraps at 8 bytes per line
|
||||||
int numLines;
|
int numLines;
|
||||||
if (node.kind == NodeKind::Padding) {
|
if (node.kind == NodeKind::Padding) {
|
||||||
@@ -119,9 +135,11 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont);
|
||||||
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
||||||
lm.foldLevel = computeFoldLevel(depth, false);
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
|
lm.effectiveTypeW = typeW;
|
||||||
|
lm.effectiveNameW = nameW;
|
||||||
|
|
||||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||||
/*comment=*/{}, state.nameW);
|
/*comment=*/{}, typeW, nameW);
|
||||||
state.emitLine(lineText, lm);
|
state.emitLine(lineText, lm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,14 +147,17 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
// Forward declarations (base/rootId default to 0 = use precomputed offsets)
|
// Forward declarations (base/rootId default to 0 = use precomputed offsets)
|
||||||
void composeNode(ComposeState& state, const NodeTree& tree,
|
void composeNode(ComposeState& state, const NodeTree& tree,
|
||||||
const Provider& prov, int nodeIdx, int depth,
|
const Provider& prov, int nodeIdx, int depth,
|
||||||
uint64_t base = 0, uint64_t rootId = 0);
|
uint64_t base = 0, uint64_t rootId = 0, bool isArrayChild = false,
|
||||||
|
uint64_t scopeId = 0, int arrayElementIdx = -1);
|
||||||
void composeParent(ComposeState& state, const NodeTree& tree,
|
void composeParent(ComposeState& state, const NodeTree& tree,
|
||||||
const Provider& prov, int nodeIdx, int depth,
|
const Provider& prov, int nodeIdx, int depth,
|
||||||
uint64_t base = 0, uint64_t rootId = 0);
|
uint64_t base = 0, uint64_t rootId = 0, bool isArrayChild = false,
|
||||||
|
uint64_t scopeId = 0, int arrayElementIdx = -1);
|
||||||
|
|
||||||
void composeParent(ComposeState& state, const NodeTree& tree,
|
void composeParent(ComposeState& state, const NodeTree& tree,
|
||||||
const Provider& prov, int nodeIdx, int depth,
|
const Provider& prov, int nodeIdx, int depth,
|
||||||
uint64_t base, uint64_t rootId) {
|
uint64_t base, uint64_t rootId, bool isArrayChild,
|
||||||
|
uint64_t scopeId, int arrayElementIdx) {
|
||||||
const Node& node = tree.nodes[nodeIdx];
|
const Node& node = tree.nodes[nodeIdx];
|
||||||
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId);
|
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId);
|
||||||
|
|
||||||
@@ -157,8 +178,23 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
}
|
}
|
||||||
state.visiting.insert(node.id);
|
state.visiting.insert(node.id);
|
||||||
|
|
||||||
// Header line
|
// Array element separator: show [N] to indicate which element this is
|
||||||
{
|
if (isArrayChild && arrayElementIdx >= 0) {
|
||||||
|
LineMeta lm;
|
||||||
|
lm.nodeIdx = nodeIdx;
|
||||||
|
lm.nodeId = node.id;
|
||||||
|
lm.depth = depth;
|
||||||
|
lm.lineKind = LineKind::ArrayElementSeparator;
|
||||||
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||||
|
lm.nodeKind = node.kind;
|
||||||
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
|
lm.markerMask = 0;
|
||||||
|
lm.arrayElementIdx = arrayElementIdx;
|
||||||
|
state.emitLine(fmt::indent(depth) + QStringLiteral("[%1]").arg(arrayElementIdx), lm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header line (skip for array element structs - condensed display)
|
||||||
|
if (!isArrayChild) {
|
||||||
LineMeta lm;
|
LineMeta lm;
|
||||||
lm.nodeIdx = nodeIdx;
|
lm.nodeIdx = nodeIdx;
|
||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
@@ -170,28 +206,46 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.foldCollapsed = node.collapsed;
|
lm.foldCollapsed = node.collapsed;
|
||||||
lm.foldLevel = computeFoldLevel(depth, true);
|
lm.foldLevel = computeFoldLevel(depth, true);
|
||||||
lm.markerMask = (1u << M_STRUCT_BG);
|
lm.markerMask = (1u << M_STRUCT_BG);
|
||||||
lm.isRootHeader = (node.parentId == 0); // Root-level struct
|
lm.isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct);
|
||||||
|
|
||||||
// Root structs show base address, nested structs show normal header
|
QString headerText;
|
||||||
QString headerText = lm.isRootHeader
|
if (node.kind == NodeKind::Array) {
|
||||||
? fmt::fmtStructHeaderWithBase(node, depth, tree.baseAddress)
|
// Array header with navigation: "uint32_t[16] name { <0/16>"
|
||||||
: fmt::fmtStructHeader(node, depth);
|
lm.isArrayHeader = true;
|
||||||
|
lm.elementKind = node.elementKind;
|
||||||
|
lm.arrayViewIdx = node.viewIndex;
|
||||||
|
lm.arrayCount = node.arrayLen;
|
||||||
|
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex);
|
||||||
|
} else if (lm.isRootHeader) {
|
||||||
|
// Root structs show base address
|
||||||
|
headerText = fmt::fmtStructHeaderWithBase(node, depth, tree.baseAddress);
|
||||||
|
} else {
|
||||||
|
// Nested structs show normal header
|
||||||
|
headerText = fmt::fmtStructHeader(node, depth);
|
||||||
|
}
|
||||||
state.emitLine(headerText, lm);
|
state.emitLine(headerText, lm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!node.collapsed) {
|
if (!node.collapsed || isArrayChild) {
|
||||||
QVector<int> children = state.childMap.value(node.id);
|
QVector<int> children = state.childMap.value(node.id);
|
||||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// For arrays, render children as condensed (no header/footer for struct elements)
|
||||||
|
bool childrenAreArrayElements = (node.kind == NodeKind::Array);
|
||||||
|
int elementIdx = 0;
|
||||||
for (int childIdx : children) {
|
for (int childIdx : children) {
|
||||||
composeNode(state, tree, prov, childIdx, depth + 1, base, rootId);
|
// Pass this container's id as the scope for children (for per-scope widths)
|
||||||
|
// For array elements, also pass the element index for [N] separator
|
||||||
|
composeNode(state, tree, prov, childIdx, depth + 1, base, rootId,
|
||||||
|
childrenAreArrayElements, node.id,
|
||||||
|
childrenAreArrayElements ? elementIdx++ : -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer line
|
// Footer line (skip for array element structs - condensed display)
|
||||||
{
|
if (!isArrayChild) {
|
||||||
LineMeta lm;
|
LineMeta lm;
|
||||||
lm.nodeIdx = nodeIdx;
|
lm.nodeIdx = nodeIdx;
|
||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
@@ -210,10 +264,15 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
|
|
||||||
void composeNode(ComposeState& state, const NodeTree& tree,
|
void composeNode(ComposeState& state, const NodeTree& tree,
|
||||||
const Provider& prov, int nodeIdx, int depth,
|
const Provider& prov, int nodeIdx, int depth,
|
||||||
uint64_t base, uint64_t rootId) {
|
uint64_t base, uint64_t rootId, bool isArrayChild,
|
||||||
|
uint64_t scopeId, int arrayElementIdx) {
|
||||||
const Node& node = tree.nodes[nodeIdx];
|
const Node& node = tree.nodes[nodeIdx];
|
||||||
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId);
|
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId);
|
||||||
|
|
||||||
|
// Get per-scope widths for this node
|
||||||
|
int typeW = state.effectiveTypeW(scopeId);
|
||||||
|
int nameW = state.effectiveNameW(scopeId);
|
||||||
|
|
||||||
// Pointer deref expansion
|
// Pointer deref expansion
|
||||||
if ((node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64)
|
if ((node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64)
|
||||||
&& node.refId != 0) {
|
&& node.refId != 0) {
|
||||||
@@ -229,7 +288,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.foldCollapsed = node.collapsed;
|
lm.foldCollapsed = node.collapsed;
|
||||||
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);
|
||||||
state.emitLine(fmt::fmtNodeLine(node, prov, absAddr, depth, 0), lm);
|
lm.effectiveTypeW = typeW;
|
||||||
|
lm.effectiveNameW = nameW;
|
||||||
|
state.emitLine(fmt::fmtNodeLine(node, prov, absAddr, depth, 0, {}, typeW, nameW), lm);
|
||||||
}
|
}
|
||||||
if (!node.collapsed) {
|
if (!node.collapsed) {
|
||||||
int sz = node.byteSize();
|
int sz = node.byteSize();
|
||||||
@@ -257,9 +318,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
|
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
|
||||||
composeParent(state, tree, prov, nodeIdx, depth, base, rootId);
|
composeParent(state, tree, prov, nodeIdx, depth, base, rootId, isArrayChild, scopeId, arrayElementIdx);
|
||||||
} else {
|
} else {
|
||||||
composeLeaf(state, tree, prov, nodeIdx, depth, absAddr);
|
composeLeaf(state, tree, prov, nodeIdx, depth, absAddr, scopeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +338,20 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
|||||||
for (int i = 0; i < tree.nodes.size(); i++)
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
state.absOffsets[i] = tree.computeOffset(i);
|
state.absOffsets[i] = tree.computeOffset(i);
|
||||||
|
|
||||||
|
// Compute effective type column width from longest type name
|
||||||
|
int maxTypeLen = kMinTypeW;
|
||||||
|
for (const Node& node : tree.nodes) {
|
||||||
|
QString typeName;
|
||||||
|
if (node.kind == NodeKind::Array) {
|
||||||
|
// Array type: "int32_t[10]", "char[64]", etc.
|
||||||
|
typeName = fmt::arrayTypeName(node.elementKind, node.arrayLen);
|
||||||
|
} else {
|
||||||
|
typeName = fmt::typeNameRaw(node.kind);
|
||||||
|
}
|
||||||
|
maxTypeLen = qMax(maxTypeLen, typeName.size());
|
||||||
|
}
|
||||||
|
state.typeW = qBound(kMinTypeW, maxTypeLen + 1, kMaxTypeW);
|
||||||
|
|
||||||
// Compute effective name column width from longest name
|
// Compute effective name column width from longest name
|
||||||
int maxNameLen = kMinNameW;
|
int maxNameLen = kMinNameW;
|
||||||
for (const Node& node : tree.nodes) {
|
for (const Node& node : tree.nodes) {
|
||||||
@@ -288,6 +363,38 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
|||||||
}
|
}
|
||||||
state.nameW = qBound(kMinNameW, maxNameLen + 1, kMaxNameW);
|
state.nameW = qBound(kMinNameW, maxNameLen + 1, kMaxNameW);
|
||||||
|
|
||||||
|
// Pre-compute per-scope widths (each container gets widths based on direct children only)
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
const Node& container = tree.nodes[i];
|
||||||
|
if (container.kind != NodeKind::Struct && container.kind != NodeKind::Array)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int scopeMaxType = kMinTypeW;
|
||||||
|
int scopeMaxName = kMinNameW;
|
||||||
|
|
||||||
|
for (int childIdx : state.childMap.value(container.id)) {
|
||||||
|
const Node& child = tree.nodes[childIdx];
|
||||||
|
|
||||||
|
// Type width
|
||||||
|
QString childTypeName;
|
||||||
|
if (child.kind == NodeKind::Array) {
|
||||||
|
childTypeName = fmt::arrayTypeName(child.elementKind, child.arrayLen);
|
||||||
|
} else {
|
||||||
|
childTypeName = fmt::typeNameRaw(child.kind);
|
||||||
|
}
|
||||||
|
scopeMaxType = qMax(scopeMaxType, childTypeName.size());
|
||||||
|
|
||||||
|
// Name width (skip hex/padding and containers)
|
||||||
|
if (!isHexPreview(child.kind) &&
|
||||||
|
child.kind != NodeKind::Struct && child.kind != NodeKind::Array) {
|
||||||
|
scopeMaxName = qMax(scopeMaxName, child.name.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType + 1, kMaxTypeW);
|
||||||
|
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName + 1, kMaxNameW);
|
||||||
|
}
|
||||||
|
|
||||||
QVector<int> roots = state.childMap.value(0);
|
QVector<int> roots = state.childMap.value(0);
|
||||||
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
|
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
|
||||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||||
@@ -297,7 +404,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
|||||||
composeNode(state, tree, prov, idx, 0);
|
composeNode(state, tree, prov, idx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { state.text, state.meta, LayoutInfo{state.nameW} };
|
return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW} };
|
||||||
}
|
}
|
||||||
|
|
||||||
QSet<uint64_t> NodeTree::normalizePreferAncestors(const QSet<uint64_t>& ids) const {
|
QSet<uint64_t> NodeTree::normalizePreferAncestors(const QSet<uint64_t>& ids) const {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
|
// Footer selection ID: set high bit to distinguish footer-only selections from node selections
|
||||||
|
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
|
||||||
|
|
||||||
// ── RcxDocument ──
|
// ── RcxDocument ──
|
||||||
|
|
||||||
RcxDocument::RcxDocument(QObject* parent)
|
RcxDocument::RcxDocument(QObject* parent)
|
||||||
@@ -137,9 +140,33 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EditTarget::Type: {
|
case EditTarget::Type: {
|
||||||
bool ok;
|
// Check for array type syntax: "type[count]" e.g. "int32_t[10]"
|
||||||
NodeKind k = kindFromTypeName(text, &ok);
|
int bracketPos = text.indexOf('[');
|
||||||
if (ok) changeNodeKind(nodeIdx, k);
|
if (bracketPos > 0 && text.endsWith(']')) {
|
||||||
|
QString elemTypeName = text.left(bracketPos).trimmed();
|
||||||
|
QString countStr = text.mid(bracketPos + 1, text.size() - bracketPos - 2);
|
||||||
|
bool countOk;
|
||||||
|
int newCount = countStr.toInt(&countOk);
|
||||||
|
if (countOk && newCount > 0) {
|
||||||
|
bool typeOk;
|
||||||
|
NodeKind elemKind = kindFromTypeName(elemTypeName, &typeOk);
|
||||||
|
if (typeOk && nodeIdx < m_doc->tree.nodes.size()) {
|
||||||
|
Node& node = m_doc->tree.nodes[nodeIdx];
|
||||||
|
if (node.kind == NodeKind::Array) {
|
||||||
|
// Update element kind and count (no undo for now)
|
||||||
|
node.elementKind = elemKind;
|
||||||
|
node.arrayLen = newCount;
|
||||||
|
if (node.viewIndex >= newCount)
|
||||||
|
node.viewIndex = qMax(0, newCount - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Regular type change
|
||||||
|
bool ok;
|
||||||
|
NodeKind k = kindFromTypeName(text, &ok);
|
||||||
|
if (ok) changeNodeKind(nodeIdx, k);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EditTarget::Value:
|
case EditTarget::Value:
|
||||||
@@ -197,6 +224,10 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case EditTarget::ArrayIndex:
|
||||||
|
case EditTarget::ArrayCount:
|
||||||
|
// Array navigation removed - these cases are unreachable
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// Always refresh to restore canonical text (handles parse failures, no-ops, etc.)
|
// Always refresh to restore canonical text (handles parse failures, no-ops, etc.)
|
||||||
refresh();
|
refresh();
|
||||||
@@ -640,20 +671,30 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
|||||||
bool ctrl = mods & Qt::ControlModifier;
|
bool ctrl = mods & Qt::ControlModifier;
|
||||||
bool shift = mods & Qt::ShiftModifier;
|
bool shift = mods & Qt::ShiftModifier;
|
||||||
|
|
||||||
|
// Compute effective selection ID: footers use nodeId | kFooterIdBit
|
||||||
|
auto effectiveId = [this](int ln, uint64_t nid) -> uint64_t {
|
||||||
|
if (ln >= 0 && ln < m_lastResult.meta.size() &&
|
||||||
|
m_lastResult.meta[ln].lineKind == LineKind::Footer)
|
||||||
|
return nid | kFooterIdBit;
|
||||||
|
return nid;
|
||||||
|
};
|
||||||
|
|
||||||
|
uint64_t selId = effectiveId(line, nodeId);
|
||||||
|
|
||||||
if (!ctrl && !shift) {
|
if (!ctrl && !shift) {
|
||||||
m_selIds.clear();
|
m_selIds.clear();
|
||||||
m_selIds.insert(nodeId);
|
m_selIds.insert(selId);
|
||||||
m_anchorLine = line;
|
m_anchorLine = line;
|
||||||
} else if (ctrl && !shift) {
|
} else if (ctrl && !shift) {
|
||||||
if (m_selIds.contains(nodeId))
|
if (m_selIds.contains(selId))
|
||||||
m_selIds.remove(nodeId);
|
m_selIds.remove(selId);
|
||||||
else
|
else
|
||||||
m_selIds.insert(nodeId);
|
m_selIds.insert(selId);
|
||||||
m_anchorLine = line;
|
m_anchorLine = line;
|
||||||
} else if (shift && !ctrl) {
|
} else if (shift && !ctrl) {
|
||||||
if (m_anchorLine < 0) {
|
if (m_anchorLine < 0) {
|
||||||
m_selIds.clear();
|
m_selIds.clear();
|
||||||
m_selIds.insert(nodeId);
|
m_selIds.insert(selId);
|
||||||
m_anchorLine = line;
|
m_anchorLine = line;
|
||||||
} else {
|
} else {
|
||||||
m_selIds.clear();
|
m_selIds.clear();
|
||||||
@@ -661,19 +702,19 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
|||||||
int to = qMax(m_anchorLine, line);
|
int to = qMax(m_anchorLine, line);
|
||||||
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
|
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
|
||||||
uint64_t nid = m_lastResult.meta[i].nodeId;
|
uint64_t nid = m_lastResult.meta[i].nodeId;
|
||||||
if (nid != 0) m_selIds.insert(nid);
|
if (nid != 0) m_selIds.insert(effectiveId(i, nid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // Ctrl+Shift
|
} else { // Ctrl+Shift
|
||||||
if (m_anchorLine < 0) {
|
if (m_anchorLine < 0) {
|
||||||
m_selIds.insert(nodeId);
|
m_selIds.insert(selId);
|
||||||
m_anchorLine = line;
|
m_anchorLine = line;
|
||||||
} else {
|
} else {
|
||||||
int from = qMin(m_anchorLine, line);
|
int from = qMin(m_anchorLine, line);
|
||||||
int to = qMax(m_anchorLine, line);
|
int to = qMax(m_anchorLine, line);
|
||||||
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
|
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
|
||||||
uint64_t nid = m_lastResult.meta[i].nodeId;
|
uint64_t nid = m_lastResult.meta[i].nodeId;
|
||||||
if (nid != 0) m_selIds.insert(nid);
|
if (nid != 0) m_selIds.insert(effectiveId(i, nid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -682,7 +723,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
|||||||
|
|
||||||
if (m_selIds.size() == 1) {
|
if (m_selIds.size() == 1) {
|
||||||
uint64_t sid = *m_selIds.begin();
|
uint64_t sid = *m_selIds.begin();
|
||||||
int idx = m_doc->tree.indexOfId(sid);
|
// Strip footer bit for node lookup
|
||||||
|
int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit);
|
||||||
if (idx >= 0) emit nodeSelected(idx);
|
if (idx >= 0) emit nodeSelected(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
src/core.h
102
src/core.h
@@ -235,16 +235,19 @@ struct Node {
|
|||||||
QString name;
|
QString name;
|
||||||
uint64_t parentId = 0; // 0 = root (no parent)
|
uint64_t parentId = 0; // 0 = root (no parent)
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
int arrayLen = 0;
|
int arrayLen = 1; // Array: element count
|
||||||
int strLen = 64;
|
int strLen = 64;
|
||||||
bool collapsed = false;
|
bool collapsed = false;
|
||||||
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
|
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
|
||||||
|
NodeKind elementKind = NodeKind::UInt8; // Array: element type
|
||||||
|
int viewIndex = 0; // Array: current view offset (transient)
|
||||||
|
|
||||||
int byteSize() const {
|
int byteSize() const {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case NodeKind::UTF8: return strLen;
|
case NodeKind::UTF8: return strLen;
|
||||||
case NodeKind::UTF16: return strLen * 2;
|
case NodeKind::UTF16: return strLen * 2;
|
||||||
case NodeKind::Padding: return qMax(1, arrayLen);
|
case NodeKind::Padding: return qMax(1, arrayLen);
|
||||||
|
case NodeKind::Array: return arrayLen * sizeForKind(elementKind);
|
||||||
default: return sizeForKind(kind);
|
default: return sizeForKind(kind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,6 +263,7 @@ struct Node {
|
|||||||
o["strLen"] = strLen;
|
o["strLen"] = strLen;
|
||||||
o["collapsed"] = collapsed;
|
o["collapsed"] = collapsed;
|
||||||
o["refId"] = QString::number(refId);
|
o["refId"] = QString::number(refId);
|
||||||
|
o["elementKind"] = kindToString(elementKind);
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
static Node fromJson(const QJsonObject& o) {
|
static Node fromJson(const QJsonObject& o) {
|
||||||
@@ -269,12 +273,19 @@ struct Node {
|
|||||||
n.name = o["name"].toString();
|
n.name = o["name"].toString();
|
||||||
n.parentId = o["parentId"].toString("0").toULongLong();
|
n.parentId = o["parentId"].toString("0").toULongLong();
|
||||||
n.offset = o["offset"].toInt(0);
|
n.offset = o["offset"].toInt(0);
|
||||||
n.arrayLen = o["arrayLen"].toInt(0);
|
n.arrayLen = o["arrayLen"].toInt(1);
|
||||||
n.strLen = o["strLen"].toInt(64);
|
n.strLen = o["strLen"].toInt(64);
|
||||||
n.collapsed = o["collapsed"].toBool(false);
|
n.collapsed = o["collapsed"].toBool(false);
|
||||||
n.refId = o["refId"].toString("0").toULongLong();
|
n.refId = o["refId"].toString("0").toULongLong();
|
||||||
|
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: is this a string-like array (char[] or wchar_t[])?
|
||||||
|
bool isStringArray() const {
|
||||||
|
return kind == NodeKind::Array &&
|
||||||
|
(elementKind == NodeKind::UInt8 || elementKind == NodeKind::UInt16);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── NodeTree ──
|
// ── NodeTree ──
|
||||||
@@ -415,7 +426,7 @@ struct NodeTree {
|
|||||||
// ── LineMeta ──
|
// ── LineMeta ──
|
||||||
|
|
||||||
enum class LineKind : uint8_t {
|
enum class LineKind : uint8_t {
|
||||||
Header, Field, Continuation, Footer
|
Header, Field, Continuation, Footer, ArrayElementSeparator
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LineMeta {
|
struct LineMeta {
|
||||||
@@ -428,15 +439,23 @@ struct LineMeta {
|
|||||||
bool foldCollapsed = false;
|
bool foldCollapsed = false;
|
||||||
bool isContinuation = false;
|
bool isContinuation = false;
|
||||||
bool isRootHeader = false; // true for top-level struct headers (base address editable)
|
bool isRootHeader = false; // true for top-level struct headers (base address editable)
|
||||||
|
bool isArrayHeader = false; // true for array headers (has <idx/count> nav)
|
||||||
LineKind lineKind = LineKind::Field;
|
LineKind lineKind = LineKind::Field;
|
||||||
NodeKind nodeKind = NodeKind::Int32;
|
NodeKind nodeKind = NodeKind::Int32;
|
||||||
|
NodeKind elementKind = NodeKind::UInt8; // Array element type
|
||||||
|
int arrayViewIdx = 0; // Array: current view index
|
||||||
|
int arrayCount = 0; // Array: total element count
|
||||||
|
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
|
||||||
QString offsetText;
|
QString offsetText;
|
||||||
uint32_t markerMask = 0;
|
uint32_t markerMask = 0;
|
||||||
|
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
||||||
|
int effectiveNameW = 22; // Per-line name column width used for rendering
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Layout Info ──
|
// ── Layout Info ──
|
||||||
|
|
||||||
struct LayoutInfo {
|
struct LayoutInfo {
|
||||||
|
int typeW = 14; // Effective type column width (default = kColType)
|
||||||
int nameW = 22; // Effective name column width (default = kColName)
|
int nameW = 22; // Effective name column width (default = kColName)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -476,30 +495,32 @@ struct ColumnSpan {
|
|||||||
bool valid = false;
|
bool valid = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class EditTarget { Name, Type, Value, BaseAddress };
|
enum class EditTarget { Name, Type, Value, BaseAddress, ArrayIndex, ArrayCount };
|
||||||
|
|
||||||
// Column layout constants (shared with format.cpp span computation)
|
// Column layout constants (shared with format.cpp span computation)
|
||||||
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
|
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
|
||||||
inline constexpr int kColType = 10;
|
inline constexpr int kColType = 14; // Max type column width (fits "uint64_t[999]")
|
||||||
inline constexpr int kColName = 22;
|
inline constexpr int kColName = 22;
|
||||||
inline constexpr int kColValue = 32;
|
inline constexpr int kColValue = 32;
|
||||||
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
||||||
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
|
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
|
||||||
inline constexpr int kSepWidth = 2;
|
inline constexpr int kSepWidth = 2;
|
||||||
|
inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t")
|
||||||
|
inline constexpr int kMaxTypeW = 14; // Maximum type column width (fits "uint64_t[999]")
|
||||||
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
|
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
|
||||||
inline constexpr int kMaxNameW = 22; // Maximum name column width (= kColName)
|
inline constexpr int kMaxNameW = 22; // Maximum name column width (= kColName)
|
||||||
|
|
||||||
inline ColumnSpan typeSpanFor(const LineMeta& lm) {
|
inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) {
|
||||||
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
|
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
return {ind, ind + kColType, true};
|
return {ind, ind + typeW, true};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline ColumnSpan nameSpanFor(const LineMeta& lm, int nameW = kColName) {
|
inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int nameW = kColName) {
|
||||||
if (lm.isContinuation || lm.lineKind != LineKind::Field) return {};
|
if (lm.isContinuation || lm.lineKind != LineKind::Field) return {};
|
||||||
|
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
int start = ind + kColType + kSepWidth;
|
int start = ind + typeW + kSepWidth;
|
||||||
|
|
||||||
// Hex/Padding: ASCII preview takes the name column position (8 chars)
|
// Hex/Padding: ASCII preview takes the name column position (8 chars)
|
||||||
if (isHexPreview(lm.nodeKind))
|
if (isHexPreview(lm.nodeKind))
|
||||||
@@ -508,8 +529,9 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int nameW = kColName) {
|
|||||||
return {start, start + nameW, true};
|
return {start, start + nameW, true};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int nameW = kColName) {
|
inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) {
|
||||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer ||
|
||||||
|
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
|
|
||||||
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
|
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
|
||||||
@@ -518,20 +540,20 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int nameW
|
|||||||
|
|
||||||
if (lm.isContinuation) {
|
if (lm.isContinuation) {
|
||||||
int prefixW = isHexPad
|
int prefixW = isHexPad
|
||||||
? (kColType + kSepWidth + 8 + kSepWidth)
|
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||||
: (kColType + nameW + 4);
|
: (typeW + nameW + 4);
|
||||||
int start = ind + prefixW;
|
int start = ind + prefixW;
|
||||||
return {start, start + valWidth, true};
|
return {start, start + valWidth, true};
|
||||||
}
|
}
|
||||||
if (lm.lineKind != LineKind::Field) return {};
|
if (lm.lineKind != LineKind::Field) return {};
|
||||||
|
|
||||||
int start = isHexPad
|
int start = isHexPad
|
||||||
? (ind + kColType + kSepWidth + 8 + kSepWidth)
|
? (ind + typeW + kSepWidth + 8 + kSepWidth)
|
||||||
: (ind + kColType + kSepWidth + nameW + kSepWidth);
|
: (ind + typeW + kSepWidth + nameW + kSepWidth);
|
||||||
return {start, start + valWidth, true};
|
return {start, start + valWidth, true};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int nameW = kColName) {
|
inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName) {
|
||||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
|
|
||||||
@@ -541,13 +563,13 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int nameW =
|
|||||||
int start;
|
int start;
|
||||||
if (lm.isContinuation) {
|
if (lm.isContinuation) {
|
||||||
int prefixW = isHexPad
|
int prefixW = isHexPad
|
||||||
? (kColType + kSepWidth + 8 + kSepWidth)
|
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||||
: (kColType + nameW + 4);
|
: (typeW + nameW + 4);
|
||||||
start = ind + prefixW + valWidth;
|
start = ind + prefixW + valWidth;
|
||||||
} else {
|
} else {
|
||||||
start = isHexPad
|
start = isHexPad
|
||||||
? (ind + kColType + kSepWidth + 8 + kSepWidth + valWidth)
|
? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth)
|
||||||
: (ind + kColType + kSepWidth + nameW + kSepWidth + valWidth);
|
: (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth);
|
||||||
}
|
}
|
||||||
return {start, lineLength, start < lineLength};
|
return {start, lineLength, start < lineLength};
|
||||||
}
|
}
|
||||||
@@ -579,6 +601,39 @@ inline ColumnSpan baseAddressFullSpanFor(const LineMeta& lm, const QString& line
|
|||||||
return {baseIdx, endPos, true};
|
return {baseIdx, endPos, true};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Array navigation spans ──
|
||||||
|
// Line format: "uint32_t[16] name { <0/16>"
|
||||||
|
|
||||||
|
inline ColumnSpan arrayPrevSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||||
|
if (!lm.isArrayHeader) return {};
|
||||||
|
int lt = lineText.lastIndexOf('<');
|
||||||
|
if (lt < 0) return {};
|
||||||
|
return {lt, lt + 1, true};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline ColumnSpan arrayIndexSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||||
|
if (!lm.isArrayHeader) return {};
|
||||||
|
int lt = lineText.lastIndexOf('<');
|
||||||
|
int slash = lineText.indexOf('/', lt);
|
||||||
|
if (lt < 0 || slash < 0) return {};
|
||||||
|
return {lt + 1, slash, true};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline ColumnSpan arrayCountSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||||
|
if (!lm.isArrayHeader) return {};
|
||||||
|
int slash = lineText.lastIndexOf('/');
|
||||||
|
int gt = lineText.indexOf('>', slash);
|
||||||
|
if (slash < 0 || gt < 0) return {};
|
||||||
|
return {slash + 1, gt, true};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline ColumnSpan arrayNextSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||||
|
if (!lm.isArrayHeader) return {};
|
||||||
|
int gt = lineText.lastIndexOf('>');
|
||||||
|
if (gt < 0) return {};
|
||||||
|
return {gt, gt + 1, true};
|
||||||
|
}
|
||||||
|
|
||||||
// ── ViewState ──
|
// ── ViewState ──
|
||||||
|
|
||||||
struct ViewState {
|
struct ViewState {
|
||||||
@@ -592,7 +647,8 @@ struct ViewState {
|
|||||||
namespace fmt {
|
namespace fmt {
|
||||||
using TypeNameFn = QString (*)(NodeKind);
|
using TypeNameFn = QString (*)(NodeKind);
|
||||||
void setTypeNameProvider(TypeNameFn fn);
|
void setTypeNameProvider(TypeNameFn fn);
|
||||||
QString typeName(NodeKind kind);
|
QString typeName(NodeKind kind, int colType = kColType);
|
||||||
|
QString typeNameRaw(NodeKind kind); // Unpadded type name for width calculation
|
||||||
QString fmtInt8(int8_t v);
|
QString fmtInt8(int8_t v);
|
||||||
QString fmtInt16(int16_t v);
|
QString fmtInt16(int16_t v);
|
||||||
QString fmtInt32(int32_t v);
|
QString fmtInt32(int32_t v);
|
||||||
@@ -608,11 +664,13 @@ namespace fmt {
|
|||||||
QString fmtPointer64(uint64_t v);
|
QString fmtPointer64(uint64_t v);
|
||||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||||
uint64_t addr, int depth, int subLine = 0,
|
uint64_t addr, int depth, int subLine = 0,
|
||||||
const QString& comment = {}, int colName = kColName);
|
const QString& comment = {}, int colType = kColType, int colName = kColName);
|
||||||
QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation);
|
QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation);
|
||||||
QString fmtStructHeader(const Node& node, int depth);
|
QString fmtStructHeader(const Node& node, int depth);
|
||||||
QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress);
|
QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress);
|
||||||
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
||||||
|
QString fmtArrayHeader(const Node& node, int depth, int viewIdx);
|
||||||
|
QString arrayTypeName(NodeKind elemKind, int count);
|
||||||
QString validateBaseAddress(const QString& text);
|
QString validateBaseAddress(const QString& text);
|
||||||
QString indent(int depth);
|
QString indent(int depth);
|
||||||
QString readValue(const Node& node, const Provider& prov,
|
QString readValue(const Node& node, const Provider& prov,
|
||||||
|
|||||||
150
src/editor.cpp
150
src/editor.cpp
@@ -26,6 +26,9 @@ static constexpr int IND_HEX_DIM = 9;
|
|||||||
static constexpr int IND_BASE_ADDR = 10; // Green color for base address
|
static constexpr int IND_BASE_ADDR = 10; // Green color for base address
|
||||||
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
||||||
|
|
||||||
|
// Footer selection ID: set high bit to distinguish footer-only selections from node selections
|
||||||
|
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
|
||||||
|
|
||||||
static QString g_fontName = "Consolas";
|
static QString g_fontName = "Consolas";
|
||||||
|
|
||||||
static QFont editorFont() {
|
static QFont editorFont() {
|
||||||
@@ -388,9 +391,15 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
|
||||||
|
|
||||||
for (int i = 0; i < m_meta.size(); i++) {
|
for (int i = 0; i < m_meta.size(); i++) {
|
||||||
if (selIds.contains(m_meta[i].nodeId)) {
|
uint64_t nodeId = m_meta[i].nodeId;
|
||||||
|
bool isFooter = (m_meta[i].lineKind == LineKind::Footer);
|
||||||
|
|
||||||
|
// Footers check for footerId, non-footers check for plain nodeId
|
||||||
|
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId;
|
||||||
|
if (selIds.contains(checkId)) {
|
||||||
m_sci->markerAdd(i, M_SELECTED);
|
m_sci->markerAdd(i, M_SELECTED);
|
||||||
paintEditableSpans(i);
|
if (!isFooter)
|
||||||
|
paintEditableSpans(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,10 +415,25 @@ void RcxEditor::applyHoverHighlight() {
|
|||||||
if (m_editState.active) return;
|
if (m_editState.active) return;
|
||||||
if (!m_hoverInside) return;
|
if (!m_hoverInside) return;
|
||||||
if (m_hoveredNodeId == 0) return;
|
if (m_hoveredNodeId == 0) return;
|
||||||
if (m_currentSelIds.contains(m_hoveredNodeId)) return;
|
|
||||||
for (int i = 0; i < m_meta.size(); i++) {
|
// Check if hovered line is a footer - footers highlight independently
|
||||||
if (m_meta[i].nodeId == m_hoveredNodeId)
|
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||||
m_sci->markerAdd(i, M_HOVER);
|
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
|
||||||
|
|
||||||
|
// Check if the hovered item is already selected (using appropriate ID)
|
||||||
|
uint64_t checkId = hoveringFooter ? (m_hoveredNodeId | kFooterIdBit) : m_hoveredNodeId;
|
||||||
|
if (m_currentSelIds.contains(checkId)) return;
|
||||||
|
|
||||||
|
if (hoveringFooter) {
|
||||||
|
// Footer: only highlight this specific line
|
||||||
|
m_sci->markerAdd(m_hoveredLine, M_HOVER);
|
||||||
|
} else {
|
||||||
|
// Non-footer: highlight all matching lines except footers
|
||||||
|
for (int i = 0; i < m_meta.size(); i++) {
|
||||||
|
if (m_meta[i].nodeId == m_hoveredNodeId &&
|
||||||
|
m_meta[i].lineKind != LineKind::Footer)
|
||||||
|
m_sci->markerAdd(i, M_HOVER);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,9 +473,9 @@ int RcxEditor::currentNodeIndex() const {
|
|||||||
|
|
||||||
// ── Column span computation ──
|
// ── Column span computation ──
|
||||||
|
|
||||||
ColumnSpan RcxEditor::typeSpan(const LineMeta& lm) { return typeSpanFor(lm); }
|
ColumnSpan RcxEditor::typeSpan(const LineMeta& lm, int typeW) { return typeSpanFor(lm, typeW); }
|
||||||
ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int nameW) { return nameSpanFor(lm, nameW); }
|
ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int typeW, int nameW) { return nameSpanFor(lm, typeW, nameW); }
|
||||||
ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int nameW) { return valueSpanFor(lm, lineLength, nameW); }
|
ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int typeW, int nameW) { return valueSpanFor(lm, lineLength, typeW, nameW); }
|
||||||
|
|
||||||
// ── Multi-selection ──
|
// ── Multi-selection ──
|
||||||
|
|
||||||
@@ -538,9 +562,24 @@ static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
|
|||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
int typeEnd = lineText.indexOf(' ', ind);
|
int typeEnd = lineText.indexOf(' ', ind);
|
||||||
if (typeEnd <= ind || typeEnd >= bracePos) return {};
|
if (typeEnd <= ind || typeEnd >= bracePos) return {};
|
||||||
|
|
||||||
|
// Don't allow editing array element names like "[0]", "[1]", etc.
|
||||||
|
QString name = lineText.mid(typeEnd + 1, bracePos - typeEnd - 1).trimmed();
|
||||||
|
if (name.startsWith('[') && name.endsWith(']'))
|
||||||
|
return {};
|
||||||
|
|
||||||
return {typeEnd + 1, bracePos, true};
|
return {typeEnd + 1, bracePos, true};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type span for array headers: "int32_t[10]" in "int32_t[10] positions {"
|
||||||
|
static ColumnSpan arrayHeaderTypeSpan(const LineMeta& lm, const QString& lineText) {
|
||||||
|
if (lm.lineKind != LineKind::Header || !lm.isArrayHeader) return {};
|
||||||
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
|
int typeEnd = lineText.indexOf(' ', ind);
|
||||||
|
if (typeEnd <= ind) return {};
|
||||||
|
return {ind, typeEnd, true};
|
||||||
|
}
|
||||||
|
|
||||||
RcxEditor::NormalizedSpan RcxEditor::normalizeSpan(
|
RcxEditor::NormalizedSpan RcxEditor::normalizeSpan(
|
||||||
const ColumnSpan& raw, const QString& lineText,
|
const ColumnSpan& raw, const QString& lineText,
|
||||||
EditTarget target, bool skipPrefixes) const
|
EditTarget target, bool skipPrefixes) const
|
||||||
@@ -589,14 +628,24 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
|||||||
QString lineText = getLineText(m_sci, line);
|
QString lineText = getLineText(m_sci, line);
|
||||||
int textLen = lineText.size();
|
int textLen = lineText.size();
|
||||||
|
|
||||||
|
// Use per-line effective widths (set during compose based on containing scope)
|
||||||
|
int typeW = lm->effectiveTypeW;
|
||||||
|
int nameW = lm->effectiveNameW;
|
||||||
|
|
||||||
ColumnSpan s;
|
ColumnSpan s;
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case EditTarget::Type: s = typeSpan(*lm); break;
|
case EditTarget::Type: s = typeSpan(*lm, typeW); break;
|
||||||
case EditTarget::Name: s = nameSpan(*lm, m_layout.nameW); break;
|
case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break;
|
||||||
case EditTarget::Value: s = valueSpan(*lm, textLen, m_layout.nameW); break;
|
case EditTarget::Value: s = valueSpan(*lm, textLen, typeW, nameW); break;
|
||||||
case EditTarget::BaseAddress: s = baseAddressSpanFor(*lm, lineText); break;
|
case EditTarget::BaseAddress: s = baseAddressSpanFor(*lm, lineText); break;
|
||||||
|
case EditTarget::ArrayIndex:
|
||||||
|
case EditTarget::ArrayCount:
|
||||||
|
break; // Array navigation removed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback spans for header lines
|
||||||
|
if (!s.valid && t == EditTarget::Type)
|
||||||
|
s = arrayHeaderTypeSpan(*lm, lineText);
|
||||||
if (!s.valid && t == EditTarget::Name)
|
if (!s.valid && t == EditTarget::Name)
|
||||||
s = headerNameSpan(*lm, lineText);
|
s = headerNameSpan(*lm, lineText);
|
||||||
|
|
||||||
@@ -640,8 +689,7 @@ RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const {
|
|||||||
static bool hitTestTarget(QsciScintilla* sci,
|
static bool hitTestTarget(QsciScintilla* sci,
|
||||||
const QVector<LineMeta>& meta,
|
const QVector<LineMeta>& meta,
|
||||||
const QPoint& viewportPos,
|
const QPoint& viewportPos,
|
||||||
int& outLine, EditTarget& outTarget,
|
int& outLine, EditTarget& outTarget)
|
||||||
int nameW = kColName)
|
|
||||||
{
|
{
|
||||||
long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
|
long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
|
||||||
(unsigned long)viewportPos.x(), (long)viewportPos.y());
|
(unsigned long)viewportPos.x(), (long)viewportPos.y());
|
||||||
@@ -656,18 +704,29 @@ static bool hitTestTarget(QsciScintilla* sci,
|
|||||||
int textLen = lineText.size();
|
int textLen = lineText.size();
|
||||||
|
|
||||||
const LineMeta& lm = meta[line];
|
const LineMeta& lm = meta[line];
|
||||||
ColumnSpan ts = RcxEditor::typeSpan(lm);
|
|
||||||
ColumnSpan ns = RcxEditor::nameSpan(lm, nameW);
|
|
||||||
ColumnSpan vs = RcxEditor::valueSpan(lm, textLen, nameW);
|
|
||||||
ColumnSpan bs = baseAddressSpanFor(lm, lineText); // Base address for root headers
|
|
||||||
|
|
||||||
if (!ns.valid)
|
// Array element separators are not interactive
|
||||||
ns = headerNameSpan(lm, lineText);
|
if (lm.lineKind == LineKind::ArrayElementSeparator) return false;
|
||||||
|
|
||||||
|
// Use per-line effective widths from LineMeta
|
||||||
|
int typeW = lm.effectiveTypeW;
|
||||||
|
int nameW = lm.effectiveNameW;
|
||||||
|
|
||||||
auto inSpan = [&](const ColumnSpan& s) {
|
auto inSpan = [&](const ColumnSpan& s) {
|
||||||
return s.valid && col >= s.start && col < s.end;
|
return s.valid && col >= s.start && col < s.end;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ColumnSpan ts = RcxEditor::typeSpan(lm, typeW);
|
||||||
|
ColumnSpan ns = RcxEditor::nameSpan(lm, typeW, nameW);
|
||||||
|
ColumnSpan vs = RcxEditor::valueSpan(lm, textLen, typeW, nameW);
|
||||||
|
ColumnSpan bs = baseAddressSpanFor(lm, lineText); // Base address for root headers
|
||||||
|
|
||||||
|
// Fallback spans for header lines
|
||||||
|
if (!ts.valid)
|
||||||
|
ts = arrayHeaderTypeSpan(lm, lineText);
|
||||||
|
if (!ns.valid)
|
||||||
|
ns = headerNameSpan(lm, lineText);
|
||||||
|
|
||||||
if (inSpan(bs)) outTarget = EditTarget::BaseAddress;
|
if (inSpan(bs)) outTarget = EditTarget::BaseAddress;
|
||||||
else if (inSpan(ts)) outTarget = EditTarget::Type;
|
else if (inSpan(ts)) outTarget = EditTarget::Type;
|
||||||
else if (inSpan(ns)) outTarget = EditTarget::Name;
|
else if (inSpan(ns)) outTarget = EditTarget::Name;
|
||||||
@@ -701,12 +760,17 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
const LineMeta* lm = metaForLine(m_editState.line);
|
const LineMeta* lm = metaForLine(m_editState.line);
|
||||||
if (lm) {
|
if (lm) {
|
||||||
QString lineText = getLineText(m_sci, h.line);
|
QString lineText = getLineText(m_sci, h.line);
|
||||||
|
// Use per-line effective widths
|
||||||
|
int typeW = lm->effectiveTypeW;
|
||||||
|
int nameW = lm->effectiveNameW;
|
||||||
ColumnSpan raw;
|
ColumnSpan raw;
|
||||||
switch (m_editState.target) {
|
switch (m_editState.target) {
|
||||||
case EditTarget::Type: raw = typeSpan(*lm); break;
|
case EditTarget::Type: raw = typeSpan(*lm, typeW); break;
|
||||||
case EditTarget::Name: raw = nameSpan(*lm, m_layout.nameW); break;
|
case EditTarget::Name: raw = nameSpan(*lm, typeW, nameW); break;
|
||||||
case EditTarget::Value: raw = valueSpan(*lm, lineText.size(), m_layout.nameW); break;
|
case EditTarget::Value: raw = valueSpan(*lm, lineText.size(), typeW, nameW); break;
|
||||||
case EditTarget::BaseAddress: raw = baseAddressSpanFor(*lm, lineText); break;
|
case EditTarget::BaseAddress: raw = baseAddressSpanFor(*lm, lineText); break;
|
||||||
|
case EditTarget::ArrayIndex: raw = arrayIndexSpanFor(*lm, lineText); break;
|
||||||
|
case EditTarget::ArrayCount: raw = arrayCountSpanFor(*lm, lineText); break;
|
||||||
}
|
}
|
||||||
if (raw.valid && h.col >= raw.start && h.col < raw.end) {
|
if (raw.valid && h.col >= raw.start && h.col < raw.end) {
|
||||||
// Within raw span but outside trimmed text → move cursor to end
|
// Within raw span but outside trimmed text → move cursor to end
|
||||||
@@ -732,8 +796,9 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
m_hoverInside = true;
|
m_hoverInside = true;
|
||||||
auto h = hitTest(me->pos());
|
auto h = hitTest(me->pos());
|
||||||
uint64_t newHoverId = (h.line >= 0) ? h.nodeId : 0;
|
uint64_t newHoverId = (h.line >= 0) ? h.nodeId : 0;
|
||||||
if (newHoverId != m_hoveredNodeId) {
|
if (newHoverId != m_hoveredNodeId || h.line != m_hoveredLine) {
|
||||||
m_hoveredNodeId = newHoverId;
|
m_hoveredNodeId = newHoverId;
|
||||||
|
m_hoveredLine = h.line;
|
||||||
applyHoverHighlight();
|
applyHoverHighlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,9 +811,9 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier));
|
bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier));
|
||||||
|
|
||||||
// Single-click on editable token of already-selected node → edit
|
// Single-click on editable token of already-selected node → edit
|
||||||
if (alreadySelected && plain) {
|
int tLine; EditTarget t;
|
||||||
int tLine; EditTarget t;
|
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) {
|
||||||
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t, m_layout.nameW)) {
|
if (alreadySelected && plain) {
|
||||||
m_pendingClickNodeId = 0;
|
m_pendingClickNodeId = 0;
|
||||||
return beginInlineEdit(t, tLine);
|
return beginInlineEdit(t, tLine);
|
||||||
}
|
}
|
||||||
@@ -824,7 +889,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
&& event->type() == QEvent::MouseButtonDblClick) {
|
&& event->type() == QEvent::MouseButtonDblClick) {
|
||||||
auto* me = static_cast<QMouseEvent*>(event);
|
auto* me = static_cast<QMouseEvent*>(event);
|
||||||
int line; EditTarget t;
|
int line; EditTarget t;
|
||||||
if (hitTestTarget(m_sci, m_meta, me->pos(), line, t, m_layout.nameW)) {
|
if (hitTestTarget(m_sci, m_meta, me->pos(), line, t)) {
|
||||||
m_pendingClickNodeId = 0; // cancel deferred selection change
|
m_pendingClickNodeId = 0; // cancel deferred selection change
|
||||||
return beginInlineEdit(t, line);
|
return beginInlineEdit(t, line);
|
||||||
}
|
}
|
||||||
@@ -856,6 +921,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
} else if (event->type() == QEvent::Leave) {
|
} else if (event->type() == QEvent::Leave) {
|
||||||
m_hoverInside = false;
|
m_hoverInside = false;
|
||||||
m_hoveredNodeId = 0;
|
m_hoveredNodeId = 0;
|
||||||
|
m_hoveredLine = -1;
|
||||||
applyHoverHighlight();
|
applyHoverHighlight();
|
||||||
} else if (event->type() == QEvent::Wheel) {
|
} else if (event->type() == QEvent::Wheel) {
|
||||||
m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos());
|
m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos());
|
||||||
@@ -866,8 +932,10 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
|| event->type() == QEvent::Wheel) {
|
|| event->type() == QEvent::Wheel) {
|
||||||
auto h = hitTest(m_lastHoverPos);
|
auto h = hitTest(m_lastHoverPos);
|
||||||
uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0;
|
uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0;
|
||||||
if (newHoverId != m_hoveredNodeId) {
|
int newHoverLine = (m_hoverInside && h.line >= 0) ? h.line : -1;
|
||||||
|
if (newHoverId != m_hoveredNodeId || newHoverLine != m_hoveredLine) {
|
||||||
m_hoveredNodeId = newHoverId;
|
m_hoveredNodeId = newHoverId;
|
||||||
|
m_hoveredLine = newHoverLine;
|
||||||
applyHoverHighlight();
|
applyHoverHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -948,6 +1016,7 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
|||||||
bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
||||||
if (m_editState.active) return false;
|
if (m_editState.active) return false;
|
||||||
m_hoveredNodeId = 0;
|
m_hoveredNodeId = 0;
|
||||||
|
m_hoveredLine = -1;
|
||||||
applyHoverHighlight();
|
applyHoverHighlight();
|
||||||
// Clear editable-token color hints (de-emphasize non-active tokens)
|
// Clear editable-token color hints (de-emphasize non-active tokens)
|
||||||
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
||||||
@@ -982,7 +1051,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
|||||||
|
|
||||||
// Store fixed comment column position for value editing
|
// Store fixed comment column position for value editing
|
||||||
if (target == EditTarget::Value) {
|
if (target == EditTarget::Value) {
|
||||||
ColumnSpan cs = commentSpanFor(*lm, lineText.size(), m_layout.nameW);
|
ColumnSpan cs = commentSpanFor(*lm, lineText.size(), lm->effectiveTypeW, lm->effectiveNameW);
|
||||||
m_editState.commentCol = cs.valid ? cs.start : -1;
|
m_editState.commentCol = cs.valid ? cs.start : -1;
|
||||||
m_editState.lastValidationOk = true; // original value is always valid
|
m_editState.lastValidationOk = true; // original value is always valid
|
||||||
} else {
|
} else {
|
||||||
@@ -1172,7 +1241,8 @@ void RcxEditor::updateTypeListFilter() {
|
|||||||
|
|
||||||
void RcxEditor::paintEditableSpans(int line) {
|
void RcxEditor::paintEditableSpans(int line) {
|
||||||
NormalizedSpan norm;
|
NormalizedSpan norm;
|
||||||
for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value}) {
|
for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value,
|
||||||
|
EditTarget::BaseAddress}) {
|
||||||
if (resolvedSpanFor(line, t, norm))
|
if (resolvedSpanFor(line, t, norm))
|
||||||
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
||||||
}
|
}
|
||||||
@@ -1191,13 +1261,21 @@ void RcxEditor::updateEditableIndicators(int line) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to check if a line's node is selected (handles footer IDs)
|
||||||
|
auto isLineSelected = [this](const LineMeta* lm) -> bool {
|
||||||
|
if (!lm) return false;
|
||||||
|
bool isFooter = (lm->lineKind == LineKind::Footer);
|
||||||
|
uint64_t checkId = isFooter ? (lm->nodeId | kFooterIdBit) : lm->nodeId;
|
||||||
|
return m_currentSelIds.contains(checkId);
|
||||||
|
};
|
||||||
|
|
||||||
// If new line is selected, its indicators are managed by applySelectionOverlay
|
// If new line is selected, its indicators are managed by applySelectionOverlay
|
||||||
// But we still need to clear the old non-selected hint line
|
// But we still need to clear the old non-selected hint line
|
||||||
const LineMeta* newLm = metaForLine(line);
|
const LineMeta* newLm = metaForLine(line);
|
||||||
if (newLm && m_currentSelIds.contains(newLm->nodeId)) {
|
if (isLineSelected(newLm)) {
|
||||||
if (m_hintLine >= 0) {
|
if (m_hintLine >= 0) {
|
||||||
const LineMeta* oldLm = metaForLine(m_hintLine);
|
const LineMeta* oldLm = metaForLine(m_hintLine);
|
||||||
if (!oldLm || !m_currentSelIds.contains(oldLm->nodeId))
|
if (!isLineSelected(oldLm))
|
||||||
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
||||||
}
|
}
|
||||||
m_hintLine = line;
|
m_hintLine = line;
|
||||||
@@ -1207,7 +1285,7 @@ void RcxEditor::updateEditableIndicators(int line) {
|
|||||||
// Clear old cursor line (only if not a selected node)
|
// Clear old cursor line (only if not a selected node)
|
||||||
if (m_hintLine >= 0) {
|
if (m_hintLine >= 0) {
|
||||||
const LineMeta* oldLm = metaForLine(m_hintLine);
|
const LineMeta* oldLm = metaForLine(m_hintLine);
|
||||||
if (!oldLm || !m_currentSelIds.contains(oldLm->nodeId))
|
if (!isLineSelected(oldLm))
|
||||||
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1251,9 +1329,9 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int line; EditTarget t;
|
int line; EditTarget t;
|
||||||
bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t, m_layout.nameW);
|
bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t);
|
||||||
|
|
||||||
// Apply hover span indicator (blue text like a link)
|
// Apply hover span indicator (blue text like a link) for editable spans
|
||||||
if (tokenHit) {
|
if (tokenHit) {
|
||||||
NormalizedSpan span;
|
NormalizedSpan span;
|
||||||
if (resolvedSpanFor(line, t, span)) {
|
if (resolvedSpanFor(line, t, span)) {
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ public:
|
|||||||
int currentNodeIndex() const;
|
int currentNodeIndex() const;
|
||||||
|
|
||||||
// ── Column span computation ──
|
// ── Column span computation ──
|
||||||
static ColumnSpan typeSpan(const LineMeta& lm);
|
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
|
||||||
static ColumnSpan nameSpan(const LineMeta& lm, int nameW = kColName);
|
static ColumnSpan nameSpan(const LineMeta& lm, int typeW = kColType, int nameW = kColName);
|
||||||
static ColumnSpan valueSpan(const LineMeta& lm, int lineLength, int nameW = kColName);
|
static ColumnSpan valueSpan(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName);
|
||||||
|
|
||||||
// ── Multi-selection ──
|
// ── Multi-selection ──
|
||||||
QSet<int> selectedNodeIndices() const;
|
QSet<int> selectedNodeIndices() const;
|
||||||
@@ -65,6 +65,7 @@ private:
|
|||||||
bool m_hoverInside = false;
|
bool m_hoverInside = false;
|
||||||
bool m_cursorOverridden = false;
|
bool m_cursorOverridden = false;
|
||||||
uint64_t m_hoveredNodeId = 0;
|
uint64_t m_hoveredNodeId = 0;
|
||||||
|
int m_hoveredLine = -1;
|
||||||
QSet<uint64_t> m_currentSelIds;
|
QSet<uint64_t> m_currentSelIds;
|
||||||
int m_hoverSpanLine = -1; // Line with hover span indicator
|
int m_hoverSpanLine = -1; // Line with hover span indicator
|
||||||
// ── Drag selection ──
|
// ── Drag selection ──
|
||||||
|
|||||||
@@ -28,10 +28,27 @@ static TypeNameFn g_typeNameFn = nullptr;
|
|||||||
|
|
||||||
void setTypeNameProvider(TypeNameFn fn) { g_typeNameFn = fn; }
|
void setTypeNameProvider(TypeNameFn fn) { g_typeNameFn = fn; }
|
||||||
|
|
||||||
QString typeName(NodeKind kind) {
|
// Unpadded type name for width calculation
|
||||||
if (g_typeNameFn) return fit(g_typeNameFn(kind), COL_TYPE);
|
QString typeNameRaw(NodeKind kind) {
|
||||||
|
if (g_typeNameFn) return g_typeNameFn(kind);
|
||||||
auto* m = kindMeta(kind);
|
auto* m = kindMeta(kind);
|
||||||
return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), COL_TYPE);
|
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString typeName(NodeKind kind, int colType) {
|
||||||
|
if (g_typeNameFn) return fit(g_typeNameFn(kind), colType);
|
||||||
|
auto* m = kindMeta(kind);
|
||||||
|
return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), colType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array type string: "uint32_t[16]" or "char[64]"
|
||||||
|
QString arrayTypeName(NodeKind elemKind, int count) {
|
||||||
|
auto* m = kindMeta(elemKind);
|
||||||
|
QString elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
||||||
|
// char[] for UInt8, wchar_t[] for UInt16
|
||||||
|
if (elemKind == NodeKind::UInt8) elem = QStringLiteral("char");
|
||||||
|
else if (elemKind == NodeKind::UInt16) elem = QStringLiteral("wchar_t");
|
||||||
|
return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Value formatting ──
|
// ── Value formatting ──
|
||||||
@@ -95,12 +112,15 @@ QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddres
|
|||||||
return header + QStringLiteral("base: ") + baseHex;
|
return header + QStringLiteral("base: ") + baseHex;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString fmtStructFooter(const Node& node, int depth, int totalSize) {
|
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
||||||
QString s = indent(depth) + QStringLiteral("};");
|
return indent(depth) + QStringLiteral("};");
|
||||||
if (totalSize > 0)
|
}
|
||||||
s += QStringLiteral(" // sizeof(") + node.name + QStringLiteral(")=0x")
|
|
||||||
+ QString::number(totalSize, 16).toUpper();
|
// ── Array header ──
|
||||||
return s;
|
// Format: "uint32_t[16] myArray {" (like struct header, no fixed columns)
|
||||||
|
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/) {
|
||||||
|
QString type = arrayTypeName(node.elementKind, node.arrayLen);
|
||||||
|
return indent(depth) + type + QStringLiteral(" ") + node.name + QStringLiteral(" {");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hex / ASCII preview ──
|
// ── Hex / ASCII preview ──
|
||||||
@@ -230,12 +250,12 @@ QString readValue(const Node& node, const Provider& prov,
|
|||||||
|
|
||||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||||
uint64_t addr, int depth, int subLine,
|
uint64_t addr, int depth, int subLine,
|
||||||
const QString& comment, int colName) {
|
const QString& comment, int colType, int colName) {
|
||||||
QString ind = indent(depth);
|
QString ind = indent(depth);
|
||||||
QString type = typeName(node.kind);
|
QString type = typeName(node.kind, colType);
|
||||||
QString name = fit(node.name, colName);
|
QString name = fit(node.name, colName);
|
||||||
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
||||||
const int prefixW = COL_TYPE + colName + 4; // 2 seps × 2 chars
|
const int prefixW = colType + colName + 4; // 2 seps × 2 chars
|
||||||
|
|
||||||
// Comment suffix (padded or empty)
|
// Comment suffix (padded or empty)
|
||||||
QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ')
|
QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ')
|
||||||
@@ -268,7 +288,7 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
|||||||
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
|
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
|
||||||
if (subLine == 0)
|
if (subLine == 0)
|
||||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||||
return ind + QString(COL_TYPE + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
||||||
}
|
}
|
||||||
// Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns
|
// Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns
|
||||||
const int sz = sizeForKind(node.kind);
|
const int sz = sizeForKind(node.kind);
|
||||||
|
|||||||
434
src/main.cpp
434
src/main.cpp
@@ -260,187 +260,281 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
void MainWindow::newFile() {
|
void MainWindow::newFile() {
|
||||||
auto* doc = new RcxDocument(this);
|
auto* doc = new RcxDocument(this);
|
||||||
|
|
||||||
// Autoload self as binary data
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
doc->loadData(QCoreApplication::applicationFilePath());
|
// PE Header Demo - Realistic PE32+ (64-bit) executable structure
|
||||||
doc->tree.baseAddress = 0;
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Layout:
|
||||||
|
// 0x000: DOS Header (64 bytes)
|
||||||
|
// 0x040: DOS Stub (64 bytes padding)
|
||||||
|
// 0x080: PE Signature (4 bytes)
|
||||||
|
// 0x084: File Header (20 bytes)
|
||||||
|
// 0x098: Optional Header PE32+ (240 bytes)
|
||||||
|
// - Standard fields (24 bytes)
|
||||||
|
// - Windows fields (88 bytes)
|
||||||
|
// - Data Directories (16 * 8 = 128 bytes)
|
||||||
|
// 0x188: Section Headers (4 * 40 = 160 bytes)
|
||||||
|
// 0x228: End of headers (total 552 bytes)
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
// Read e_lfanew to find PE header offset
|
QByteArray peData(0x300, '\0'); // 768 bytes
|
||||||
uint32_t lfanew = doc->provider->readU32(0x3C);
|
char* d = peData.data();
|
||||||
if (lfanew < 0x40 || lfanew >= (uint32_t)doc->provider->size())
|
|
||||||
lfanew = 0x40;
|
|
||||||
uint32_t pe = lfanew; // PE signature
|
|
||||||
uint32_t fh = pe + 4; // IMAGE_FILE_HEADER
|
|
||||||
uint32_t oh = fh + 20; // IMAGE_OPTIONAL_HEADER (PE32+)
|
|
||||||
|
|
||||||
Node root;
|
// ── DOS Header (IMAGE_DOS_HEADER) ──
|
||||||
root.kind = NodeKind::Struct;
|
d[0x00] = 'M'; d[0x01] = 'Z'; // e_magic
|
||||||
root.name = "PE_HEADER";
|
*(uint16_t*)(d + 0x02) = 0x0090; // e_cblp (bytes on last page)
|
||||||
root.parentId = 0;
|
*(uint16_t*)(d + 0x04) = 0x0003; // e_cp (pages in file)
|
||||||
root.offset = 0;
|
*(uint16_t*)(d + 0x06) = 0x0000; // e_crlc (relocations)
|
||||||
int ri = doc->tree.addNode(root);
|
*(uint16_t*)(d + 0x08) = 0x0004; // e_cparhdr (header size in paragraphs)
|
||||||
uint64_t rootId = doc->tree.nodes[ri].id;
|
*(uint16_t*)(d + 0x0A) = 0x0000; // e_minalloc
|
||||||
|
*(uint16_t*)(d + 0x0C) = 0xFFFF; // e_maxalloc
|
||||||
auto add = [&](NodeKind k, const QString& name, int off) {
|
*(uint16_t*)(d + 0x0E) = 0x0000; // e_ss
|
||||||
Node n;
|
*(uint16_t*)(d + 0x10) = 0x00B8; // e_sp
|
||||||
n.kind = k;
|
*(uint16_t*)(d + 0x12) = 0x0000; // e_csum
|
||||||
n.name = name;
|
*(uint16_t*)(d + 0x14) = 0x0000; // e_ip
|
||||||
n.offset = off;
|
*(uint16_t*)(d + 0x16) = 0x0000; // e_cs
|
||||||
n.parentId = rootId;
|
*(uint16_t*)(d + 0x18) = 0x0040; // e_lfarlc
|
||||||
doc->tree.addNode(n);
|
*(uint16_t*)(d + 0x1A) = 0x0000; // e_ovno
|
||||||
};
|
// e_res[4] at 0x1C-0x23 (zeroed)
|
||||||
|
*(uint16_t*)(d + 0x24) = 0x0000; // e_oemid
|
||||||
// ── IMAGE_DOS_HEADER (0x00 – 0x3F) ──
|
*(uint16_t*)(d + 0x26) = 0x0000; // e_oeminfo
|
||||||
add(NodeKind::UInt16, "e_magic", 0x00);
|
// e_res2[10] at 0x28-0x3B (zeroed)
|
||||||
add(NodeKind::UInt16, "e_cblp", 0x02);
|
*(uint32_t*)(d + 0x3C) = 0x00000080; // e_lfanew → PE header at 0x80
|
||||||
add(NodeKind::UInt16, "e_cp", 0x04);
|
|
||||||
add(NodeKind::UInt16, "e_crlc", 0x06);
|
|
||||||
add(NodeKind::UInt16, "e_cparhdr", 0x08);
|
|
||||||
add(NodeKind::UInt16, "e_minalloc", 0x0A);
|
|
||||||
add(NodeKind::UInt16, "e_maxalloc", 0x0C);
|
|
||||||
add(NodeKind::UInt16, "e_ss", 0x0E);
|
|
||||||
add(NodeKind::UInt16, "e_sp", 0x10);
|
|
||||||
add(NodeKind::UInt16, "e_csum", 0x12);
|
|
||||||
add(NodeKind::UInt16, "e_ip", 0x14);
|
|
||||||
add(NodeKind::UInt16, "e_cs", 0x16);
|
|
||||||
add(NodeKind::UInt16, "e_lfarlc", 0x18);
|
|
||||||
add(NodeKind::UInt16, "e_ovno", 0x1A);
|
|
||||||
add(NodeKind::Hex64, "e_res", 0x1C);
|
|
||||||
add(NodeKind::UInt16, "e_oemid", 0x24);
|
|
||||||
add(NodeKind::UInt16, "e_oeminfo", 0x26);
|
|
||||||
add(NodeKind::Hex64, "e_res2_0", 0x28);
|
|
||||||
add(NodeKind::Hex64, "e_res2_1", 0x30);
|
|
||||||
add(NodeKind::Hex32, "e_res2_2", 0x38);
|
|
||||||
add(NodeKind::UInt32, "e_lfanew", 0x3C);
|
|
||||||
|
|
||||||
// ── DOS Stub (0x40 to PE signature) — fill with Hex nodes ──
|
|
||||||
{
|
|
||||||
int cursor = 0x40;
|
|
||||||
while (cursor + 8 <= (int)pe) {
|
|
||||||
add(NodeKind::Hex64,
|
|
||||||
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
|
|
||||||
cursor);
|
|
||||||
cursor += 8;
|
|
||||||
}
|
|
||||||
if (cursor + 4 <= (int)pe) {
|
|
||||||
add(NodeKind::Hex32,
|
|
||||||
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
|
|
||||||
cursor);
|
|
||||||
cursor += 4;
|
|
||||||
}
|
|
||||||
if (cursor + 2 <= (int)pe) {
|
|
||||||
add(NodeKind::Hex16,
|
|
||||||
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
|
|
||||||
cursor);
|
|
||||||
cursor += 2;
|
|
||||||
}
|
|
||||||
if (cursor + 1 <= (int)pe) {
|
|
||||||
add(NodeKind::Hex8,
|
|
||||||
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
|
|
||||||
cursor);
|
|
||||||
cursor += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── PE Signature ──
|
// ── PE Signature ──
|
||||||
add(NodeKind::UInt32, "Signature", pe);
|
const int peOff = 0x80;
|
||||||
|
d[peOff+0] = 'P'; d[peOff+1] = 'E'; d[peOff+2] = 0; d[peOff+3] = 0;
|
||||||
|
|
||||||
// ── IMAGE_FILE_HEADER (nested struct) ──
|
// ── File Header (IMAGE_FILE_HEADER) ──
|
||||||
{
|
const int fhOff = peOff + 4; // 0x84
|
||||||
Node fhStruct;
|
*(uint16_t*)(d + fhOff + 0) = 0x8664; // Machine (AMD64)
|
||||||
fhStruct.kind = NodeKind::Struct;
|
*(uint16_t*)(d + fhOff + 2) = 0x0004; // NumberOfSections
|
||||||
fhStruct.name = "IMAGE_FILE_HEADER";
|
*(uint32_t*)(d + fhOff + 4) = 0x65A3B2C1; // TimeDateStamp
|
||||||
fhStruct.parentId = rootId;
|
*(uint32_t*)(d + fhOff + 8) = 0x00000000; // PointerToSymbolTable
|
||||||
fhStruct.offset = fh;
|
*(uint32_t*)(d + fhOff + 12) = 0x00000000; // NumberOfSymbols
|
||||||
int fi = doc->tree.addNode(fhStruct);
|
*(uint16_t*)(d + fhOff + 16) = 0x00F0; // SizeOfOptionalHeader (240)
|
||||||
uint64_t fhId = doc->tree.nodes[fi].id;
|
*(uint16_t*)(d + fhOff + 18) = 0x0022; // Characteristics (EXECUTABLE|LARGE_ADDRESS_AWARE)
|
||||||
|
|
||||||
auto addFH = [&](NodeKind k, const QString& name, int off) {
|
// ── Optional Header PE32+ (IMAGE_OPTIONAL_HEADER64) ──
|
||||||
Node n;
|
const int ohOff = fhOff + 20; // 0x98
|
||||||
n.kind = k;
|
*(uint16_t*)(d + ohOff + 0) = 0x020B; // Magic (PE32+)
|
||||||
n.name = name;
|
*(uint8_t*)(d + ohOff + 2) = 0x0E; // MajorLinkerVersion
|
||||||
n.offset = off;
|
*(uint8_t*)(d + ohOff + 3) = 0x00; // MinorLinkerVersion
|
||||||
n.parentId = fhId;
|
*(uint32_t*)(d + ohOff + 4) = 0x00012000; // SizeOfCode
|
||||||
doc->tree.addNode(n);
|
*(uint32_t*)(d + ohOff + 8) = 0x00008000; // SizeOfInitializedData
|
||||||
};
|
*(uint32_t*)(d + ohOff + 12) = 0x00000000; // SizeOfUninitializedData
|
||||||
|
*(uint32_t*)(d + ohOff + 16) = 0x00001000; // AddressOfEntryPoint
|
||||||
|
*(uint32_t*)(d + ohOff + 20) = 0x00001000; // BaseOfCode
|
||||||
|
|
||||||
addFH(NodeKind::UInt16, "Machine", 0x00);
|
// Windows-specific fields (PE32+)
|
||||||
addFH(NodeKind::UInt16, "NumberOfSections", 0x02);
|
*(uint64_t*)(d + ohOff + 24) = 0x0000000140000000ULL; // ImageBase
|
||||||
addFH(NodeKind::UInt32, "TimeDateStamp", 0x04);
|
*(uint32_t*)(d + ohOff + 32) = 0x00001000; // SectionAlignment
|
||||||
addFH(NodeKind::UInt32, "PtrToSymbolTable", 0x08);
|
*(uint32_t*)(d + ohOff + 36) = 0x00000200; // FileAlignment
|
||||||
addFH(NodeKind::UInt32, "NumberOfSymbols", 0x0C);
|
*(uint16_t*)(d + ohOff + 40) = 0x0006; // MajorOperatingSystemVersion
|
||||||
addFH(NodeKind::UInt16, "SizeOfOptionalHeader", 0x10);
|
*(uint16_t*)(d + ohOff + 42) = 0x0000; // MinorOperatingSystemVersion
|
||||||
addFH(NodeKind::UInt16, "Characteristics", 0x12);
|
*(uint16_t*)(d + ohOff + 44) = 0x0000; // MajorImageVersion
|
||||||
|
*(uint16_t*)(d + ohOff + 46) = 0x0000; // MinorImageVersion
|
||||||
|
*(uint16_t*)(d + ohOff + 48) = 0x0006; // MajorSubsystemVersion
|
||||||
|
*(uint16_t*)(d + ohOff + 50) = 0x0000; // MinorSubsystemVersion
|
||||||
|
*(uint32_t*)(d + ohOff + 52) = 0x00000000; // Win32VersionValue
|
||||||
|
*(uint32_t*)(d + ohOff + 56) = 0x00025000; // SizeOfImage
|
||||||
|
*(uint32_t*)(d + ohOff + 60) = 0x00000200; // SizeOfHeaders
|
||||||
|
*(uint32_t*)(d + ohOff + 64) = 0x00000000; // CheckSum
|
||||||
|
*(uint16_t*)(d + ohOff + 68) = 0x0003; // Subsystem (CONSOLE)
|
||||||
|
*(uint16_t*)(d + ohOff + 70) = 0x8160; // DllCharacteristics (DYNAMIC_BASE|NX_COMPAT|TERMINAL_SERVER_AWARE)
|
||||||
|
*(uint64_t*)(d + ohOff + 72) = 0x0000000000100000ULL; // SizeOfStackReserve
|
||||||
|
*(uint64_t*)(d + ohOff + 80) = 0x0000000000001000ULL; // SizeOfStackCommit
|
||||||
|
*(uint64_t*)(d + ohOff + 88) = 0x0000000000100000ULL; // SizeOfHeapReserve
|
||||||
|
*(uint64_t*)(d + ohOff + 96) = 0x0000000000001000ULL; // SizeOfHeapCommit
|
||||||
|
*(uint32_t*)(d + ohOff + 104) = 0x00000000; // LoaderFlags
|
||||||
|
*(uint32_t*)(d + ohOff + 108) = 0x00000010; // NumberOfRvaAndSizes (16)
|
||||||
|
|
||||||
|
// ── Data Directories (16 entries × 8 bytes) ──
|
||||||
|
const int ddOff = ohOff + 112; // 0x108
|
||||||
|
// Each entry: VirtualAddress (4) + Size (4)
|
||||||
|
struct { uint32_t rva; uint32_t size; } dataDirs[16] = {
|
||||||
|
{0x00000000, 0x00000000}, // 0: Export
|
||||||
|
{0x00014000, 0x000000A0}, // 1: Import
|
||||||
|
{0x00000000, 0x00000000}, // 2: Resource
|
||||||
|
{0x00000000, 0x00000000}, // 3: Exception
|
||||||
|
{0x00000000, 0x00000000}, // 4: Security
|
||||||
|
{0x00000000, 0x00000000}, // 5: BaseReloc
|
||||||
|
{0x00013000, 0x00000038}, // 6: Debug
|
||||||
|
{0x00000000, 0x00000000}, // 7: Architecture
|
||||||
|
{0x00000000, 0x00000000}, // 8: GlobalPtr
|
||||||
|
{0x00000000, 0x00000000}, // 9: TLS
|
||||||
|
{0x00000000, 0x00000000}, // 10: LoadConfig
|
||||||
|
{0x00000000, 0x00000000}, // 11: BoundImport
|
||||||
|
{0x00014050, 0x00000048}, // 12: IAT
|
||||||
|
{0x00000000, 0x00000000}, // 13: DelayImport
|
||||||
|
{0x00000000, 0x00000000}, // 14: CLR
|
||||||
|
{0x00000000, 0x00000000}, // 15: Reserved
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
*(uint32_t*)(d + ddOff + i*8 + 0) = dataDirs[i].rva;
|
||||||
|
*(uint32_t*)(d + ddOff + i*8 + 4) = dataDirs[i].size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── IMAGE_OPTIONAL_HEADER64 (nested struct) ──
|
// ── Section Headers (4 sections × 40 bytes) ──
|
||||||
{
|
const int shOff = ddOff + 128; // 0x188
|
||||||
Node ohStruct;
|
struct SectionDef { const char* name; uint32_t vsize; uint32_t vaddr; uint32_t rawsz; uint32_t rawptr; uint32_t chars; };
|
||||||
ohStruct.kind = NodeKind::Struct;
|
SectionDef sections[4] = {
|
||||||
ohStruct.name = "IMAGE_OPTIONAL_HEADER64";
|
{".text", 0x00011234, 0x00001000, 0x00011400, 0x00000200, 0x60000020}, // CODE|EXECUTE|READ
|
||||||
ohStruct.parentId = rootId;
|
{".rdata", 0x00002ABC, 0x00013000, 0x00002C00, 0x00011600, 0x40000040}, // INITIALIZED|READ
|
||||||
ohStruct.offset = oh;
|
{".data", 0x00001000, 0x00016000, 0x00000400, 0x00014200, 0xC0000040}, // INITIALIZED|READ|WRITE
|
||||||
int oi = doc->tree.addNode(ohStruct);
|
{".pdata", 0x00000800, 0x00017000, 0x00000800, 0x00014600, 0x40000040}, // INITIALIZED|READ
|
||||||
uint64_t ohId = doc->tree.nodes[oi].id;
|
};
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
auto addOH = [&](NodeKind k, const QString& name, int off) {
|
int off = shOff + i * 40;
|
||||||
Node n;
|
memcpy(d + off, sections[i].name, 8); // Name[8]
|
||||||
n.kind = k;
|
*(uint32_t*)(d + off + 8) = sections[i].vsize; // VirtualSize
|
||||||
n.name = name;
|
*(uint32_t*)(d + off + 12) = sections[i].vaddr; // VirtualAddress
|
||||||
n.offset = off;
|
*(uint32_t*)(d + off + 16) = sections[i].rawsz; // SizeOfRawData
|
||||||
n.parentId = ohId;
|
*(uint32_t*)(d + off + 20) = sections[i].rawptr; // PointerToRawData
|
||||||
doc->tree.addNode(n);
|
*(uint32_t*)(d + off + 24) = 0x00000000; // PointerToRelocations
|
||||||
};
|
*(uint32_t*)(d + off + 28) = 0x00000000; // PointerToLinenumbers
|
||||||
|
*(uint16_t*)(d + off + 32) = 0x0000; // NumberOfRelocations
|
||||||
addOH(NodeKind::UInt16, "Magic", 0x00);
|
*(uint16_t*)(d + off + 34) = 0x0000; // NumberOfLinenumbers
|
||||||
addOH(NodeKind::UInt8, "MajorLinkerVersion", 0x02);
|
*(uint32_t*)(d + off + 36) = sections[i].chars; // Characteristics
|
||||||
addOH(NodeKind::UInt8, "MinorLinkerVersion", 0x03);
|
|
||||||
addOH(NodeKind::UInt32, "SizeOfCode", 0x04);
|
|
||||||
addOH(NodeKind::UInt32, "SizeOfInitData", 0x08);
|
|
||||||
addOH(NodeKind::UInt32, "SizeOfUninitData", 0x0C);
|
|
||||||
addOH(NodeKind::UInt32, "AddressOfEntryPoint", 0x10);
|
|
||||||
addOH(NodeKind::UInt32, "BaseOfCode", 0x14);
|
|
||||||
addOH(NodeKind::UInt64, "ImageBase", 0x18);
|
|
||||||
addOH(NodeKind::UInt32, "SectionAlignment", 0x20);
|
|
||||||
addOH(NodeKind::UInt32, "FileAlignment", 0x24);
|
|
||||||
addOH(NodeKind::UInt16, "MajorOSVersion", 0x28);
|
|
||||||
addOH(NodeKind::UInt16, "MinorOSVersion", 0x2A);
|
|
||||||
addOH(NodeKind::UInt16, "MajorImageVersion", 0x2C);
|
|
||||||
addOH(NodeKind::UInt16, "MinorImageVersion", 0x2E);
|
|
||||||
addOH(NodeKind::UInt16, "MajorSubsysVersion", 0x30);
|
|
||||||
addOH(NodeKind::UInt16, "MinorSubsysVersion", 0x32);
|
|
||||||
addOH(NodeKind::UInt32, "Win32VersionValue", 0x34);
|
|
||||||
addOH(NodeKind::UInt32, "SizeOfImage", 0x38);
|
|
||||||
addOH(NodeKind::UInt32, "SizeOfHeaders", 0x3C);
|
|
||||||
addOH(NodeKind::UInt32, "CheckSum", 0x40);
|
|
||||||
addOH(NodeKind::UInt16, "Subsystem", 0x44);
|
|
||||||
addOH(NodeKind::UInt16, "DllCharacteristics", 0x46);
|
|
||||||
addOH(NodeKind::UInt64, "SizeOfStackReserve", 0x48);
|
|
||||||
addOH(NodeKind::UInt64, "SizeOfStackCommit", 0x50);
|
|
||||||
addOH(NodeKind::UInt64, "SizeOfHeapReserve", 0x58);
|
|
||||||
addOH(NodeKind::UInt64, "SizeOfHeapCommit", 0x60);
|
|
||||||
addOH(NodeKind::UInt32, "LoaderFlags", 0x68);
|
|
||||||
addOH(NodeKind::UInt32, "NumberOfRvaAndSizes", 0x6C);
|
|
||||||
|
|
||||||
// Data directories (16 entries × 8 bytes)
|
|
||||||
static const char* dirNames[] = {
|
|
||||||
"Export", "Import", "Resource", "Exception",
|
|
||||||
"Security", "BaseReloc", "Debug", "Architecture",
|
|
||||||
"GlobalPtr", "TLS", "LoadConfig", "BoundImport",
|
|
||||||
"IAT", "DelayImport", "CLR", "Reserved"
|
|
||||||
};
|
|
||||||
for (int i = 0; i < 16; i++) {
|
|
||||||
int doff = 0x70 + i * 8;
|
|
||||||
addOH(NodeKind::UInt32, QString("%1_RVA").arg(dirNames[i]), doff);
|
|
||||||
addOH(NodeKind::UInt32, QString("%1_Size").arg(dirNames[i]), doff + 4);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fill with Hex64 until 0x6000 for stress testing ──
|
doc->loadData(peData);
|
||||||
int padStart = oh + 0xF0; // end of optional header
|
doc->tree.baseAddress = 0x140000000; // Typical 64-bit image base
|
||||||
for (int off = padStart; off < 0x6000; off += 8) {
|
|
||||||
add(NodeKind::Hex64,
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
QString("data_%1").arg(off, 4, 16, QChar('0')),
|
// Build Node Tree
|
||||||
off);
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
auto addField = [&](uint64_t parent, int offset, NodeKind kind, const QString& name) -> uint64_t {
|
||||||
|
Node n;
|
||||||
|
n.kind = kind;
|
||||||
|
n.name = name;
|
||||||
|
n.parentId = parent;
|
||||||
|
n.offset = offset;
|
||||||
|
int idx = doc->tree.addNode(n);
|
||||||
|
return doc->tree.nodes[idx].id;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto addStruct = [&](uint64_t parent, int offset, const QString& name) -> uint64_t {
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::Struct;
|
||||||
|
n.name = name;
|
||||||
|
n.parentId = parent;
|
||||||
|
n.offset = offset;
|
||||||
|
int idx = doc->tree.addNode(n);
|
||||||
|
return doc->tree.nodes[idx].id;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto addArray = [&](uint64_t parent, int offset, const QString& name, int count, NodeKind elemKind) -> uint64_t {
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::Array;
|
||||||
|
n.name = name;
|
||||||
|
n.parentId = parent;
|
||||||
|
n.offset = offset;
|
||||||
|
n.arrayLen = count;
|
||||||
|
n.elementKind = elemKind;
|
||||||
|
int idx = doc->tree.addNode(n);
|
||||||
|
return doc->tree.nodes[idx].id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Root: IMAGE_DOS_HEADER ──
|
||||||
|
uint64_t dosId = addStruct(0, 0x00, "IMAGE_DOS_HEADER");
|
||||||
|
addField(dosId, 0x00, NodeKind::UInt16, "e_magic");
|
||||||
|
addField(dosId, 0x02, NodeKind::UInt16, "e_cblp");
|
||||||
|
addField(dosId, 0x04, NodeKind::UInt16, "e_cp");
|
||||||
|
addField(dosId, 0x06, NodeKind::UInt16, "e_crlc");
|
||||||
|
addField(dosId, 0x08, NodeKind::UInt16, "e_cparhdr");
|
||||||
|
addField(dosId, 0x0A, NodeKind::UInt16, "e_minalloc");
|
||||||
|
addField(dosId, 0x0C, NodeKind::UInt16, "e_maxalloc");
|
||||||
|
addField(dosId, 0x0E, NodeKind::UInt16, "e_ss");
|
||||||
|
addField(dosId, 0x10, NodeKind::UInt16, "e_sp");
|
||||||
|
addField(dosId, 0x12, NodeKind::UInt16, "e_csum");
|
||||||
|
addField(dosId, 0x14, NodeKind::UInt16, "e_ip");
|
||||||
|
addField(dosId, 0x16, NodeKind::UInt16, "e_cs");
|
||||||
|
addField(dosId, 0x18, NodeKind::UInt16, "e_lfarlc");
|
||||||
|
addField(dosId, 0x1A, NodeKind::UInt16, "e_ovno");
|
||||||
|
addField(dosId, 0x3C, NodeKind::UInt32, "e_lfanew");
|
||||||
|
|
||||||
|
// ── PE Signature ──
|
||||||
|
addField(0, peOff, NodeKind::UInt32, "PE_Signature");
|
||||||
|
|
||||||
|
// ── IMAGE_FILE_HEADER ──
|
||||||
|
uint64_t fhId = addStruct(0, fhOff, "IMAGE_FILE_HEADER");
|
||||||
|
addField(fhId, 0, NodeKind::UInt16, "Machine");
|
||||||
|
addField(fhId, 2, NodeKind::UInt16, "NumberOfSections");
|
||||||
|
addField(fhId, 4, NodeKind::UInt32, "TimeDateStamp");
|
||||||
|
addField(fhId, 8, NodeKind::UInt32, "PointerToSymbolTable");
|
||||||
|
addField(fhId, 12, NodeKind::UInt32, "NumberOfSymbols");
|
||||||
|
addField(fhId, 16, NodeKind::UInt16, "SizeOfOptionalHeader");
|
||||||
|
addField(fhId, 18, NodeKind::UInt16, "Characteristics");
|
||||||
|
|
||||||
|
// ── IMAGE_OPTIONAL_HEADER64 ──
|
||||||
|
uint64_t ohId = addStruct(0, ohOff, "IMAGE_OPTIONAL_HEADER64");
|
||||||
|
addField(ohId, 0, NodeKind::UInt16, "Magic");
|
||||||
|
addField(ohId, 2, NodeKind::UInt8, "MajorLinkerVersion");
|
||||||
|
addField(ohId, 3, NodeKind::UInt8, "MinorLinkerVersion");
|
||||||
|
addField(ohId, 4, NodeKind::UInt32, "SizeOfCode");
|
||||||
|
addField(ohId, 8, NodeKind::UInt32, "SizeOfInitializedData");
|
||||||
|
addField(ohId, 12, NodeKind::UInt32, "SizeOfUninitializedData");
|
||||||
|
addField(ohId, 16, NodeKind::UInt32, "AddressOfEntryPoint");
|
||||||
|
addField(ohId, 20, NodeKind::UInt32, "BaseOfCode");
|
||||||
|
addField(ohId, 24, NodeKind::UInt64, "ImageBase");
|
||||||
|
addField(ohId, 32, NodeKind::UInt32, "SectionAlignment");
|
||||||
|
addField(ohId, 36, NodeKind::UInt32, "FileAlignment");
|
||||||
|
addField(ohId, 40, NodeKind::UInt16, "MajorOSVersion");
|
||||||
|
addField(ohId, 42, NodeKind::UInt16, "MinorOSVersion");
|
||||||
|
addField(ohId, 44, NodeKind::UInt16, "MajorImageVersion");
|
||||||
|
addField(ohId, 46, NodeKind::UInt16, "MinorImageVersion");
|
||||||
|
addField(ohId, 48, NodeKind::UInt16, "MajorSubsystemVersion");
|
||||||
|
addField(ohId, 50, NodeKind::UInt16, "MinorSubsystemVersion");
|
||||||
|
addField(ohId, 52, NodeKind::UInt32, "Win32VersionValue");
|
||||||
|
addField(ohId, 56, NodeKind::UInt32, "SizeOfImage");
|
||||||
|
addField(ohId, 60, NodeKind::UInt32, "SizeOfHeaders");
|
||||||
|
addField(ohId, 64, NodeKind::UInt32, "CheckSum");
|
||||||
|
addField(ohId, 68, NodeKind::UInt16, "Subsystem");
|
||||||
|
addField(ohId, 70, NodeKind::UInt16, "DllCharacteristics");
|
||||||
|
addField(ohId, 72, NodeKind::UInt64, "SizeOfStackReserve");
|
||||||
|
addField(ohId, 80, NodeKind::UInt64, "SizeOfStackCommit");
|
||||||
|
addField(ohId, 88, NodeKind::UInt64, "SizeOfHeapReserve");
|
||||||
|
addField(ohId, 96, NodeKind::UInt64, "SizeOfHeapCommit");
|
||||||
|
addField(ohId, 104, NodeKind::UInt32, "LoaderFlags");
|
||||||
|
addField(ohId, 108, NodeKind::UInt32, "NumberOfRvaAndSizes");
|
||||||
|
|
||||||
|
// ── Data Directories Array (16 entries) ──
|
||||||
|
uint64_t ddArrId = addArray(ohId, 112, "DataDirectory", 16, NodeKind::Struct);
|
||||||
|
const char* ddNames[16] = {
|
||||||
|
"Export", "Import", "Resource", "Exception",
|
||||||
|
"Security", "BaseReloc", "Debug", "Architecture",
|
||||||
|
"GlobalPtr", "TLS", "LoadConfig", "BoundImport",
|
||||||
|
"IAT", "DelayImport", "CLR", "Reserved"
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
uint64_t entryId = addStruct(ddArrId, i * 8, QString("[%1] %2").arg(i).arg(ddNames[i]));
|
||||||
|
addField(entryId, 0, NodeKind::UInt32, "VirtualAddress");
|
||||||
|
addField(entryId, 4, NodeKind::UInt32, "Size");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Section Headers Array (4 sections) ──
|
||||||
|
uint64_t shArrId = addArray(0, shOff, "SectionHeaders", 4, NodeKind::Struct);
|
||||||
|
const char* secNames[4] = {".text", ".rdata", ".data", ".pdata"};
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
uint64_t secId = addStruct(shArrId, i * 40, QString("[%1] %2").arg(i).arg(secNames[i]));
|
||||||
|
// Name is 8 bytes - show as UTF8 string
|
||||||
|
Node nameNode;
|
||||||
|
nameNode.kind = NodeKind::UTF8;
|
||||||
|
nameNode.name = "Name";
|
||||||
|
nameNode.parentId = secId;
|
||||||
|
nameNode.offset = 0;
|
||||||
|
nameNode.strLen = 8;
|
||||||
|
doc->tree.addNode(nameNode);
|
||||||
|
addField(secId, 8, NodeKind::UInt32, "VirtualSize");
|
||||||
|
addField(secId, 12, NodeKind::UInt32, "VirtualAddress");
|
||||||
|
addField(secId, 16, NodeKind::UInt32, "SizeOfRawData");
|
||||||
|
addField(secId, 20, NodeKind::UInt32, "PointerToRawData");
|
||||||
|
addField(secId, 24, NodeKind::UInt32, "PointerToRelocations");
|
||||||
|
addField(secId, 28, NodeKind::UInt32, "PointerToLinenumbers");
|
||||||
|
addField(secId, 32, NodeKind::UInt16, "NumberOfRelocations");
|
||||||
|
addField(secId, 34, NodeKind::UInt16, "NumberOfLinenumbers");
|
||||||
|
addField(secId, 36, NodeKind::UInt32, "Characteristics");
|
||||||
}
|
}
|
||||||
|
|
||||||
createTab(doc);
|
createTab(doc);
|
||||||
|
|||||||
@@ -608,7 +608,7 @@ private slots:
|
|||||||
QCOMPARE(result.meta[2].lineKind, LineKind::Header); // Recursive header (expansion)
|
QCOMPARE(result.meta[2].lineKind, LineKind::Header); // Recursive header (expansion)
|
||||||
}
|
}
|
||||||
|
|
||||||
void testStructFooterSizeof() {
|
void testStructFooterSimple() {
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = 0;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
@@ -627,13 +627,6 @@ private slots:
|
|||||||
f1.offset = 0;
|
f1.offset = 0;
|
||||||
tree.addNode(f1);
|
tree.addNode(f1);
|
||||||
|
|
||||||
Node f2;
|
|
||||||
f2.kind = NodeKind::UInt64;
|
|
||||||
f2.name = "b";
|
|
||||||
f2.parentId = rootId;
|
|
||||||
f2.offset = 4;
|
|
||||||
tree.addNode(f2);
|
|
||||||
|
|
||||||
NullProvider prov;
|
NullProvider prov;
|
||||||
ComposeResult result = compose(tree, prov);
|
ComposeResult result = compose(tree, prov);
|
||||||
|
|
||||||
@@ -641,9 +634,10 @@ private slots:
|
|||||||
int lastLine = result.meta.size() - 1;
|
int lastLine = result.meta.size() - 1;
|
||||||
QCOMPARE(result.meta[lastLine].lineKind, LineKind::Footer);
|
QCOMPARE(result.meta[lastLine].lineKind, LineKind::Footer);
|
||||||
|
|
||||||
// Footer text should contain sizeof(Sized)=0xC (4+8=12=0xC)
|
// Footer text should just be "};" (no sizeof)
|
||||||
QString footerText = result.text.split('\n').last();
|
QString footerText = result.text.split('\n').last();
|
||||||
QVERIFY(footerText.contains("sizeof(Sized)=0xC"));
|
QVERIFY(footerText.contains("};"));
|
||||||
|
QVERIFY(!footerText.contains("sizeof"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void testLineMetaHasNodeId() {
|
void testLineMetaHasNodeId() {
|
||||||
@@ -669,115 +663,6 @@ private slots:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void testSizeofUpdatesAfterDelete() {
|
|
||||||
// Test that sizeof recalculates after deleting a node
|
|
||||||
NodeTree tree;
|
|
||||||
tree.baseAddress = 0;
|
|
||||||
|
|
||||||
Node root;
|
|
||||||
root.kind = NodeKind::Struct;
|
|
||||||
root.name = "Test";
|
|
||||||
root.parentId = 0;
|
|
||||||
root.offset = 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::UInt64;
|
|
||||||
f2.name = "b";
|
|
||||||
f2.parentId = rootId;
|
|
||||||
f2.offset = 4;
|
|
||||||
int f2i = tree.addNode(f2);
|
|
||||||
uint64_t f2Id = tree.nodes[f2i].id;
|
|
||||||
|
|
||||||
NullProvider prov;
|
|
||||||
|
|
||||||
// First compose: sizeof should be 0xC (4+8=12)
|
|
||||||
ComposeResult result1 = compose(tree, prov);
|
|
||||||
QString footer1 = result1.text.split('\n').last();
|
|
||||||
QVERIFY2(footer1.contains("sizeof(Test)=0xC"),
|
|
||||||
qPrintable("Before delete: " + footer1));
|
|
||||||
|
|
||||||
// Delete the second field
|
|
||||||
int idx = tree.indexOfId(f2Id);
|
|
||||||
QVERIFY(idx >= 0);
|
|
||||||
tree.nodes.remove(idx);
|
|
||||||
tree.invalidateIdCache();
|
|
||||||
|
|
||||||
// Second compose: sizeof should be 0x4 (only UInt32 remains)
|
|
||||||
ComposeResult result2 = compose(tree, prov);
|
|
||||||
QString footer2 = result2.text.split('\n').last();
|
|
||||||
QVERIFY2(footer2.contains("sizeof(Test)=0x4"),
|
|
||||||
qPrintable("After delete: " + footer2));
|
|
||||||
}
|
|
||||||
|
|
||||||
void testNestedStructSizeofUpdates() {
|
|
||||||
// Test nested struct sizeof updates when child is deleted
|
|
||||||
NodeTree tree;
|
|
||||||
tree.baseAddress = 0;
|
|
||||||
|
|
||||||
// Root struct
|
|
||||||
Node root;
|
|
||||||
root.kind = NodeKind::Struct;
|
|
||||||
root.name = "Root";
|
|
||||||
root.parentId = 0;
|
|
||||||
root.offset = 0;
|
|
||||||
int ri = tree.addNode(root);
|
|
||||||
uint64_t rootId = tree.nodes[ri].id;
|
|
||||||
|
|
||||||
// Nested struct (like IMAGE_FILE_HEADER)
|
|
||||||
Node nested;
|
|
||||||
nested.kind = NodeKind::Struct;
|
|
||||||
nested.name = "Nested";
|
|
||||||
nested.parentId = rootId;
|
|
||||||
nested.offset = 0;
|
|
||||||
int ni = tree.addNode(nested);
|
|
||||||
uint64_t nestedId = tree.nodes[ni].id;
|
|
||||||
|
|
||||||
// Field in nested struct
|
|
||||||
Node f1;
|
|
||||||
f1.kind = NodeKind::UInt32;
|
|
||||||
f1.name = "a";
|
|
||||||
f1.parentId = nestedId;
|
|
||||||
f1.offset = 0;
|
|
||||||
tree.addNode(f1);
|
|
||||||
|
|
||||||
Node f2;
|
|
||||||
f2.kind = NodeKind::UInt32;
|
|
||||||
f2.name = "b";
|
|
||||||
f2.parentId = nestedId;
|
|
||||||
f2.offset = 4;
|
|
||||||
int f2i = tree.addNode(f2);
|
|
||||||
uint64_t f2Id = tree.nodes[f2i].id;
|
|
||||||
|
|
||||||
NullProvider prov;
|
|
||||||
|
|
||||||
// First compose
|
|
||||||
ComposeResult result1 = compose(tree, prov);
|
|
||||||
// Find nested struct footer
|
|
||||||
QString text1 = result1.text;
|
|
||||||
QVERIFY2(text1.contains("sizeof(Nested)=0x8"),
|
|
||||||
qPrintable("Before delete nested sizeof: " + text1));
|
|
||||||
|
|
||||||
// Delete field from nested struct
|
|
||||||
int idx = tree.indexOfId(f2Id);
|
|
||||||
QVERIFY(idx >= 0);
|
|
||||||
tree.nodes.remove(idx);
|
|
||||||
tree.invalidateIdCache();
|
|
||||||
|
|
||||||
// Second compose - nested sizeof should update
|
|
||||||
ComposeResult result2 = compose(tree, prov);
|
|
||||||
QString text2 = result2.text;
|
|
||||||
QVERIFY2(text2.contains("sizeof(Nested)=0x4"),
|
|
||||||
qPrintable("After delete nested sizeof: " + text2));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_MAIN(TestCompose)
|
QTEST_MAIN(TestCompose)
|
||||||
|
|||||||
@@ -326,17 +326,17 @@ private slots:
|
|||||||
auto ts = rcx::typeSpanFor(lm);
|
auto ts = rcx::typeSpanFor(lm);
|
||||||
QVERIFY(ts.valid);
|
QVERIFY(ts.valid);
|
||||||
QCOMPARE(ts.start, 6);
|
QCOMPARE(ts.start, 6);
|
||||||
QCOMPARE(ts.end, 16); // 6 + 10
|
QCOMPARE(ts.end, 20); // 6 + 14 (kColType)
|
||||||
|
|
||||||
auto ns = rcx::nameSpanFor(lm);
|
auto ns = rcx::nameSpanFor(lm);
|
||||||
QVERIFY(ns.valid);
|
QVERIFY(ns.valid);
|
||||||
QCOMPARE(ns.start, 18); // 6 + 10 + 2
|
QCOMPARE(ns.start, 22); // 6 + 14 + 2
|
||||||
QCOMPARE(ns.end, 42); // 18 + 24
|
QCOMPARE(ns.end, 44); // 22 + 22 (kColName)
|
||||||
|
|
||||||
auto vs = rcx::valueSpanFor(lm, 60);
|
auto vs = rcx::valueSpanFor(lm, 100);
|
||||||
QVERIFY(vs.valid);
|
QVERIFY(vs.valid);
|
||||||
QCOMPARE(vs.start, 44); // 18 + 24 + 2
|
QCOMPARE(vs.start, 46); // 22 + 22 + 2
|
||||||
QCOMPARE(vs.end, 60);
|
QCOMPARE(vs.end, 78); // 46 + 32 (kColValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
void testColumnSpan_continuation() {
|
void testColumnSpan_continuation() {
|
||||||
@@ -349,10 +349,10 @@ private slots:
|
|||||||
QVERIFY(!rcx::typeSpanFor(lm).valid);
|
QVERIFY(!rcx::typeSpanFor(lm).valid);
|
||||||
QVERIFY(!rcx::nameSpanFor(lm).valid);
|
QVERIFY(!rcx::nameSpanFor(lm).valid);
|
||||||
|
|
||||||
auto vs = rcx::valueSpanFor(lm, 60);
|
auto vs = rcx::valueSpanFor(lm, 100);
|
||||||
QVERIFY(vs.valid);
|
QVERIFY(vs.valid);
|
||||||
QCOMPARE(vs.start, 6 + 10 + 24 + 4); // kFoldCol+indent + COL_TYPE + COL_NAME + 4
|
QCOMPARE(vs.start, 6 + 14 + 22 + 4); // kFoldCol+indent + kColType(14) + kColName(22) + 4
|
||||||
QCOMPARE(vs.end, 60);
|
QCOMPARE(vs.end, 46 + 32); // start + kColValue
|
||||||
}
|
}
|
||||||
|
|
||||||
void testColumnSpan_headerFooter() {
|
void testColumnSpan_headerFooter() {
|
||||||
@@ -382,17 +382,17 @@ private slots:
|
|||||||
auto ts = rcx::typeSpanFor(lm);
|
auto ts = rcx::typeSpanFor(lm);
|
||||||
QVERIFY(ts.valid);
|
QVERIFY(ts.valid);
|
||||||
QCOMPARE(ts.start, 3);
|
QCOMPARE(ts.start, 3);
|
||||||
QCOMPARE(ts.end, 13); // 3 + 10
|
QCOMPARE(ts.end, 17); // 3 + 14 (kColType)
|
||||||
|
|
||||||
auto ns = rcx::nameSpanFor(lm);
|
auto ns = rcx::nameSpanFor(lm);
|
||||||
QVERIFY(ns.valid);
|
QVERIFY(ns.valid);
|
||||||
QCOMPARE(ns.start, 15); // 3 + 10 + 2
|
QCOMPARE(ns.start, 19); // 3 + 14 + 2
|
||||||
QCOMPARE(ns.end, 39); // 15 + 24
|
QCOMPARE(ns.end, 41); // 19 + 22 (kColName)
|
||||||
|
|
||||||
auto vs = rcx::valueSpanFor(lm, 50);
|
auto vs = rcx::valueSpanFor(lm, 100);
|
||||||
QVERIFY(vs.valid);
|
QVERIFY(vs.valid);
|
||||||
QCOMPARE(vs.start, 41); // 15 + 24 + 2
|
QCOMPARE(vs.start, 43); // 19 + 22 + 2
|
||||||
QCOMPARE(vs.end, 50);
|
QCOMPARE(vs.end, 75); // 43 + 32 (kColValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNodeIdJsonRoundTrip() {
|
void testNodeIdJsonRoundTrip() {
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ private slots:
|
|||||||
void testTypeName() {
|
void testTypeName() {
|
||||||
QString s = fmt::typeName(NodeKind::Float);
|
QString s = fmt::typeName(NodeKind::Float);
|
||||||
QVERIFY(s.trimmed() == "float");
|
QVERIFY(s.trimmed() == "float");
|
||||||
QCOMPARE(s.size(), 10); // COL_TYPE
|
QCOMPARE(s.size(), 14); // kColType
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFmtInt32() {
|
void testFmtInt32() {
|
||||||
QCOMPARE(fmt::fmtInt32(-42), QString("-42"));
|
// fmtInt32 outputs hex representation (0xffffffd6 for -42)
|
||||||
QCOMPARE(fmt::fmtInt32(0), QString("0"));
|
QCOMPARE(fmt::fmtInt32(-42), QString("0xffffffd6"));
|
||||||
|
QCOMPARE(fmt::fmtInt32(0), QString("0x0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFmtFloat() {
|
void testFmtFloat() {
|
||||||
@@ -224,25 +225,15 @@ private slots:
|
|||||||
QVERIFY(!ok);
|
QVERIFY(!ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFmtStructFooterWithSize() {
|
void testFmtStructFooterSimple() {
|
||||||
Node n;
|
Node n;
|
||||||
n.kind = NodeKind::Struct;
|
n.kind = NodeKind::Struct;
|
||||||
n.name = "Test";
|
n.name = "Test";
|
||||||
|
|
||||||
// With size
|
// Footer is always just "};" (no sizeof comment)
|
||||||
QString s1 = fmt::fmtStructFooter(n, 0, 0x14);
|
QString s = fmt::fmtStructFooter(n, 0, 0x14);
|
||||||
QVERIFY(s1.contains("};"));
|
QVERIFY(s.contains("};"));
|
||||||
QVERIFY(s1.contains("sizeof(Test)=0x14"));
|
QVERIFY(!s.contains("sizeof")); // No sizeof comment
|
||||||
|
|
||||||
// Size 0 → no sizeof
|
|
||||||
QString s2 = fmt::fmtStructFooter(n, 0, 0);
|
|
||||||
QVERIFY(s2.contains("};"));
|
|
||||||
QVERIFY(!s2.contains("sizeof"));
|
|
||||||
|
|
||||||
// Default (no size arg) → no sizeof
|
|
||||||
QString s3 = fmt::fmtStructFooter(n, 0);
|
|
||||||
QVERIFY(s3.contains("};"));
|
|
||||||
QVERIFY(!s3.contains("sizeof"));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user