mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add class keyword picker, alignas alignment picker with member realignment
- Class keyword (struct/class/union/enum) persists in JSON, drives generator output - CommandRow2 shows alignas(N) pill computed from struct member alignment - Clicking alignas opens picker (1, 4, 8, 16) to realign all members - Going up: members repositioned to N-byte boundaries, padding fills gaps - Going down: excess padding removed, members pack tighter - Add cmd::ChangeOffset for undoable offset repositioning - All 11 tests pass Co-Authored-By: combuter <combuter@users.noreply.github.com>
This commit is contained in:
@@ -39,7 +39,7 @@ struct ComposeState {
|
||||
if (currentLine > 0) text += '\n';
|
||||
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
|
||||
// CommandRow has no fold prefix (flush left)
|
||||
if (lm.lineKind == LineKind::CommandRow) {
|
||||
if (lm.lineKind == LineKind::CommandRow || lm.lineKind == LineKind::CommandRow2) {
|
||||
// no prefix
|
||||
} else if (lm.foldHead)
|
||||
text += lm.foldCollapsed ? QStringLiteral(" + ") : QStringLiteral(" - ");
|
||||
@@ -148,6 +148,16 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
lm.effectiveNameW = nameW;
|
||||
lm.pointerTargetName = ptrTargetName;
|
||||
|
||||
// Set byte count for hex preview lines (used for per-byte change highlighting)
|
||||
if (isHexPreview(node.kind)) {
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
int totalSz = qMax(1, node.arrayLen);
|
||||
lm.lineByteCount = qMin(8, totalSz - sub * 8);
|
||||
} else {
|
||||
lm.lineByteCount = sizeForKind(node.kind);
|
||||
}
|
||||
}
|
||||
|
||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride);
|
||||
state.emitLine(lineText, lm);
|
||||
@@ -203,8 +213,14 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("[%1]").arg(arrayElementIdx), lm);
|
||||
}
|
||||
|
||||
// Header line (skip for array element structs - condensed display)
|
||||
if (!isArrayChild) {
|
||||
// Detect root header: first root-level struct — suppressed from display
|
||||
// (CommandRow2 already shows the root class type + name)
|
||||
bool isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct && !state.baseEmitted);
|
||||
if (isRootHeader)
|
||||
state.baseEmitted = true;
|
||||
|
||||
// Header line (skip for array element structs and root struct)
|
||||
if (!isArrayChild && !isRootHeader) {
|
||||
// Get per-scope widths for this header's parent scope
|
||||
int typeW = state.effectiveTypeW(scopeId);
|
||||
int nameW = state.effectiveNameW(scopeId);
|
||||
@@ -216,12 +232,11 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.lineKind = LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||
lm.nodeKind = node.kind;
|
||||
lm.isRootHeader = false;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = node.collapsed;
|
||||
lm.foldLevel = computeFoldLevel(depth, true);
|
||||
lm.markerMask = (1u << M_STRUCT_BG);
|
||||
lm.isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct && !state.baseEmitted);
|
||||
if (lm.isRootHeader) state.baseEmitted = true;
|
||||
lm.effectiveTypeW = typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
|
||||
@@ -240,26 +255,29 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
state.emitLine(headerText, lm);
|
||||
}
|
||||
|
||||
if (!node.collapsed || isArrayChild) {
|
||||
if (!node.collapsed || isArrayChild || isRootHeader) {
|
||||
QVector<int> children = state.childMap.value(node.id);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
// Root struct children compose at same depth (no header to indent from)
|
||||
int childDepth = isRootHeader ? depth : depth + 1;
|
||||
|
||||
// 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) {
|
||||
// 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,
|
||||
composeNode(state, tree, prov, childIdx, childDepth, base, rootId,
|
||||
childrenAreArrayElements, node.id,
|
||||
childrenAreArrayElements ? elementIdx++ : -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Footer line: skip when collapsed (only header shows) or for array element structs
|
||||
if (!isArrayChild && !node.collapsed) {
|
||||
// Footer line: skip when collapsed, for array element structs, or for root struct
|
||||
if (!isArrayChild && !isRootHeader && !node.collapsed) {
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
@@ -464,6 +482,22 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
||||
state.emitLine(QStringLiteral("File Address: 0x0"), lm);
|
||||
}
|
||||
|
||||
// Emit CommandRow2 as line 1 (root class type + name)
|
||||
{
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = -1;
|
||||
lm.nodeId = kCommandRow2Id;
|
||||
lm.depth = 0;
|
||||
lm.lineKind = LineKind::CommandRow2;
|
||||
lm.foldLevel = SC_FOLDLEVELBASE;
|
||||
lm.foldHead = false;
|
||||
lm.offsetText.clear();
|
||||
lm.markerMask = 0;
|
||||
lm.effectiveTypeW = state.typeW;
|
||||
lm.effectiveNameW = state.nameW;
|
||||
state.emitLine(QStringLiteral("struct <no class> alignas(1)"), lm);
|
||||
}
|
||||
|
||||
QVector<int> roots = state.childMap.value(0);
|
||||
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
@@ -515,4 +549,20 @@ QSet<uint64_t> NodeTree::normalizePreferDescendants(const QSet<uint64_t>& ids) c
|
||||
return result;
|
||||
}
|
||||
|
||||
int NodeTree::computeStructAlignment(uint64_t structId) const {
|
||||
int idx = indexOfId(structId);
|
||||
if (idx < 0) return 1;
|
||||
int maxAlign = 1;
|
||||
QVector<int> kids = childrenOf(structId);
|
||||
for (int ci : kids) {
|
||||
const Node& c = nodes[ci];
|
||||
if (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) {
|
||||
maxAlign = qMax(maxAlign, computeStructAlignment(c.id));
|
||||
} else {
|
||||
maxAlign = qMax(maxAlign, alignmentFor(c.kind));
|
||||
}
|
||||
}
|
||||
return maxAlign;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -175,8 +175,10 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
// Inline editing signals
|
||||
connect(editor, &RcxEditor::inlineEditCommitted,
|
||||
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) {
|
||||
// CommandRow BaseAddress/Source edit has nodeIdx=-1
|
||||
if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source) { refresh(); return; }
|
||||
// CommandRow BaseAddress/Source edit has nodeIdx=-1; CommandRow2 edits too
|
||||
if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source
|
||||
&& target != EditTarget::RootClassType && target != EditTarget::RootClassName
|
||||
&& target != EditTarget::Alignas) { refresh(); return; }
|
||||
switch (target) {
|
||||
case EditTarget::Name: {
|
||||
if (text.isEmpty()) break;
|
||||
@@ -423,10 +425,59 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditTarget::RootClassType: {
|
||||
QString kw = text.toLower().trimmed();
|
||||
if (kw != QStringLiteral("struct") && kw != QStringLiteral("class") && kw != QStringLiteral("enum")) break;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
auto& n = m_doc->tree.nodes[i];
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
||||
QString oldKw = n.resolvedClassKeyword();
|
||||
if (oldKw != kw) {
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeClassKeyword{n.id, oldKw, kw}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditTarget::RootClassName: {
|
||||
// Rename the root struct's structTypeName
|
||||
if (!text.isEmpty()) {
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
auto& n = m_doc->tree.nodes[i];
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
||||
QString oldName = n.structTypeName;
|
||||
if (oldName != text) {
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeStructTypeName{n.id, oldName, text}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditTarget::ArrayIndex:
|
||||
case EditTarget::ArrayCount:
|
||||
// Array navigation removed - these cases are unreachable
|
||||
break;
|
||||
case EditTarget::Alignas: {
|
||||
// Parse "alignas(N)" → N
|
||||
int paren = text.indexOf('(');
|
||||
int close = text.indexOf(')');
|
||||
if (paren < 0 || close < 0) break;
|
||||
int newAlign = text.mid(paren + 1, close - paren - 1).toInt();
|
||||
if (newAlign <= 0) break;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
const auto& n = m_doc->tree.nodes[i];
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
||||
performRealignment(n.id, newAlign);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Always refresh to restore canonical text (handles parse failures, no-ops, etc.)
|
||||
refresh();
|
||||
@@ -447,11 +498,26 @@ void RcxController::refresh() {
|
||||
for (auto& lm : m_lastResult.meta) {
|
||||
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
|
||||
int64_t offset = m_doc->tree.computeOffset(lm.nodeIdx);
|
||||
int sz = m_doc->tree.nodes[lm.nodeIdx].byteSize();
|
||||
for (int64_t b = offset; b < offset + sz; b++) {
|
||||
if (m_changedOffsets.contains(b)) {
|
||||
lm.dataChanged = true;
|
||||
break;
|
||||
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
|
||||
|
||||
if (isHexPreview(node.kind)) {
|
||||
// Per-byte tracking for hex preview nodes
|
||||
int lineOff = (node.kind == NodeKind::Padding) ? lm.subLine * 8 : 0;
|
||||
int byteCount = lm.lineByteCount;
|
||||
for (int b = 0; b < byteCount; b++) {
|
||||
if (m_changedOffsets.contains(offset + lineOff + b)) {
|
||||
lm.changedByteIndices.append(b);
|
||||
lm.dataChanged = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Existing boolean logic for non-hex nodes
|
||||
int sz = node.byteSize();
|
||||
for (int64_t b = offset; b < offset + sz; b++) {
|
||||
if (m_changedOffsets.contains(b)) {
|
||||
lm.dataChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,6 +784,14 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
int idx = tree.indexOfId(c.nodeId);
|
||||
if (idx >= 0)
|
||||
tree.nodes[idx].structTypeName = isUndo ? c.oldName : c.newName;
|
||||
} else if constexpr (std::is_same_v<T, cmd::ChangeClassKeyword>) {
|
||||
int idx = tree.indexOfId(c.nodeId);
|
||||
if (idx >= 0)
|
||||
tree.nodes[idx].classKeyword = isUndo ? c.oldKeyword : c.newKeyword;
|
||||
} else if constexpr (std::is_same_v<T, cmd::ChangeOffset>) {
|
||||
int idx = tree.indexOfId(c.nodeId);
|
||||
if (idx >= 0)
|
||||
tree.nodes[idx].offset = isUndo ? c.oldOffset : c.newOffset;
|
||||
}
|
||||
}, command);
|
||||
|
||||
@@ -1052,7 +1126,7 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
||||
int to = qMax(m_anchorLine, line);
|
||||
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
|
||||
uint64_t nid = m_lastResult.meta[i].nodeId;
|
||||
if (nid != 0 && nid != kCommandRowId) m_selIds.insert(effectiveId(i, nid));
|
||||
if (nid != 0 && nid != kCommandRowId && nid != kCommandRow2Id) m_selIds.insert(effectiveId(i, nid));
|
||||
}
|
||||
}
|
||||
} else { // Ctrl+Shift
|
||||
@@ -1064,7 +1138,7 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
||||
int to = qMax(m_anchorLine, line);
|
||||
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
|
||||
uint64_t nid = m_lastResult.meta[i].nodeId;
|
||||
if (nid != 0 && nid != kCommandRowId) m_selIds.insert(effectiveId(i, nid));
|
||||
if (nid != 0 && nid != kCommandRowId && nid != kCommandRow2Id) m_selIds.insert(effectiveId(i, nid));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1092,6 +1166,100 @@ void RcxController::applySelectionOverlays() {
|
||||
editor->applySelectionOverlay(m_selIds);
|
||||
}
|
||||
|
||||
void RcxController::performRealignment(uint64_t structId, int targetAlign) {
|
||||
auto& tree = m_doc->tree;
|
||||
int rootIdx = tree.indexOfId(structId);
|
||||
if (rootIdx < 0) return;
|
||||
|
||||
// Gather direct children sorted by offset
|
||||
QVector<int> kids = tree.childrenOf(structId);
|
||||
std::sort(kids.begin(), kids.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
// Separate into real nodes (non-Padding) and padding nodes
|
||||
struct NodeInfo { uint64_t id; int offset; int size; };
|
||||
QVector<NodeInfo> realNodes;
|
||||
QVector<uint64_t> padIds;
|
||||
|
||||
for (int ci : kids) {
|
||||
const Node& child = tree.nodes[ci];
|
||||
int sz = (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
|
||||
? tree.structSpan(child.id) : child.byteSize();
|
||||
if (child.kind == NodeKind::Padding)
|
||||
padIds.append(child.id);
|
||||
else
|
||||
realNodes.append({child.id, child.offset, sz});
|
||||
}
|
||||
|
||||
auto roundUp = [](int x, int align) -> int {
|
||||
return align <= 1 ? x : ((x + align - 1) / align) * align;
|
||||
};
|
||||
|
||||
// Compute new offsets for real nodes
|
||||
struct OffChange { uint64_t id; int oldOff; int newOff; };
|
||||
QVector<OffChange> offChanges;
|
||||
int cursor = 0;
|
||||
for (auto& rn : realNodes) {
|
||||
int newOff = roundUp(cursor, targetAlign);
|
||||
if (newOff != rn.offset)
|
||||
offChanges.append({rn.id, rn.offset, newOff});
|
||||
rn.offset = newOff; // update local copy for gap computation
|
||||
cursor = newOff + rn.size;
|
||||
}
|
||||
|
||||
// Compute where padding is needed (gaps between consecutive nodes)
|
||||
struct PadInsert { int offset; int size; };
|
||||
QVector<PadInsert> padsNeeded;
|
||||
|
||||
for (int i = 0; i < realNodes.size(); i++) {
|
||||
int gapStart = (i == 0) ? 0 : realNodes[i - 1].offset + realNodes[i - 1].size;
|
||||
int gapEnd = realNodes[i].offset;
|
||||
if (gapEnd > gapStart)
|
||||
padsNeeded.append({gapStart, gapEnd - gapStart});
|
||||
}
|
||||
|
||||
// Check if anything actually changes
|
||||
if (offChanges.isEmpty() && padIds.isEmpty() && padsNeeded.isEmpty())
|
||||
return;
|
||||
|
||||
// Apply as undoable macro
|
||||
bool wasSuppressed = m_suppressRefresh;
|
||||
m_suppressRefresh = true;
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Realign to %1").arg(targetAlign));
|
||||
|
||||
// 1. Remove all existing Padding nodes (no offset adjustments — we recompute)
|
||||
for (uint64_t pid : padIds) {
|
||||
int idx = tree.indexOfId(pid);
|
||||
if (idx < 0) continue;
|
||||
QVector<Node> subtree;
|
||||
subtree.append(tree.nodes[idx]);
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::Remove{pid, subtree, {}}));
|
||||
}
|
||||
|
||||
// 2. Reposition real nodes
|
||||
for (const auto& oc : offChanges) {
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeOffset{oc.id, oc.oldOff, oc.newOff}));
|
||||
}
|
||||
|
||||
// 3. Insert new padding in gaps
|
||||
for (const auto& pi : padsNeeded) {
|
||||
Node pad;
|
||||
pad.kind = NodeKind::Padding;
|
||||
pad.parentId = structId;
|
||||
pad.offset = pi.offset;
|
||||
pad.arrayLen = pi.size;
|
||||
pad.id = tree.reserveId();
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{pad}));
|
||||
}
|
||||
|
||||
m_doc->undoStack.endMacro();
|
||||
m_suppressRefresh = wasSuppressed;
|
||||
if (!m_suppressRefresh) refresh();
|
||||
}
|
||||
|
||||
void RcxController::updateCommandRow() {
|
||||
// -- Source label: driven by provider metadata --
|
||||
QString src;
|
||||
@@ -1128,8 +1296,26 @@ void RcxController::updateCommandRow() {
|
||||
.arg(elide(src, 40), elide(addr, 24), elide(sym, 40));
|
||||
}
|
||||
|
||||
for (auto* ed : m_editors)
|
||||
// Build row 2: root class type + name + alignment
|
||||
QString row2;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
const auto& n = m_doc->tree.nodes[i];
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
||||
QString keyword = n.resolvedClassKeyword();
|
||||
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
||||
int alignment = m_doc->tree.computeStructAlignment(n.id);
|
||||
row2 = QStringLiteral("%1 %2 alignas(%3)")
|
||||
.arg(keyword, className).arg(alignment);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (row2.isEmpty())
|
||||
row2 = QStringLiteral("struct <no class> alignas(1)");
|
||||
|
||||
for (auto* ed : m_editors) {
|
||||
ed->setCommandRowText(row);
|
||||
ed->setCommandRow2Text(row2);
|
||||
}
|
||||
emit selectionChanged(m_selIds.size());
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ private:
|
||||
void connectEditor(RcxEditor* editor);
|
||||
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
|
||||
void updateCommandRow();
|
||||
void performRealignment(uint64_t structId, int targetAlign);
|
||||
void attachToProcess(uint32_t pid, const QString& processName);
|
||||
void switchToSavedSource(int idx);
|
||||
void pushSavedSourcesToEditors();
|
||||
|
||||
70
src/core.h
70
src/core.h
@@ -122,6 +122,9 @@ inline constexpr uint32_t flagsFor(NodeKind k) {
|
||||
inline constexpr bool isHexPreview(NodeKind k) {
|
||||
return flagsFor(k) & KF_HexPreview;
|
||||
}
|
||||
inline constexpr bool isHexNode(NodeKind k) {
|
||||
return k >= NodeKind::Hex8 && k <= NodeKind::Hex64;
|
||||
}
|
||||
|
||||
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
||||
QStringList out;
|
||||
@@ -157,6 +160,7 @@ struct Node {
|
||||
NodeKind kind = NodeKind::Hex8;
|
||||
QString name;
|
||||
QString structTypeName; // Struct/Array: optional type name (e.g., "IMAGE_DOS_HEADER")
|
||||
QString classKeyword; // "struct", "class", or "enum" (empty = "struct")
|
||||
uint64_t parentId = 0; // 0 = root (no parent)
|
||||
int offset = 0;
|
||||
int arrayLen = 1; // Array: element count
|
||||
@@ -184,6 +188,8 @@ struct Node {
|
||||
o["name"] = name;
|
||||
if (!structTypeName.isEmpty())
|
||||
o["structTypeName"] = structTypeName;
|
||||
if (!classKeyword.isEmpty() && classKeyword != QStringLiteral("struct"))
|
||||
o["classKeyword"] = classKeyword;
|
||||
o["parentId"] = QString::number(parentId);
|
||||
o["offset"] = offset;
|
||||
o["arrayLen"] = arrayLen;
|
||||
@@ -199,6 +205,7 @@ struct Node {
|
||||
n.kind = kindFromString(o["kind"].toString());
|
||||
n.name = o["name"].toString();
|
||||
n.structTypeName = o["structTypeName"].toString();
|
||||
n.classKeyword = o["classKeyword"].toString();
|
||||
n.parentId = o["parentId"].toString("0").toULongLong();
|
||||
n.offset = o["offset"].toInt(0);
|
||||
n.arrayLen = o["arrayLen"].toInt(1);
|
||||
@@ -209,6 +216,11 @@ struct Node {
|
||||
return n;
|
||||
}
|
||||
|
||||
// Resolved class keyword (never empty)
|
||||
QString resolvedClassKeyword() const {
|
||||
return classKeyword.isEmpty() ? QStringLiteral("struct") : classKeyword;
|
||||
}
|
||||
|
||||
// Helper: is this a string-like array (char[] or wchar_t[])?
|
||||
bool isStringArray() const {
|
||||
return kind == NodeKind::Array &&
|
||||
@@ -339,6 +351,9 @@ struct NodeTree {
|
||||
return qMax(declaredSize, maxEnd);
|
||||
}
|
||||
|
||||
// Compute natural alignment of a struct (max alignment of direct children)
|
||||
int computeStructAlignment(uint64_t structId) const;
|
||||
|
||||
// Batch selection normalizers
|
||||
QSet<uint64_t> normalizePreferAncestors(const QSet<uint64_t>& ids) const;
|
||||
QSet<uint64_t> normalizePreferDescendants(const QSet<uint64_t>& ids) const;
|
||||
@@ -371,13 +386,16 @@ struct NodeTree {
|
||||
// ── LineMeta ──
|
||||
|
||||
enum class LineKind : uint8_t {
|
||||
CommandRow, // line 0 only, synthetic UI
|
||||
CommandRow, // line 0: source + address
|
||||
CommandRow2, // line 1: root class type + name
|
||||
Header, Field, Continuation, Footer, ArrayElementSeparator
|
||||
};
|
||||
|
||||
static constexpr uint64_t kCommandRowId = UINT64_MAX;
|
||||
static constexpr uint64_t kCommandRow2Id = UINT64_MAX - 1;
|
||||
static constexpr int kCommandRowLine = 0;
|
||||
static constexpr int kFirstDataLine = 1;
|
||||
static constexpr int kCommandRow2Line = 1;
|
||||
static constexpr int kFirstDataLine = 2;
|
||||
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
|
||||
|
||||
struct LineMeta {
|
||||
@@ -400,13 +418,15 @@ struct LineMeta {
|
||||
QString offsetText;
|
||||
uint32_t markerMask = 0;
|
||||
bool dataChanged = false; // true if any byte in this node changed since last refresh
|
||||
QVector<int> changedByteIndices; // Hex preview: which byte indices (0-based) changed on this line
|
||||
int lineByteCount = 0; // Hex preview: actual data byte count on this line
|
||||
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
||||
int effectiveNameW = 22; // Per-line name column width used for rendering
|
||||
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
|
||||
};
|
||||
|
||||
inline bool isSyntheticLine(const LineMeta& lm) {
|
||||
return lm.lineKind == LineKind::CommandRow;
|
||||
return lm.lineKind == LineKind::CommandRow || lm.lineKind == LineKind::CommandRow2;
|
||||
}
|
||||
|
||||
// ── Layout Info ──
|
||||
@@ -443,12 +463,15 @@ namespace cmd {
|
||||
struct ChangePointerRef { uint64_t nodeId;
|
||||
uint64_t oldRefId, newRefId; };
|
||||
struct ChangeStructTypeName { uint64_t nodeId; QString oldName, newName; };
|
||||
struct ChangeClassKeyword { uint64_t nodeId; QString oldKeyword, newKeyword; };
|
||||
struct ChangeOffset { uint64_t nodeId; int oldOffset, newOffset; };
|
||||
}
|
||||
|
||||
using Command = std::variant<
|
||||
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
|
||||
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
|
||||
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName
|
||||
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
|
||||
cmd::ChangeClassKeyword, cmd::ChangeOffset
|
||||
>;
|
||||
|
||||
// ── Column spans (for inline editing) ──
|
||||
@@ -460,7 +483,8 @@ struct ColumnSpan {
|
||||
};
|
||||
|
||||
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount,
|
||||
ArrayElementType, ArrayElementCount, PointerTarget };
|
||||
ArrayElementType, ArrayElementCount, PointerTarget,
|
||||
RootClassType, RootClassName, Alignas };
|
||||
|
||||
// Column layout constants (shared with format.cpp span computation)
|
||||
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
|
||||
@@ -562,6 +586,42 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
|
||||
return {start, end, true};
|
||||
}
|
||||
|
||||
// ── CommandRow2 spans ──
|
||||
// Line format: "struct ClassName alignas(8)"
|
||||
|
||||
inline ColumnSpan commandRow2TypeSpan(const QString& lineText) {
|
||||
int start = 0;
|
||||
while (start < lineText.size() && lineText[start].isSpace()) start++;
|
||||
if (start >= lineText.size()) return {};
|
||||
int end = lineText.indexOf(' ', start);
|
||||
if (end <= start) return {start, (int)lineText.size(), true};
|
||||
return {start, end, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRow2NameSpan(const QString& lineText) {
|
||||
int start = 0;
|
||||
while (start < lineText.size() && lineText[start].isSpace()) start++;
|
||||
int space = lineText.indexOf(' ', start);
|
||||
if (space < 0) return {};
|
||||
int nameStart = space + 1;
|
||||
while (nameStart < lineText.size() && lineText[nameStart].isSpace()) nameStart++;
|
||||
if (nameStart >= lineText.size()) return {};
|
||||
// Name ends before "alignas(" if present, otherwise at line end
|
||||
int nameEnd = lineText.indexOf(QStringLiteral(" alignas("), nameStart);
|
||||
if (nameEnd < 0) nameEnd = lineText.size();
|
||||
while (nameEnd > nameStart && lineText[nameEnd - 1].isSpace()) nameEnd--;
|
||||
if (nameEnd <= nameStart) return {};
|
||||
return {nameStart, nameEnd, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRow2AlignasSpan(const QString& lineText) {
|
||||
int idx = lineText.indexOf(QStringLiteral("alignas("));
|
||||
if (idx < 0) return {};
|
||||
int end = lineText.indexOf(')', idx);
|
||||
if (end < 0) return {};
|
||||
return {idx, end + 1, true};
|
||||
}
|
||||
|
||||
// ── Array element type/count spans (within type column of array headers) ──
|
||||
// Line format: " int32_t[10] name {"
|
||||
// arrayElemTypeSpan covers "int32_t", arrayElemCountSpan covers "10"
|
||||
|
||||
372
src/editor.cpp
372
src/editor.cpp
@@ -80,7 +80,9 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
if (!m_editState.active) return;
|
||||
if (id == 1 && (m_editState.target == EditTarget::Type
|
||||
|| m_editState.target == EditTarget::ArrayElementType
|
||||
|| m_editState.target == EditTarget::PointerTarget)) {
|
||||
|| m_editState.target == EditTarget::PointerTarget
|
||||
|| m_editState.target == EditTarget::RootClassType
|
||||
|| m_editState.target == EditTarget::Alignas)) {
|
||||
auto info = endInlineEdit();
|
||||
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text);
|
||||
}
|
||||
@@ -105,8 +107,6 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
}
|
||||
|
||||
RcxEditor::~RcxEditor() {
|
||||
if (m_cursorOverridden)
|
||||
QApplication::restoreOverrideCursor();
|
||||
}
|
||||
|
||||
void RcxEditor::setupScintilla() {
|
||||
@@ -137,7 +137,7 @@ void RcxEditor::setupScintilla() {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0);
|
||||
|
||||
// Editable-field indicator - set to HIDDEN (no visual, avoids INDIC_PLAIN underline)
|
||||
// Editable-field indicator - HIDDEN (no visual)
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_EDITABLE, 5 /*INDIC_HIDDEN*/);
|
||||
|
||||
@@ -169,11 +169,11 @@ void RcxEditor::setupScintilla() {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
||||
IND_CMD_PILL, (long)1);
|
||||
|
||||
// Data-changed indicator — amber text for values that changed since last refresh
|
||||
// Data-changed indicator — muted green text (derived from number green #b5cea8)
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_DATA_CHANGED, 17 /*INDIC_TEXTFORE*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_DATA_CHANGED, QColor("#E5A00D"));
|
||||
IND_DATA_CHANGED, QColor("#8fbc7a"));
|
||||
|
||||
}
|
||||
|
||||
@@ -359,7 +359,7 @@ void RcxEditor::applyMarkers(const QVector<LineMeta>& meta) {
|
||||
}
|
||||
m_sci->markerDeleteAll(M_CMD_ROW);
|
||||
for (int i = 0; i < meta.size(); i++) {
|
||||
if (meta[i].lineKind == LineKind::CommandRow) {
|
||||
if (meta[i].lineKind == LineKind::CommandRow || meta[i].lineKind == LineKind::CommandRow2) {
|
||||
m_sci->markerAdd(i, M_CMD_ROW);
|
||||
continue;
|
||||
}
|
||||
@@ -557,12 +557,32 @@ void RcxEditor::applyDataChangedHighlight(const QVector<LineMeta>& meta) {
|
||||
if (!meta[i].dataChanged) continue;
|
||||
if (isSyntheticLine(meta[i])) continue;
|
||||
|
||||
QString lineText = getLineText(m_sci, i);
|
||||
int typeW = meta[i].effectiveTypeW;
|
||||
int nameW = meta[i].effectiveNameW;
|
||||
ColumnSpan vs = valueSpan(meta[i], lineText.size(), typeW, nameW);
|
||||
if (vs.valid)
|
||||
fillIndicatorCols(IND_DATA_CHANGED, i, vs.start, vs.end);
|
||||
const LineMeta& lm = meta[i];
|
||||
int typeW = lm.effectiveTypeW;
|
||||
int nameW = lm.effectiveNameW;
|
||||
|
||||
if (isHexPreview(lm.nodeKind) && !lm.changedByteIndices.isEmpty()) {
|
||||
// Per-byte highlighting in ASCII + hex areas
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int asciiStart = ind + typeW + kSepWidth;
|
||||
// Hex8-64: ASCII always padded to 8; Padding: ASCII = lineByteCount chars
|
||||
int asciiWidth = (lm.nodeKind == NodeKind::Padding) ? lm.lineByteCount : 8;
|
||||
int hexStart = asciiStart + asciiWidth + kSepWidth;
|
||||
|
||||
for (int byteIdx : lm.changedByteIndices) {
|
||||
// Highlight in ASCII area (1 char per byte)
|
||||
fillIndicatorCols(IND_DATA_CHANGED, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
|
||||
// Highlight in hex area (2 hex chars per byte at position byteIdx*3)
|
||||
int hexCol = hexStart + byteIdx * 3;
|
||||
fillIndicatorCols(IND_DATA_CHANGED, i, hexCol, hexCol + 2);
|
||||
}
|
||||
} else {
|
||||
// Non-hex nodes: highlight entire value span
|
||||
QString lineText = getLineText(m_sci, i);
|
||||
ColumnSpan vs = valueSpan(lm, lineText.size(), typeW, nameW);
|
||||
if (vs.valid)
|
||||
fillIndicatorCols(IND_DATA_CHANGED, i, vs.start, vs.end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,6 +603,7 @@ void RcxEditor::applyCommandRowPills() {
|
||||
QString t = getLineText(m_sci, line);
|
||||
|
||||
clearIndicatorLine(IND_CMD_PILL, line);
|
||||
clearIndicatorLine(IND_HEX_DIM, line);
|
||||
|
||||
auto fillPadded = [&](ColumnSpan s) {
|
||||
if (!s.valid) return;
|
||||
@@ -593,6 +614,48 @@ void RcxEditor::applyCommandRowPills() {
|
||||
|
||||
fillPadded(commandRowSrcSpan(t));
|
||||
fillPadded(commandRowAddrSpan(t));
|
||||
|
||||
// Dim label text: provider kind ("File"/"Process") and "Address:"
|
||||
ColumnSpan srcSpan = commandRowSrcSpan(t);
|
||||
if (srcSpan.valid) {
|
||||
int quotePos = t.indexOf('\'', srcSpan.start);
|
||||
int kindEnd = (quotePos > srcSpan.start) ? quotePos : srcSpan.end;
|
||||
while (kindEnd > srcSpan.start && t[kindEnd - 1].isSpace()) kindEnd--;
|
||||
if (kindEnd > srcSpan.start)
|
||||
fillIndicatorCols(IND_HEX_DIM, line, srcSpan.start, kindEnd);
|
||||
}
|
||||
int addrTag = t.indexOf(QStringLiteral(" Address: "));
|
||||
if (addrTag >= 0)
|
||||
fillIndicatorCols(IND_HEX_DIM, line, addrTag + 1, addrTag + 9);
|
||||
|
||||
// Style CommandRow2 (line 1) if present
|
||||
if (m_meta.size() > 1 && m_meta[1].lineKind == LineKind::CommandRow2) {
|
||||
constexpr int line2 = 1;
|
||||
QString t2 = getLineText(m_sci, line2);
|
||||
|
||||
clearIndicatorLine(IND_CMD_PILL, line2);
|
||||
clearIndicatorLine(IND_HEX_DIM, line2);
|
||||
|
||||
auto fillPadded2 = [&](ColumnSpan s) {
|
||||
if (!s.valid) return;
|
||||
int a = qMax(0, s.start - 1);
|
||||
int b = qMin(t2.size(), s.end + 1);
|
||||
fillIndicatorCols(IND_CMD_PILL, line2, a, b);
|
||||
};
|
||||
|
||||
ColumnSpan typeSpan = commandRow2TypeSpan(t2);
|
||||
fillPadded2(typeSpan);
|
||||
if (typeSpan.valid)
|
||||
fillIndicatorCols(IND_HEX_DIM, line2, typeSpan.start, typeSpan.end);
|
||||
|
||||
ColumnSpan nameSpan = commandRow2NameSpan(t2);
|
||||
fillPadded2(nameSpan);
|
||||
|
||||
ColumnSpan alignasSpan = commandRow2AlignasSpan(t2);
|
||||
fillPadded2(alignasSpan);
|
||||
if (alignasSpan.valid)
|
||||
fillIndicatorCols(IND_HEX_DIM, line2, alignasSpan.start, alignasSpan.end);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared inline-edit shutdown ──
|
||||
@@ -607,13 +670,8 @@ RcxEditor::EndEditInfo RcxEditor::endInlineEdit() {
|
||||
m_editState.active = false;
|
||||
m_sci->setReadOnly(true);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 0);
|
||||
// Switch from I-beam to Arrow (keep override active to block Scintilla's cursor)
|
||||
if (m_cursorOverridden) {
|
||||
QApplication::changeOverrideCursor(Qt::ArrowCursor);
|
||||
} else {
|
||||
QApplication::setOverrideCursor(Qt::ArrowCursor);
|
||||
m_cursorOverridden = true;
|
||||
}
|
||||
// Switch back to Arrow cursor (widget-local, doesn't fight splitters/menus)
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
// Disable selection rendering again
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0);
|
||||
@@ -753,11 +811,28 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
||||
return out.valid;
|
||||
}
|
||||
|
||||
// CommandRow2: root class type, name, and alignas
|
||||
if (lm->lineKind == LineKind::CommandRow2) {
|
||||
if (t != EditTarget::RootClassType && t != EditTarget::RootClassName
|
||||
&& t != EditTarget::Alignas) return false;
|
||||
QString lineText = getLineText(m_sci, line);
|
||||
ColumnSpan s;
|
||||
if (t == EditTarget::RootClassType) s = commandRow2TypeSpan(lineText);
|
||||
else if (t == EditTarget::RootClassName) s = commandRow2NameSpan(lineText);
|
||||
else s = commandRow2AlignasSpan(lineText);
|
||||
out = normalizeSpan(s, lineText, t, false);
|
||||
if (lineTextOut) *lineTextOut = lineText;
|
||||
return out.valid;
|
||||
}
|
||||
|
||||
if (lm->nodeIdx < 0) return false;
|
||||
|
||||
// Padding: reject value editing (hex bytes are display-only)
|
||||
if (t == EditTarget::Value && lm->nodeKind == NodeKind::Padding)
|
||||
return false;
|
||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||
if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind))
|
||||
return false;
|
||||
|
||||
QString lineText = getLineText(m_sci, line);
|
||||
int textLen = lineText.size();
|
||||
@@ -868,6 +943,17 @@ static bool hitTestTarget(QsciScintilla* sci,
|
||||
return false;
|
||||
}
|
||||
|
||||
// CommandRow2: root class type, name, and alignas
|
||||
if (lm.lineKind == LineKind::CommandRow2) {
|
||||
ColumnSpan ts = commandRow2TypeSpan(lineText);
|
||||
if (inSpan(ts)) { outTarget = EditTarget::RootClassType; outLine = line; return true; }
|
||||
ColumnSpan ns = commandRow2NameSpan(lineText);
|
||||
if (inSpan(ns)) { outTarget = EditTarget::RootClassName; outLine = line; return true; }
|
||||
ColumnSpan as = commandRow2AlignasSpan(lineText);
|
||||
if (inSpan(as)) { outTarget = EditTarget::Alignas; outLine = line; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use per-line effective widths from LineMeta
|
||||
int typeW = lm.effectiveTypeW;
|
||||
int nameW = lm.effectiveNameW;
|
||||
@@ -909,6 +995,9 @@ static bool hitTestTarget(QsciScintilla* sci,
|
||||
// Padding nodes: hex bytes are display-only, not editable
|
||||
if (outTarget == EditTarget::Value && lm.nodeKind == NodeKind::Padding)
|
||||
return false;
|
||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||
if ((outTarget == EditTarget::Name || outTarget == EditTarget::Value) && isHexNode(lm.nodeKind))
|
||||
return false;
|
||||
|
||||
outLine = line;
|
||||
return true;
|
||||
@@ -995,7 +1084,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
return true;
|
||||
}
|
||||
// CommandRow: try ADDR edit or consume
|
||||
if (h.nodeId == kCommandRowId) {
|
||||
if (h.nodeId == kCommandRowId || h.nodeId == kCommandRow2Id) {
|
||||
int tLine; EditTarget t;
|
||||
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t))
|
||||
beginInlineEdit(t, tLine);
|
||||
@@ -1032,6 +1121,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
m_pendingClickNodeId = 0;
|
||||
}
|
||||
}
|
||||
return true; // consume ALL left-clicks (prevent QScintilla caret/cursor)
|
||||
}
|
||||
}
|
||||
// Drag-select: extend selection as mouse moves with button held
|
||||
@@ -1044,7 +1134,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
if (!m_dragStarted) {
|
||||
int dy = me->pos().y() - m_dragStartPos.y();
|
||||
if (qAbs(dy) < 8)
|
||||
return false; // not yet a drag, let Scintilla handle
|
||||
return true; // not yet a drag, but still consume (don't let Scintilla handle)
|
||||
m_dragStarted = true;
|
||||
}
|
||||
|
||||
@@ -1072,6 +1162,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
m_pendingClickMods);
|
||||
m_pendingClickNodeId = 0;
|
||||
}
|
||||
return true; // consume release (prevent QScintilla from acting on it)
|
||||
}
|
||||
// Double-click during edit mode: select entire editable text
|
||||
if (obj == m_sci->viewport() && m_editState.active
|
||||
@@ -1088,10 +1179,11 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
m_pendingClickNodeId = 0; // cancel deferred selection change
|
||||
// Narrow selection to this node before editing
|
||||
auto h = hitTest(me->pos());
|
||||
if (h.nodeId != 0 && h.nodeId != kCommandRowId)
|
||||
if (h.nodeId != 0 && h.nodeId != kCommandRowId && h.nodeId != kCommandRow2Id)
|
||||
emit nodeClicked(h.line, h.nodeId, Qt::NoModifier);
|
||||
return beginInlineEdit(t, line);
|
||||
}
|
||||
return true; // consume even on miss (prevent QScintilla word-select)
|
||||
}
|
||||
if (obj == m_sci && event->type() == QEvent::FocusOut) {
|
||||
auto* fe = static_cast<QFocusEvent*>(event);
|
||||
@@ -1113,22 +1205,25 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
m_sci->getCursorPosition(&line, &col);
|
||||
updateEditableIndicators(line);
|
||||
}
|
||||
if (obj == m_sci->viewport() && !m_editState.active) {
|
||||
// Track mouse position for cursor updates (both edit and non-edit mode)
|
||||
if (obj == m_sci->viewport()) {
|
||||
if (event->type() == QEvent::MouseMove) {
|
||||
m_lastHoverPos = static_cast<QMouseEvent*>(event)->pos();
|
||||
m_hoverInside = true;
|
||||
} else if (event->type() == QEvent::Leave) {
|
||||
m_hoverInside = false;
|
||||
m_hoveredNodeId = 0;
|
||||
m_hoveredLine = -1;
|
||||
applyHoverHighlight();
|
||||
if (!m_editState.active) {
|
||||
m_hoveredNodeId = 0;
|
||||
m_hoveredLine = -1;
|
||||
applyHoverHighlight();
|
||||
}
|
||||
} else if (event->type() == QEvent::Wheel) {
|
||||
m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos());
|
||||
m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos);
|
||||
}
|
||||
// Resolve hovered nodeId on move/wheel
|
||||
if (event->type() == QEvent::MouseMove
|
||||
|| event->type() == QEvent::Wheel) {
|
||||
// Resolve hovered nodeId on move/wheel (non-edit mode only)
|
||||
if (!m_editState.active &&
|
||||
(event->type() == QEvent::MouseMove || event->type() == QEvent::Wheel)) {
|
||||
auto h = hitTest(m_lastHoverPos);
|
||||
uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0;
|
||||
int newHoverLine = (m_hoverInside && h.line >= 0) ? h.line : -1;
|
||||
@@ -1138,10 +1233,16 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
applyHoverHighlight();
|
||||
}
|
||||
}
|
||||
// Update cursor on move/leave/wheel (both edit and non-edit mode)
|
||||
if (event->type() == QEvent::MouseMove
|
||||
|| event->type() == QEvent::Leave
|
||||
|| event->type() == QEvent::Wheel)
|
||||
applyHoverCursor();
|
||||
|
||||
// Consume MouseMove in non-edit mode so QScintilla's internal handler
|
||||
// doesn't override our cursor (it resets to Arrow for read-only widgets)
|
||||
if (!m_editState.active && event->type() == QEvent::MouseMove)
|
||||
return true;
|
||||
}
|
||||
return QWidget::eventFilter(obj, event);
|
||||
}
|
||||
@@ -1264,13 +1365,19 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
||||
m_sci->getCursorPosition(&line, &col);
|
||||
auto* lm = metaForLine(line);
|
||||
if (!lm) return false;
|
||||
// Allow nodeIdx=-1 only for CommandRow BaseAddress/Source editing
|
||||
// Allow nodeIdx=-1 only for CommandRow/CommandRow2 editing
|
||||
if (lm->nodeIdx < 0 && !(lm->lineKind == LineKind::CommandRow &&
|
||||
(target == EditTarget::BaseAddress || target == EditTarget::Source)))
|
||||
(target == EditTarget::BaseAddress || target == EditTarget::Source))
|
||||
&& !(lm->lineKind == LineKind::CommandRow2 &&
|
||||
(target == EditTarget::RootClassType || target == EditTarget::RootClassName
|
||||
|| target == EditTarget::Alignas)))
|
||||
return false;
|
||||
// Padding: reject value editing (display-only hex bytes)
|
||||
if (target == EditTarget::Value && lm->nodeKind == NodeKind::Padding)
|
||||
return false;
|
||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||
if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind))
|
||||
return false;
|
||||
|
||||
QString lineText;
|
||||
NormalizedSpan norm;
|
||||
@@ -1306,13 +1413,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
||||
m_sci->setReadOnly(false);
|
||||
// Switch to I-beam for editing (skip for picker-based targets)
|
||||
if (target != EditTarget::Type && target != EditTarget::Source
|
||||
&& target != EditTarget::ArrayElementType && target != EditTarget::PointerTarget) {
|
||||
if (m_cursorOverridden) {
|
||||
QApplication::changeOverrideCursor(Qt::IBeamCursor);
|
||||
} else {
|
||||
QApplication::setOverrideCursor(Qt::IBeamCursor);
|
||||
m_cursorOverridden = true;
|
||||
}
|
||||
&& target != EditTarget::ArrayElementType && target != EditTarget::PointerTarget
|
||||
&& target != EditTarget::RootClassType) {
|
||||
m_sci->viewport()->setCursor(Qt::IBeamCursor);
|
||||
}
|
||||
|
||||
// Re-enable selection rendering for inline edit
|
||||
@@ -1342,6 +1445,40 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
||||
QTimer::singleShot(0, this, &RcxEditor::showSourcePicker);
|
||||
if (target == EditTarget::PointerTarget)
|
||||
QTimer::singleShot(0, this, &RcxEditor::showPointerTargetPicker);
|
||||
if (target == EditTarget::RootClassType) {
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
if (!m_editState.active || m_editState.target != EditTarget::RootClassType) return;
|
||||
// Replace text with spaces and show picker
|
||||
int len = m_editState.original.size();
|
||||
QString spaces(len, ' ');
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
|
||||
m_editState.posStart, m_editState.posEnd);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
|
||||
(uintptr_t)0, spaces.toUtf8().constData());
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n');
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
|
||||
(uintptr_t)1, "struct\nclass\nenum");
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
});
|
||||
}
|
||||
if (target == EditTarget::Alignas) {
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
if (!m_editState.active || m_editState.target != EditTarget::Alignas) return;
|
||||
int len = m_editState.original.size();
|
||||
QString spaces(len, ' ');
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
|
||||
m_editState.posStart, m_editState.posEnd);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
|
||||
(uintptr_t)0, spaces.toUtf8().constData());
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n');
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
|
||||
(uintptr_t)1,
|
||||
"alignas(1)\nalignas(4)\nalignas(8)\nalignas(16)");
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1469,7 +1606,8 @@ void RcxEditor::showTypeListFiltered(const QString& filter) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n');
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
|
||||
(uintptr_t)1, list.constData());
|
||||
// Arrow cursor for popup is handled by applyHoverCursor() via isListActive()
|
||||
// Force Arrow cursor immediately (don't wait for mouse move)
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
}
|
||||
|
||||
void RcxEditor::showSourcePicker() {
|
||||
@@ -1574,6 +1712,8 @@ void RcxEditor::showPointerTargetListFiltered(const QString& filter) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n');
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
|
||||
(uintptr_t)1, list.constData());
|
||||
// Force Arrow cursor immediately (don't wait for mouse move)
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
}
|
||||
|
||||
void RcxEditor::updatePointerTargetFilter() {
|
||||
@@ -1599,7 +1739,7 @@ void RcxEditor::paintEditableSpans(int line) {
|
||||
const LineMeta* lm = metaForLine(line);
|
||||
if (!lm) return;
|
||||
// CommandRow: paint Source and BaseAddress spans
|
||||
if (isSyntheticLine(*lm)) {
|
||||
if (lm->lineKind == LineKind::CommandRow) {
|
||||
NormalizedSpan norm;
|
||||
if (resolvedSpanFor(line, EditTarget::Source, norm))
|
||||
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
||||
@@ -1607,6 +1747,18 @@ void RcxEditor::paintEditableSpans(int line) {
|
||||
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
||||
return;
|
||||
}
|
||||
// CommandRow2: paint RootClassType, RootClassName, and Alignas spans
|
||||
if (lm->lineKind == LineKind::CommandRow2) {
|
||||
NormalizedSpan norm;
|
||||
if (resolvedSpanFor(line, EditTarget::RootClassType, norm))
|
||||
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
||||
if (resolvedSpanFor(line, EditTarget::RootClassName, norm))
|
||||
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
||||
if (resolvedSpanFor(line, EditTarget::Alignas, norm))
|
||||
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
|
||||
return;
|
||||
}
|
||||
if (isSyntheticLine(*lm)) return;
|
||||
NormalizedSpan norm;
|
||||
for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value,
|
||||
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
|
||||
@@ -1669,29 +1821,37 @@ void RcxEditor::applyHoverCursor() {
|
||||
clearIndicatorLine(IND_HOVER_SPAN, ln);
|
||||
m_hoverSpanLines.clear();
|
||||
|
||||
// Edit mode handles its own cursor (I-beam)
|
||||
if (m_editState.active)
|
||||
// Lock cursor to Arrow during drag-selection (prevents flicker)
|
||||
if (m_dragStarted) {
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Edit mode: IBeam inside edit span, Arrow outside
|
||||
if (m_editState.active) {
|
||||
if (m_sci->isListActive()) {
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
auto h = hitTest(m_lastHoverPos);
|
||||
if (h.line == m_editState.line &&
|
||||
h.col >= m_editState.spanStart && h.col <= editEndCol()) {
|
||||
m_sci->viewport()->setCursor(Qt::IBeamCursor);
|
||||
} else {
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Mouse left viewport - set Arrow
|
||||
if (!m_hoverInside || !m_sci->viewport()->underMouse()) {
|
||||
if (!m_cursorOverridden) {
|
||||
QApplication::setOverrideCursor(Qt::ArrowCursor);
|
||||
m_cursorOverridden = true;
|
||||
} else {
|
||||
QApplication::changeOverrideCursor(Qt::ArrowCursor);
|
||||
}
|
||||
if (!m_hoverInside) {
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
|
||||
// If autocomplete/user list popup is active, use arrow cursor
|
||||
if (m_sci->isListActive()) {
|
||||
if (!m_cursorOverridden) {
|
||||
QApplication::setOverrideCursor(Qt::ArrowCursor);
|
||||
m_cursorOverridden = true;
|
||||
} else {
|
||||
QApplication::changeOverrideCursor(Qt::ArrowCursor);
|
||||
}
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1699,34 +1859,13 @@ void RcxEditor::applyHoverCursor() {
|
||||
int line; EditTarget t;
|
||||
bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t);
|
||||
|
||||
// For hex preview nodes, check if cursor is in the data area (ASCII or hex bytes)
|
||||
// Skip hover span on footer lines (nothing editable)
|
||||
int hoverLine = h.line;
|
||||
bool inHexDataArea = false;
|
||||
uint64_t hoverNodeId = 0;
|
||||
if (hoverLine >= 0 && hoverLine < m_meta.size()
|
||||
&& isHexPreview(m_meta[hoverLine].nodeKind)) {
|
||||
hoverNodeId = m_meta[hoverLine].nodeId;
|
||||
if (hoverNodeId != 0 && h.col >= 0) {
|
||||
int ind = kFoldCol + m_meta[hoverLine].depth * 3;
|
||||
int typeW = m_meta[hoverLine].effectiveTypeW;
|
||||
int dataStart = ind + typeW + kSepWidth;
|
||||
inHexDataArea = (h.col >= dataStart);
|
||||
}
|
||||
}
|
||||
bool isFooterLine = (hoverLine >= 0 && hoverLine < m_meta.size()
|
||||
&& m_meta[hoverLine].lineKind == LineKind::Footer);
|
||||
|
||||
// Apply hover span indicator
|
||||
if (inHexDataArea) {
|
||||
// Hex preview nodes: highlight ASCII + hex byte areas on ALL lines of this node
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
if (m_meta[i].nodeId != hoverNodeId) continue;
|
||||
int ind = kFoldCol + m_meta[i].depth * 3;
|
||||
int typeW = m_meta[i].effectiveTypeW;
|
||||
int asciiStart = ind + typeW + kSepWidth;
|
||||
int hexEnd = asciiStart + 8 + kSepWidth + 23;
|
||||
fillIndicatorCols(IND_HOVER_SPAN, i, asciiStart, hexEnd);
|
||||
m_hoverSpanLines.append(i);
|
||||
}
|
||||
} else if (tokenHit) {
|
||||
// Apply hover span indicator for editable tokens
|
||||
if (tokenHit && !isFooterLine) {
|
||||
NormalizedSpan span;
|
||||
if (resolvedSpanFor(line, t, span)) {
|
||||
fillIndicatorCols(IND_HOVER_SPAN, line, span.start, span.end);
|
||||
@@ -1734,20 +1873,35 @@ void RcxEditor::applyHoverCursor() {
|
||||
}
|
||||
}
|
||||
|
||||
// Also show pointer cursor for fold column on fold-head lines
|
||||
bool interactive = tokenHit || inHexDataArea;
|
||||
if (!interactive) {
|
||||
if (h.inFoldCol) interactive = true;
|
||||
// Determine cursor shape based on interaction type
|
||||
Qt::CursorShape desired = Qt::ArrowCursor;
|
||||
|
||||
if (h.inFoldCol) {
|
||||
desired = Qt::PointingHandCursor; // fold toggle = button
|
||||
} else if (tokenHit) {
|
||||
// Check if mouse is actually over trimmed text content (not column padding)
|
||||
NormalizedSpan trimmed;
|
||||
bool overText = resolvedSpanFor(line, t, trimmed)
|
||||
&& h.col >= trimmed.start && h.col < trimmed.end;
|
||||
if (overText) {
|
||||
switch (t) {
|
||||
case EditTarget::Type:
|
||||
case EditTarget::Source:
|
||||
case EditTarget::ArrayElementType:
|
||||
case EditTarget::PointerTarget:
|
||||
case EditTarget::RootClassType:
|
||||
case EditTarget::Alignas:
|
||||
desired = Qt::PointingHandCursor;
|
||||
break;
|
||||
default:
|
||||
desired = Qt::IBeamCursor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// else: desired stays Arrow (hovering over column padding)
|
||||
}
|
||||
|
||||
// Set cursor: pointing hand for interactive, arrow otherwise
|
||||
Qt::CursorShape desired = interactive ? Qt::PointingHandCursor : Qt::ArrowCursor;
|
||||
if (!m_cursorOverridden) {
|
||||
QApplication::setOverrideCursor(desired);
|
||||
m_cursorOverridden = true;
|
||||
} else {
|
||||
QApplication::changeOverrideCursor(desired);
|
||||
}
|
||||
m_sci->viewport()->setCursor(desired);
|
||||
}
|
||||
|
||||
// ── Live value validation ──
|
||||
@@ -1851,6 +2005,36 @@ void RcxEditor::setCommandRowText(const QString& line) {
|
||||
applyCommandRowPills();
|
||||
}
|
||||
|
||||
void RcxEditor::setCommandRow2Text(const QString& line) {
|
||||
if (m_sci->lines() <= 1) return;
|
||||
QString s = line;
|
||||
s.replace('\n', ' ');
|
||||
s.replace('\r', ' ');
|
||||
|
||||
bool wasReadOnly = m_sci->isReadOnly();
|
||||
bool wasModified = m_sci->SendScintilla(QsciScintillaBase::SCI_GETMODIFY);
|
||||
long savedPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
|
||||
long savedAnchor = m_sci->SendScintilla(QsciScintillaBase::SCI_GETANCHOR);
|
||||
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 0);
|
||||
m_sci->setReadOnly(false);
|
||||
|
||||
long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 1);
|
||||
long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 1);
|
||||
QByteArray utf8 = s.toUtf8();
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, start);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, end);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData());
|
||||
|
||||
if (wasReadOnly) m_sci->setReadOnly(true);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 1);
|
||||
if (!wasModified) m_sci->SendScintilla(QsciScintillaBase::SCI_SETSAVEPOINT);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCURRENTPOS, savedPos);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETANCHOR, savedAnchor);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_COLOURISE, start, start + utf8.size());
|
||||
applyCommandRowPills();
|
||||
}
|
||||
|
||||
void RcxEditor::setEditorFont(const QString& fontName) {
|
||||
g_fontName = fontName;
|
||||
QFont f = editorFont();
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
|
||||
void applySelectionOverlay(const QSet<uint64_t>& selIds);
|
||||
void setCommandRowText(const QString& line);
|
||||
void setCommandRow2Text(const QString& line);
|
||||
void setEditorFont(const QString& fontName);
|
||||
static void setGlobalFontName(const QString& fontName);
|
||||
|
||||
@@ -76,7 +77,6 @@ private:
|
||||
// ── Hover cursor + highlight ──
|
||||
QPoint m_lastHoverPos;
|
||||
bool m_hoverInside = false;
|
||||
bool m_cursorOverridden = false;
|
||||
uint64_t m_hoveredNodeId = 0;
|
||||
int m_hoveredLine = -1;
|
||||
QSet<uint64_t> m_currentSelIds;
|
||||
|
||||
@@ -261,7 +261,9 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
if (refIdx >= 0 && !ctx.emittedIds.contains(child.refId)
|
||||
&& !ctx.forwardDeclared.contains(child.refId)) {
|
||||
QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]);
|
||||
ctx.output += QStringLiteral("struct %1;\n").arg(fwdName);
|
||||
QString fwdKw = ctx.tree.nodes[refIdx].resolvedClassKeyword();
|
||||
if (fwdKw == QStringLiteral("enum")) fwdKw = QStringLiteral("struct");
|
||||
ctx.output += QStringLiteral("%1 %2;\n").arg(fwdKw, fwdName);
|
||||
ctx.forwardDeclared.insert(child.refId);
|
||||
}
|
||||
}
|
||||
@@ -273,7 +275,9 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
||||
|
||||
ctx.output += QStringLiteral("#pragma pack(push, 1)\n");
|
||||
ctx.output += QStringLiteral("struct %1 {\n").arg(typeName);
|
||||
QString kw = node.resolvedClassKeyword();
|
||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic
|
||||
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
|
||||
|
||||
emitStructBody(ctx, structId);
|
||||
|
||||
|
||||
37
src/main.cpp
37
src/main.cpp
@@ -107,23 +107,16 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
||||
#include <atomic>
|
||||
#include <random>
|
||||
|
||||
struct TestLiveData {
|
||||
int32_t valA = 100;
|
||||
int32_t valB = 200;
|
||||
int32_t valC = 300;
|
||||
int32_t valD = 400;
|
||||
};
|
||||
|
||||
static TestLiveData* g_testData = nullptr;
|
||||
static uint8_t* g_testData = nullptr;
|
||||
static constexpr int kTestDataSize = 128;
|
||||
static std::atomic<bool> g_testRunning{false};
|
||||
|
||||
static void testLiveThread() {
|
||||
std::mt19937 rng(42);
|
||||
std::uniform_int_distribution<int> dist(0, 3);
|
||||
std::uniform_int_distribution<int> dist(0, kTestDataSize - 1);
|
||||
while (g_testRunning.load()) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
int32_t* fields = &g_testData->valA;
|
||||
fields[dist(rng)]++;
|
||||
g_testData[dist(rng)]++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,8 +404,8 @@ void MainWindow::newFile() {
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
#ifdef _WIN32
|
||||
// Allocate test struct — lives until process exit
|
||||
g_testData = new TestLiveData();
|
||||
// Allocate 128 bytes — lives until process exit
|
||||
g_testData = new uint8_t[kTestDataSize]();
|
||||
g_testRunning = true;
|
||||
std::thread(testLiveThread).detach();
|
||||
|
||||
@@ -424,22 +417,26 @@ void MainWindow::selfTest() {
|
||||
| PROCESS_QUERY_INFORMATION,
|
||||
FALSE, GetCurrentProcessId());
|
||||
doc->provider = std::make_shared<ProcessProvider>(
|
||||
hProc, base, (int)sizeof(TestLiveData), "ReclassX.exe");
|
||||
hProc, base, kTestDataSize, "ReclassX.exe");
|
||||
doc->tree.baseAddress = base;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "TestLiveData";
|
||||
root.structTypeName = "TestLiveData";
|
||||
root.name = "MyClass";
|
||||
root.structTypeName = "MyClass";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = doc->tree.addNode(root);
|
||||
uint64_t rootId = doc->tree.nodes[ri].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "valA"; n.parentId = rootId; n.offset = 0; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "valB"; n.parentId = rootId; n.offset = 4; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "valC"; n.parentId = rootId; n.offset = 8; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "valD"; n.parentId = rootId; n.offset = 12; doc->tree.addNode(n); }
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Hex64;
|
||||
n.name = QStringLiteral("field_%1").arg(i);
|
||||
n.parentId = rootId;
|
||||
n.offset = i * 8;
|
||||
doc->tree.addNode(n);
|
||||
}
|
||||
|
||||
createTab(doc);
|
||||
#endif
|
||||
|
||||
@@ -35,30 +35,22 @@ private slots:
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + Header + 2 fields + footer = 5 lines
|
||||
QCOMPARE(result.meta.size(), 5);
|
||||
// CommandRow + CommandRow2 + 2 fields = 4 lines (root header/footer suppressed)
|
||||
QCOMPARE(result.meta.size(), 4);
|
||||
|
||||
// Line 0 is CommandRow
|
||||
QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow);
|
||||
|
||||
// Header is fold head
|
||||
QVERIFY(result.meta[1].foldHead);
|
||||
QCOMPARE(result.meta[1].lineKind, LineKind::Header);
|
||||
// Line 1 is CommandRow2
|
||||
QCOMPARE(result.meta[1].lineKind, LineKind::CommandRow2);
|
||||
|
||||
// Fields are not fold heads
|
||||
// Fields at depth 0 (root struct suppressed)
|
||||
QVERIFY(!result.meta[2].foldHead);
|
||||
QVERIFY(!result.meta[3].foldHead);
|
||||
|
||||
// Footer
|
||||
QCOMPARE(result.meta[4].lineKind, LineKind::Footer);
|
||||
|
||||
// Offset text
|
||||
QCOMPARE(result.meta[1].offsetText, QString("0"));
|
||||
QCOMPARE(result.meta[2].offsetText, QString("0"));
|
||||
QCOMPARE(result.meta[3].offsetText, QString("4"));
|
||||
|
||||
// Header is expanded by default (fold indicator in line text)
|
||||
QVERIFY(!result.meta[1].foldCollapsed);
|
||||
}
|
||||
|
||||
void testVec3Continuation() {
|
||||
@@ -82,8 +74,8 @@ private slots:
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + Header + 3 Vec3 lines + footer = 6 lines
|
||||
QCOMPARE(result.meta.size(), 6);
|
||||
// CommandRow + CommandRow2 + 3 Vec3 lines = 5 lines (root header/footer suppressed)
|
||||
QCOMPARE(result.meta.size(), 5);
|
||||
|
||||
// Line 2 (first Vec3 component): not continuation
|
||||
QVERIFY(!result.meta[2].isContinuation);
|
||||
@@ -121,8 +113,8 @@ private slots:
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + Header + padding + footer = 4
|
||||
QCOMPARE(result.meta.size(), 4);
|
||||
// CommandRow + CommandRow2 + padding = 3 (root header/footer suppressed)
|
||||
QCOMPARE(result.meta.size(), 3);
|
||||
QVERIFY(result.meta[2].markerMask & (1u << M_PAD));
|
||||
}
|
||||
|
||||
@@ -149,7 +141,8 @@ private slots:
|
||||
BufferProvider prov(data);
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
QCOMPARE(result.meta.size(), 4);
|
||||
// CommandRow + CommandRow2 + ptr = 3 (root header/footer suppressed)
|
||||
QCOMPARE(result.meta.size(), 3);
|
||||
// No ambient validation markers — M_PTR0 is no longer set
|
||||
QVERIFY(!(result.meta[2].markerMask & (1u << M_PTR0)));
|
||||
}
|
||||
@@ -176,10 +169,10 @@ private slots:
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// Collapsed: CommandRow + header only (no children, no footer)
|
||||
QCOMPARE(result.meta.size(), 2);
|
||||
QVERIFY(result.meta[1].foldHead);
|
||||
QVERIFY(result.meta[1].foldCollapsed);
|
||||
// Collapsed: CommandRow + CommandRow2 + header only (no children, no footer)
|
||||
QCOMPARE(result.meta.size(), 3);
|
||||
QVERIFY(!result.meta[2].foldHead); // root fold suppressed
|
||||
QVERIFY(!result.meta[2].foldCollapsed); // root fold suppressed
|
||||
}
|
||||
|
||||
void testUnreadablePointerNoRead() {
|
||||
@@ -206,7 +199,8 @@ private slots:
|
||||
BufferProvider prov(data);
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
QCOMPARE(result.meta.size(), 4);
|
||||
// CommandRow + CommandRow2 + ptr = 3 (root header/footer suppressed)
|
||||
QCOMPARE(result.meta.size(), 3);
|
||||
// No ambient validation markers
|
||||
QVERIFY(!(result.meta[2].markerMask & (1u << M_ERR)));
|
||||
QVERIFY(!(result.meta[2].markerMask & (1u << M_PTR0)));
|
||||
@@ -241,17 +235,14 @@ private slots:
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// Root header (depth 0, head) -> 0x400 | 0x2000
|
||||
QCOMPARE(result.meta[1].foldLevel, 0x400 | 0x2000);
|
||||
QCOMPARE(result.meta[1].depth, 0);
|
||||
// Child header (depth 0, fold head) — root suppressed, children at depth 0
|
||||
QCOMPARE(result.meta[2].foldLevel, 0x400 | 0x2000);
|
||||
QCOMPARE(result.meta[2].depth, 0);
|
||||
QVERIFY(result.meta[2].foldHead);
|
||||
|
||||
// Child header (depth 1, head) -> 0x401 | 0x2000
|
||||
QCOMPARE(result.meta[2].foldLevel, 0x401 | 0x2000);
|
||||
QCOMPARE(result.meta[2].depth, 1);
|
||||
|
||||
// Leaf (depth 2, not head) -> 0x402
|
||||
QCOMPARE(result.meta[3].foldLevel, 0x402);
|
||||
QCOMPARE(result.meta[3].depth, 2);
|
||||
// Leaf (depth 1, not head)
|
||||
QCOMPARE(result.meta[3].foldLevel, 0x401);
|
||||
QCOMPARE(result.meta[3].depth, 1);
|
||||
}
|
||||
|
||||
void testNestedStruct() {
|
||||
@@ -298,36 +289,28 @@ private slots:
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + Outer header + flags + Inner header + x + y + Inner footer + Outer footer = 8
|
||||
QCOMPARE(result.meta.size(), 8);
|
||||
// CommandRow + CommandRow2 + flags + Inner header + x + y + Inner footer = 7
|
||||
// (root header/footer suppressed, children at depth 0)
|
||||
QCOMPARE(result.meta.size(), 7);
|
||||
|
||||
// Outer header
|
||||
QCOMPARE(result.meta[1].lineKind, LineKind::Header);
|
||||
QCOMPARE(result.meta[1].depth, 0);
|
||||
QVERIFY(result.meta[1].foldHead);
|
||||
|
||||
// flags field
|
||||
// flags field (depth 0, root children at depth 0)
|
||||
QCOMPARE(result.meta[2].lineKind, LineKind::Field);
|
||||
QCOMPARE(result.meta[2].depth, 1);
|
||||
QCOMPARE(result.meta[2].depth, 0);
|
||||
|
||||
// Inner header
|
||||
// Inner header (depth 0, fold head)
|
||||
QCOMPARE(result.meta[3].lineKind, LineKind::Header);
|
||||
QCOMPARE(result.meta[3].depth, 1);
|
||||
QCOMPARE(result.meta[3].depth, 0);
|
||||
QVERIFY(result.meta[3].foldHead);
|
||||
QCOMPARE(result.meta[3].foldLevel, 0x401 | 0x2000);
|
||||
QCOMPARE(result.meta[3].foldLevel, 0x400 | 0x2000);
|
||||
|
||||
// Inner fields at depth 2
|
||||
QCOMPARE(result.meta[4].depth, 2);
|
||||
QCOMPARE(result.meta[4].foldLevel, 0x402);
|
||||
QCOMPARE(result.meta[5].depth, 2);
|
||||
// Inner fields at depth 1
|
||||
QCOMPARE(result.meta[4].depth, 1);
|
||||
QCOMPARE(result.meta[4].foldLevel, 0x401);
|
||||
QCOMPARE(result.meta[5].depth, 1);
|
||||
|
||||
// Inner footer
|
||||
QCOMPARE(result.meta[6].lineKind, LineKind::Footer);
|
||||
QCOMPARE(result.meta[6].depth, 1);
|
||||
|
||||
// Outer footer
|
||||
QCOMPARE(result.meta[7].lineKind, LineKind::Footer);
|
||||
QCOMPARE(result.meta[7].depth, 0);
|
||||
QCOMPARE(result.meta[6].depth, 0);
|
||||
}
|
||||
|
||||
void testPointerDerefExpansion() {
|
||||
@@ -395,36 +378,28 @@ private slots:
|
||||
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + Main: header + magic + ptr(merged fold header) + fn1 + fn2 + ptr footer + Main footer = 8
|
||||
// CommandRow + CommandRow2 + magic + ptr(merged fold header) + fn1 + fn2 + ptr footer = 7
|
||||
// VTable standalone: header + fn1 + fn2 + footer = 4
|
||||
// Total = 12
|
||||
QCOMPARE(result.meta.size(), 12);
|
||||
// Total = 11 (root header/footer suppressed)
|
||||
QCOMPARE(result.meta.size(), 11);
|
||||
|
||||
// Main header
|
||||
QCOMPARE(result.meta[1].lineKind, LineKind::Header);
|
||||
QCOMPARE(result.meta[1].depth, 0);
|
||||
|
||||
// magic field
|
||||
// magic field (depth 0, root children at depth 0)
|
||||
QCOMPARE(result.meta[2].lineKind, LineKind::Field);
|
||||
QCOMPARE(result.meta[2].depth, 1);
|
||||
QCOMPARE(result.meta[2].depth, 0);
|
||||
|
||||
// Pointer as merged fold header: "ptr64<VTable> ptr {"
|
||||
QCOMPARE(result.meta[3].lineKind, LineKind::Header);
|
||||
QCOMPARE(result.meta[3].depth, 1);
|
||||
QCOMPARE(result.meta[3].depth, 0);
|
||||
QVERIFY(result.meta[3].foldHead);
|
||||
QCOMPARE(result.meta[3].nodeKind, NodeKind::Pointer64);
|
||||
|
||||
// Expanded fields at depth 2 (struct header merged into pointer)
|
||||
QCOMPARE(result.meta[4].depth, 2);
|
||||
QCOMPARE(result.meta[5].depth, 2);
|
||||
// Expanded fields at depth 1 (struct header merged into pointer)
|
||||
QCOMPARE(result.meta[4].depth, 1);
|
||||
QCOMPARE(result.meta[5].depth, 1);
|
||||
|
||||
// Pointer fold footer
|
||||
QCOMPARE(result.meta[6].lineKind, LineKind::Footer);
|
||||
QCOMPARE(result.meta[6].depth, 1);
|
||||
|
||||
// Main footer
|
||||
QCOMPARE(result.meta[7].lineKind, LineKind::Footer);
|
||||
QCOMPARE(result.meta[7].depth, 0);
|
||||
QCOMPARE(result.meta[6].depth, 0);
|
||||
}
|
||||
|
||||
void testPointerDerefNull() {
|
||||
@@ -468,10 +443,10 @@ private slots:
|
||||
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + Main: header + ptr(merged fold header) + ptr footer + Main footer = 5
|
||||
// CommandRow + CommandRow2 + ptr(merged fold header) + ptr footer = 4
|
||||
// Target standalone: header + field + footer = 3
|
||||
// Total = 8
|
||||
QCOMPARE(result.meta.size(), 8);
|
||||
// Total = 7 (root header/footer suppressed)
|
||||
QCOMPARE(result.meta.size(), 7);
|
||||
|
||||
// Pointer as merged fold header (expanded but empty — null ptr)
|
||||
QCOMPARE(result.meta[2].lineKind, LineKind::Header);
|
||||
@@ -479,10 +454,6 @@ private slots:
|
||||
|
||||
// Pointer fold footer (empty expansion)
|
||||
QCOMPARE(result.meta[3].lineKind, LineKind::Footer);
|
||||
|
||||
// Main footer
|
||||
QCOMPARE(result.meta[4].lineKind, LineKind::Footer);
|
||||
QCOMPARE(result.meta[4].depth, 0);
|
||||
}
|
||||
|
||||
void testPointerDerefCollapsed() {
|
||||
@@ -529,17 +500,14 @@ private slots:
|
||||
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + Main: header + ptr(fold head, collapsed) + footer = 4
|
||||
// CommandRow + CommandRow2 + ptr(fold head, collapsed) = 3
|
||||
// Target standalone: header + field + footer = 3
|
||||
// Total = 7
|
||||
QCOMPARE(result.meta.size(), 7);
|
||||
// Total = 6 (root header/footer suppressed)
|
||||
QCOMPARE(result.meta.size(), 6);
|
||||
|
||||
// Pointer is fold head
|
||||
// Pointer is fold head (depth 0, root children at depth 0)
|
||||
QVERIFY(result.meta[2].foldHead);
|
||||
|
||||
// No expansion — next is Main footer
|
||||
QCOMPARE(result.meta[3].lineKind, LineKind::Footer);
|
||||
QCOMPARE(result.meta[3].depth, 0);
|
||||
QCOMPARE(result.meta[2].depth, 0);
|
||||
}
|
||||
|
||||
void testPointerDerefCycle() {
|
||||
@@ -602,47 +570,58 @@ private slots:
|
||||
QVERIFY(result.meta.size() > 0);
|
||||
QVERIFY(result.meta.size() < 100); // sanity: bounded output
|
||||
|
||||
// First expansion: CommandRow + Main header + ptr merged header + data + self merged header
|
||||
// Root suppressed: CommandRow + CommandRow2 + ptr merged header + data + self merged header
|
||||
// Second expansion blocked by cycle guard: no children under self
|
||||
// Then: self footer + ptr footer + Main footer
|
||||
// Plus standalone Recursive rendering
|
||||
// The exact count depends on cycle guard behavior but must be finite
|
||||
QCOMPARE(result.meta[1].lineKind, LineKind::Header); // Main header
|
||||
// Then: self footer + ptr footer + standalone Recursive rendering
|
||||
QVERIFY(result.meta[2].foldHead); // ptr merged fold head
|
||||
QCOMPARE(result.meta[2].lineKind, LineKind::Header); // ptr merged header
|
||||
QCOMPARE(result.meta[3].lineKind, LineKind::Field); // data field (first child of Recursive)
|
||||
}
|
||||
|
||||
void testStructFooterSimple() {
|
||||
// Root footer is suppressed; test nested struct footer instead
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Sized";
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node inner;
|
||||
inner.kind = NodeKind::Struct;
|
||||
inner.name = "Inner";
|
||||
inner.parentId = rootId;
|
||||
inner.offset = 0;
|
||||
int ii = tree.addNode(inner);
|
||||
uint64_t innerId = tree.nodes[ii].id;
|
||||
|
||||
Node f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.parentId = innerId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// Footer is the last line
|
||||
int lastLine = result.meta.size() - 1;
|
||||
QCOMPARE(result.meta[lastLine].lineKind, LineKind::Footer);
|
||||
// Find a footer line (nested struct footer)
|
||||
int footerLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].lineKind == LineKind::Footer) {
|
||||
footerLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(footerLine >= 0, "Should have a footer for nested struct");
|
||||
|
||||
// Footer text should just be "};" (no sizeof)
|
||||
QString footerText = result.text.split('\n').last();
|
||||
QVERIFY(footerText.contains("};"));
|
||||
QVERIFY(!footerText.contains("sizeof"));
|
||||
// Footer text should contain "};" (no sizeof)
|
||||
QStringList lines = result.text.split('\n');
|
||||
QVERIFY(lines[footerLine].contains("};"));
|
||||
QVERIFY(!lines[footerLine].contains("sizeof"));
|
||||
}
|
||||
|
||||
void testLineMetaHasNodeId() {
|
||||
@@ -660,12 +639,17 @@ private slots:
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
// Skip CommandRow (synthetic line with sentinel nodeId)
|
||||
// Skip CommandRow / CommandRow2 (synthetic lines with sentinel nodeId)
|
||||
if (result.meta[i].lineKind == LineKind::CommandRow) {
|
||||
QCOMPARE(result.meta[i].nodeId, kCommandRowId);
|
||||
QCOMPARE(result.meta[i].nodeIdx, -1);
|
||||
continue;
|
||||
}
|
||||
if (result.meta[i].lineKind == LineKind::CommandRow2) {
|
||||
QCOMPARE(result.meta[i].nodeId, kCommandRow2Id);
|
||||
QCOMPARE(result.meta[i].nodeIdx, -1);
|
||||
continue;
|
||||
}
|
||||
QVERIFY2(result.meta[i].nodeId != 0,
|
||||
qPrintable(QString("Line %1 has nodeId=0").arg(i)));
|
||||
int ni = result.meta[i].nodeIdx;
|
||||
@@ -942,8 +926,8 @@ private slots:
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + Root header + Array header(collapsed) + Root footer = 4
|
||||
QCOMPARE(result.meta.size(), 4);
|
||||
// CommandRow + CommandRow2 + Array header(collapsed) = 3 (root header/footer suppressed)
|
||||
QCOMPARE(result.meta.size(), 3);
|
||||
|
||||
// Array header is collapsed
|
||||
int arrLine = -1;
|
||||
@@ -1138,12 +1122,11 @@ private slots:
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// Find the pointer line
|
||||
// Find the pointer line (root children at depth 0 due to root suppression)
|
||||
int ptrLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeKind == NodeKind::Pointer64 &&
|
||||
result.meta[i].lineKind == LineKind::Field &&
|
||||
result.meta[i].depth > 0) {
|
||||
result.meta[i].lineKind == LineKind::Field) {
|
||||
ptrLine = i;
|
||||
break;
|
||||
}
|
||||
@@ -1239,8 +1222,7 @@ private slots:
|
||||
int ptrLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeKind == NodeKind::Pointer64 &&
|
||||
result.meta[i].lineKind == LineKind::Field &&
|
||||
result.meta[i].depth > 0) {
|
||||
result.meta[i].lineKind == LineKind::Field) {
|
||||
ptrLine = i;
|
||||
break;
|
||||
}
|
||||
@@ -1500,14 +1482,9 @@ private slots:
|
||||
QVERIFY2(foundToMain, "Should display 'ptr64<Main>'");
|
||||
|
||||
// The first expansion of each pointer works;
|
||||
// the cycle is caught on the second attempt
|
||||
int mainHeaders = 0;
|
||||
for (const LineMeta& lm : result.meta) {
|
||||
if (lm.lineKind == LineKind::Header && lm.nodeIdx == mi)
|
||||
mainHeaders++;
|
||||
}
|
||||
// Main appears as root + expanded once from StructB, then blocked on re-expansion
|
||||
QVERIFY2(mainHeaders >= 1, "Main should appear at least once");
|
||||
// the cycle is caught on the second attempt.
|
||||
// Main root header is suppressed, and pointer deref uses isArrayChild=true
|
||||
// (which also skips headers), so we verify cycle detection by bounded output above.
|
||||
}
|
||||
|
||||
void testAllStructsResolvedAsPointerTargets() {
|
||||
@@ -1716,6 +1693,144 @@ private slots:
|
||||
qPrintable(QString("typeW=%1, should be >= 35").arg(result.layout.typeW)));
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// Class keyword + alignment tests
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
|
||||
void testClassKeywordJsonRoundTrip() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
root.classKeyword = "class";
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node f;
|
||||
f.kind = NodeKind::Hex32;
|
||||
f.name = "x";
|
||||
f.parentId = rootId;
|
||||
f.offset = 0;
|
||||
tree.addNode(f);
|
||||
|
||||
// Save and reload
|
||||
QJsonObject json = tree.toJson();
|
||||
NodeTree tree2 = NodeTree::fromJson(json);
|
||||
|
||||
// Find the root struct in the reloaded tree
|
||||
bool found = false;
|
||||
for (const auto& n : tree2.nodes) {
|
||||
if (n.kind == NodeKind::Struct && n.name == "Root") {
|
||||
QCOMPARE(n.classKeyword, QString("class"));
|
||||
QCOMPARE(n.resolvedClassKeyword(), QString("class"));
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(found, "Root struct should exist after JSON round-trip");
|
||||
}
|
||||
|
||||
void testClassKeywordDefaultsToStruct() {
|
||||
NodeTree tree;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
// classKeyword left empty
|
||||
tree.addNode(root);
|
||||
|
||||
QJsonObject json = tree.toJson();
|
||||
NodeTree tree2 = NodeTree::fromJson(json);
|
||||
|
||||
for (const auto& n : tree2.nodes) {
|
||||
if (n.kind == NodeKind::Struct) {
|
||||
QVERIFY(n.classKeyword.isEmpty());
|
||||
QCOMPARE(n.resolvedClassKeyword(), QString("struct"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void testComputeStructAlignment() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Int32 has alignment 4
|
||||
Node f1;
|
||||
f1.kind = NodeKind::Int32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
QCOMPARE(tree.computeStructAlignment(rootId), 4);
|
||||
|
||||
// Add Hex64 (alignment 8) — max should become 8
|
||||
Node f2;
|
||||
f2.kind = NodeKind::Hex64;
|
||||
f2.name = "b";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 8;
|
||||
tree.addNode(f2);
|
||||
|
||||
QCOMPARE(tree.computeStructAlignment(rootId), 8);
|
||||
}
|
||||
|
||||
void testComputeStructAlignmentEmpty() {
|
||||
NodeTree tree;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Empty";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Empty struct → alignment 1
|
||||
QCOMPARE(tree.computeStructAlignment(rootId), 1);
|
||||
}
|
||||
|
||||
void testCommandRow2AlignasSpan() {
|
||||
// Test span detection for alignas(N) in CommandRow2 text
|
||||
QString text = "struct MyClass alignas(8)";
|
||||
ColumnSpan span = commandRow2AlignasSpan(text);
|
||||
QVERIFY(span.valid);
|
||||
QVERIFY(span.start >= 0);
|
||||
QVERIFY(span.end > span.start);
|
||||
|
||||
QString spanText = text.mid(span.start, span.end - span.start);
|
||||
QCOMPARE(spanText, QString("alignas(8)"));
|
||||
}
|
||||
|
||||
void testCommandRow2AlignasSpanNoMatch() {
|
||||
// Text without alignas should return invalid span
|
||||
QString text = "struct MyClass";
|
||||
ColumnSpan span = commandRow2AlignasSpan(text);
|
||||
QVERIFY(!span.valid);
|
||||
}
|
||||
|
||||
void testCommandRow2NameSpanStopsBeforeAlignas() {
|
||||
// Name span should NOT include the alignas part
|
||||
QString text = "struct MyClass alignas(4)";
|
||||
ColumnSpan nameSpan = commandRow2NameSpan(text);
|
||||
QVERIFY(nameSpan.valid);
|
||||
|
||||
QString nameText = text.mid(nameSpan.start, nameSpan.end - nameSpan.start);
|
||||
QVERIFY2(!nameText.contains("alignas"),
|
||||
qPrintable("Name span should not include alignas: " + nameText));
|
||||
QVERIFY2(nameText.trimmed() == "MyClass",
|
||||
qPrintable("Name span should be 'MyClass', got: '" + nameText.trimmed() + "'"));
|
||||
}
|
||||
|
||||
void testTextIsNonEmpty() {
|
||||
// Verify composed text is actually generated (not empty)
|
||||
NodeTree tree;
|
||||
|
||||
@@ -3,13 +3,44 @@
|
||||
#include <QApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QFocusEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QFile>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include <Qsci/qsciscintillabase.h>
|
||||
#include "editor.h"
|
||||
#include "core.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
// ── Cursor test helpers ──
|
||||
|
||||
static Qt::CursorShape viewportCursor(RcxEditor* editor) {
|
||||
return editor->scintilla()->viewport()->cursor().shape();
|
||||
}
|
||||
|
||||
static QPoint colToViewport(QsciScintilla* sci, int line, int col) {
|
||||
long pos = sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
||||
(unsigned long)line, (long)col);
|
||||
int x = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION, 0, pos);
|
||||
int y = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION, 0, pos);
|
||||
return QPoint(x, y);
|
||||
}
|
||||
|
||||
static void sendMouseMove(QWidget* viewport, const QPoint& pos) {
|
||||
QMouseEvent move(QEvent::MouseMove, QPointF(pos), QPointF(pos),
|
||||
Qt::NoButton, Qt::NoButton, Qt::NoModifier);
|
||||
QApplication::sendEvent(viewport, &move);
|
||||
}
|
||||
|
||||
static void sendLeftClick(QWidget* viewport, const QPoint& pos) {
|
||||
QMouseEvent press(QEvent::MouseButtonPress, QPointF(pos), QPointF(pos),
|
||||
Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
|
||||
QApplication::sendEvent(viewport, &press);
|
||||
QMouseEvent release(QEvent::MouseButtonRelease, QPointF(pos), QPointF(pos),
|
||||
Qt::LeftButton, Qt::NoButton, Qt::NoModifier);
|
||||
QApplication::sendEvent(viewport, &release);
|
||||
}
|
||||
|
||||
// 0x7D0 bytes of PEB-like data with recognizable values at key offsets
|
||||
static BufferProvider makeTestProvider() {
|
||||
QByteArray data(0x7D0, '\0');
|
||||
@@ -363,7 +394,7 @@ private slots:
|
||||
|
||||
// ── Test: inline edit lifecycle (begin → commit → re-edit) ──
|
||||
void testInlineEditReEntry() {
|
||||
// Move cursor to line 2 (first field inside struct; line 0=CommandRow, 1=header)
|
||||
// Move cursor to line 2 (first field; line 0=CommandRow, 1=CommandRow2, root header suppressed)
|
||||
m_editor->scintilla()->setCursorPosition(2, 0);
|
||||
|
||||
// Should not be editing
|
||||
@@ -470,19 +501,36 @@ private slots:
|
||||
void testHeaderLineEdit() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Line 1 should be the struct header (line 0 is CommandRow)
|
||||
const LineMeta* lm = m_editor->metaForLine(1);
|
||||
// Root header is suppressed; find a nested struct header (e.g. CSDVersion)
|
||||
int headerLine = -1;
|
||||
for (int i = 0; i < m_result.meta.size(); i++) {
|
||||
if (m_result.meta[i].lineKind == LineKind::Header &&
|
||||
m_result.meta[i].foldHead) {
|
||||
headerLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(headerLine >= 0, "Should have a nested struct header");
|
||||
|
||||
const LineMeta* lm = m_editor->metaForLine(headerLine);
|
||||
QVERIFY(lm);
|
||||
QCOMPARE(lm->lineKind, LineKind::Header);
|
||||
|
||||
// Type edit on header should succeed (has typename _PEB64)
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1);
|
||||
// Scroll to header line to ensure visibility
|
||||
m_editor->scintilla()->SendScintilla(
|
||||
QsciScintillaBase::SCI_ENSUREVISIBLE, (unsigned long)headerLine);
|
||||
m_editor->scintilla()->SendScintilla(
|
||||
QsciScintillaBase::SCI_GOTOLINE, (unsigned long)headerLine);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Type edit on header should succeed
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Type, headerLine);
|
||||
QVERIFY(ok);
|
||||
QVERIFY(m_editor->isEditing());
|
||||
m_editor->cancelInlineEdit();
|
||||
|
||||
// Name edit on header should succeed
|
||||
ok = m_editor->beginInlineEdit(EditTarget::Name, 1);
|
||||
ok = m_editor->beginInlineEdit(EditTarget::Name, headerLine);
|
||||
QVERIFY(ok);
|
||||
QVERIFY(m_editor->isEditing());
|
||||
m_editor->cancelInlineEdit();
|
||||
@@ -617,7 +665,7 @@ private slots:
|
||||
void testColumnSpanHitTest() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Line 2 is a field line (UInt8), verify spans are valid (line 0=CommandRow, 1=header)
|
||||
// Line 2 is a field line (UInt8), verify spans are valid (line 0=CommandRow, 1=CommandRow2)
|
||||
const LineMeta* lm = m_editor->metaForLine(2);
|
||||
QVERIFY(lm);
|
||||
QCOMPARE(lm->lineKind, LineKind::Field);
|
||||
@@ -664,7 +712,7 @@ private slots:
|
||||
void testSelectedNodeIndices() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Put cursor on first field line (line 2; 0=CommandRow, 1=header)
|
||||
// Put cursor on first field line (line 2; 0=CommandRow, 1=CommandRow2, root header suppressed)
|
||||
m_editor->scintilla()->setCursorPosition(2, 0);
|
||||
QSet<int> sel = m_editor->selectedNodeIndices();
|
||||
QCOMPARE(sel.size(), 1);
|
||||
@@ -675,7 +723,7 @@ private slots:
|
||||
QVERIFY(sel.contains(lm->nodeIdx));
|
||||
}
|
||||
|
||||
// ── Test: header line no longer contains "// base:" ──
|
||||
// ── Test: composed text does not contain "// base:" (moved to cmd bar) ──
|
||||
void testBaseAddressDisplay() {
|
||||
NodeTree tree = makeTestTree();
|
||||
tree.baseAddress = 0x10;
|
||||
@@ -684,27 +732,14 @@ private slots:
|
||||
|
||||
m_editor->applyDocument(result);
|
||||
|
||||
// Line 1 should be the struct header (line 0 is CommandRow)
|
||||
const LineMeta* lm = m_editor->metaForLine(1);
|
||||
// Root header is suppressed; verify no "// base:" anywhere in output
|
||||
QVERIFY2(!result.text.contains("// base:"),
|
||||
"Composed text should not contain '// base:' (consolidated into cmd bar)");
|
||||
|
||||
// Line 2 should be the first field (root header suppressed)
|
||||
const LineMeta* lm = m_editor->metaForLine(2);
|
||||
QVERIFY(lm);
|
||||
QCOMPARE(lm->lineKind, LineKind::Header);
|
||||
QVERIFY(lm->isRootHeader);
|
||||
|
||||
// Get header line text — should NOT contain "// base:" (consolidated into cmd bar)
|
||||
QString lineText;
|
||||
int len = (int)m_editor->scintilla()->SendScintilla(
|
||||
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1);
|
||||
if (len > 0) {
|
||||
QByteArray buf(len + 1, '\0');
|
||||
m_editor->scintilla()->SendScintilla(
|
||||
QsciScintillaBase::SCI_GETLINE, (unsigned long)1, (void*)buf.data());
|
||||
lineText = QString::fromUtf8(buf.constData(), len).trimmed();
|
||||
}
|
||||
|
||||
QVERIFY2(!lineText.contains("// base:"),
|
||||
qPrintable("Header should no longer contain '// base:', got: " + lineText));
|
||||
QVERIFY2(lineText.contains("struct"),
|
||||
qPrintable("Header should contain 'struct', got: " + lineText));
|
||||
QCOMPARE(lm->lineKind, LineKind::Field);
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
@@ -817,7 +852,7 @@ private slots:
|
||||
void testValueEditCommitUpdatesSignal() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Line 2 = first UInt8 field (InheritedAddressSpace)
|
||||
// Line 2 = first UInt8 field (InheritedAddressSpace, root header suppressed)
|
||||
const LineMeta* lm = m_editor->metaForLine(2);
|
||||
QVERIFY(lm);
|
||||
QCOMPARE(lm->lineKind, LineKind::Field);
|
||||
@@ -878,6 +913,192 @@ private slots:
|
||||
m_editor->cancelInlineEdit();
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: cursor stays Arrow after left-click on a node ──
|
||||
void testCursorAfterLeftClick() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Click on a field line at the indent area (col 0 — not over editable text)
|
||||
QPoint clickPos = colToViewport(m_editor->scintilla(), 2, 0);
|
||||
sendLeftClick(m_editor->scintilla()->viewport(), clickPos);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Cursor must be Arrow — QScintilla must NOT have set it to IBeam
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::ArrowCursor);
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
}
|
||||
|
||||
// ── Test: cursor is IBeam only over trimmed name text, Arrow over padding ──
|
||||
void testCursorShapeOverText() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Line 2 is a field (UInt8 InheritedAddressSpace)
|
||||
const LineMeta* lm = m_editor->metaForLine(2);
|
||||
QVERIFY(lm);
|
||||
|
||||
// Get the name span (padded to kColName width)
|
||||
ColumnSpan ns = RcxEditor::nameSpan(*lm, lm->effectiveTypeW, lm->effectiveNameW);
|
||||
QVERIFY(ns.valid);
|
||||
|
||||
// Move mouse to the start of the name span (should be over text)
|
||||
QPoint textPos = colToViewport(m_editor->scintilla(), 2, ns.start + 1);
|
||||
sendMouseMove(m_editor->scintilla()->viewport(), textPos);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::IBeamCursor);
|
||||
|
||||
// Move mouse to far padding area (past end of text, within padded span)
|
||||
// The padded span ends at ns.end but the trimmed text is shorter
|
||||
QPoint padPos = colToViewport(m_editor->scintilla(), 2, ns.end - 1);
|
||||
sendMouseMove(m_editor->scintilla()->viewport(), padPos);
|
||||
QApplication::processEvents();
|
||||
// Should be Arrow (padding whitespace, not actual text)
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::ArrowCursor);
|
||||
}
|
||||
|
||||
// ── Test: cursor is PointingHand over type column text ──
|
||||
void testCursorShapeOverType() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
const LineMeta* lm = m_editor->metaForLine(2);
|
||||
QVERIFY(lm);
|
||||
|
||||
// Type span starts after the fold column + indent
|
||||
ColumnSpan ts = RcxEditor::typeSpan(*lm, lm->effectiveTypeW);
|
||||
QVERIFY(ts.valid);
|
||||
|
||||
// Move to start of type text (e.g. "uint8_t")
|
||||
QPoint typePos = colToViewport(m_editor->scintilla(), 2, ts.start + 1);
|
||||
sendMouseMove(m_editor->scintilla()->viewport(), typePos);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||
}
|
||||
|
||||
// ── Test: cursor is PointingHand over fold column ──
|
||||
void testCursorShapeInFoldColumn() {
|
||||
m_editor->applyDocument(m_result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Root header (line 2) has fold suppressed; find a nested struct with foldHead
|
||||
int foldLine = -1;
|
||||
for (int i = 0; i < m_result.meta.size(); i++) {
|
||||
if (m_result.meta[i].foldHead && m_result.meta[i].lineKind == LineKind::Header) {
|
||||
foldLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foldLine >= 0, "Should have at least one foldable struct header");
|
||||
|
||||
const LineMeta* lm = m_editor->metaForLine(foldLine);
|
||||
QVERIFY(lm);
|
||||
QVERIFY(lm->foldHead);
|
||||
|
||||
// Scroll to ensure the fold line is visible
|
||||
m_editor->scintilla()->SendScintilla(
|
||||
QsciScintillaBase::SCI_ENSUREVISIBLE, (unsigned long)foldLine);
|
||||
m_editor->scintilla()->SendScintilla(
|
||||
QsciScintillaBase::SCI_GOTOLINE, (unsigned long)foldLine);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Fold indicator is always at cols 0-2 (kFoldCol=3), regardless of depth
|
||||
QPoint foldPos = colToViewport(m_editor->scintilla(), foldLine, 1);
|
||||
QVERIFY2(foldPos.y() > 0, qPrintable(QString("Fold line %1 should be visible, got y=%2")
|
||||
.arg(foldLine).arg(foldPos.y())));
|
||||
sendMouseMove(m_editor->scintilla()->viewport(), foldPos);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||
}
|
||||
|
||||
// ── Test: no IBeam after click then mouse-move to non-editable area ──
|
||||
void testNoIBeamAfterClickThenMove() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Click on a field to select the node
|
||||
const LineMeta* lm = m_editor->metaForLine(2);
|
||||
QVERIFY(lm);
|
||||
ColumnSpan ns = RcxEditor::nameSpan(*lm, lm->effectiveTypeW, lm->effectiveNameW);
|
||||
QVERIFY(ns.valid);
|
||||
|
||||
// Click in the name area (selects the node)
|
||||
QPoint clickPos = colToViewport(m_editor->scintilla(), 2, ns.start + 1);
|
||||
sendLeftClick(m_editor->scintilla()->viewport(), clickPos);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Now move mouse to col 0 (indent area — non-editable)
|
||||
QPoint emptyPos = colToViewport(m_editor->scintilla(), 2, 0);
|
||||
sendMouseMove(m_editor->scintilla()->viewport(), emptyPos);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Must be Arrow, NOT IBeam (QScintilla must not have leaked its cursor state)
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::ArrowCursor);
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
}
|
||||
|
||||
// ── Test: CommandRow2 exists at line 1 ──
|
||||
void testCommandRow2Exists() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Line 1 should be CommandRow2
|
||||
const LineMeta* lm = m_editor->metaForLine(1);
|
||||
QVERIFY(lm);
|
||||
QCOMPARE(lm->lineKind, LineKind::CommandRow2);
|
||||
QCOMPARE(lm->nodeId, kCommandRow2Id);
|
||||
QCOMPARE(lm->nodeIdx, -1);
|
||||
|
||||
// Type/Name/Value should be rejected on CommandRow2
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Type, 1));
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Name, 1));
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, 1));
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
|
||||
// RootClassName should be allowed on CommandRow2
|
||||
m_editor->setCommandRow2Text(QStringLiteral("struct _PEB64"));
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 1);
|
||||
QVERIFY2(ok, "RootClassName edit should be allowed on CommandRow2");
|
||||
QVERIFY(m_editor->isEditing());
|
||||
m_editor->cancelInlineEdit();
|
||||
}
|
||||
|
||||
// ── Test: alignas span detection on CommandRow2 ──
|
||||
void testAlignasSpanOnCommandRow2() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Set CommandRow2 with alignas
|
||||
m_editor->setCommandRow2Text(QStringLiteral("struct _PEB64 alignas(8)"));
|
||||
|
||||
// Line 1 is CommandRow2
|
||||
const LineMeta* lm = m_editor->metaForLine(1);
|
||||
QVERIFY(lm);
|
||||
QCOMPARE(lm->lineKind, LineKind::CommandRow2);
|
||||
|
||||
// Alignas IS allowed as inline edit (picker-based)
|
||||
QVERIFY(m_editor->beginInlineEdit(EditTarget::Alignas, 1));
|
||||
QVERIFY(m_editor->isEditing());
|
||||
m_editor->cancelInlineEdit();
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: root header/footer are suppressed (CommandRow2 replaces them) ──
|
||||
void testRootFoldSuppressed() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Root struct header is completely suppressed from output.
|
||||
// Line 0 = CommandRow, Line 1 = CommandRow2, Line 2 = first field.
|
||||
const LineMeta* lm2 = m_editor->metaForLine(2);
|
||||
QVERIFY(lm2);
|
||||
QCOMPARE(lm2->lineKind, LineKind::Field);
|
||||
|
||||
// Verify no root header exists anywhere in the output
|
||||
bool foundRootHeader = false;
|
||||
for (int i = 0; i < m_result.meta.size(); i++) {
|
||||
if (m_result.meta[i].isRootHeader) {
|
||||
foundRootHeader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(!foundRootHeader,
|
||||
"Root header should be suppressed from compose output");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestEditor)
|
||||
|
||||
Reference in New Issue
Block a user