Struct headers: type+name format, auto-collapse, no footer when collapsed

- Add structTypeName field to Node for struct type names
- Struct headers now show: struct TYPENAME name {
- Auto-collapse structs/arrays by default
- Skip footer rendering when collapsed (cleaner view)
- Fix header span calculations for new format
- Disable brace matching (not needed for structured viewer)
- Change hover color to muted teal (distinct from keywords)
This commit is contained in:
sysadmin
2026-02-05 08:56:50 -07:00
parent a0d6b769b6
commit 6b9adf03fe
6 changed files with 90 additions and 25 deletions

View File

@@ -246,8 +246,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
}
}
// Footer line (skip for array element structs - condensed display)
if (!isArrayChild) {
// Footer line: skip when collapsed (only header shows) or for array element structs
if (!isArrayChild && !node.collapsed) {
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;

View File

@@ -1,4 +1,5 @@
#include "controller.h"
#include <Qsci/qsciscintilla.h>
#include <QSplitter>
#include <QFile>
#include <QJsonDocument>
@@ -338,8 +339,8 @@ void RcxController::insertNode(uint64_t parentId, int offset, NodeKind kind, con
n.offset = offset;
}
// Assign ID before storing
n.id = m_doc->tree.m_nextId;
// Reserve unique ID atomically before pushing command
n.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
}
@@ -627,6 +628,10 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
QStringLiteral("+0x") + QString::number(off, 16).toUpper().rightJustified(4, '0'));
});
menu.addAction("Copy All as &Text", [editor]() {
QApplication::clipboard()->setText(editor->scintilla()->text());
});
menu.exec(globalPos);
}

View File

@@ -233,6 +233,7 @@ struct Node {
uint64_t id = 0;
NodeKind kind = NodeKind::Hex8;
QString name;
QString structTypeName; // Struct/Array: optional type name (e.g., "IMAGE_DOS_HEADER")
uint64_t parentId = 0; // 0 = root (no parent)
int offset = 0;
int arrayLen = 1; // Array: element count
@@ -257,6 +258,8 @@ struct Node {
o["id"] = QString::number(id);
o["kind"] = kindToString(kind);
o["name"] = name;
if (!structTypeName.isEmpty())
o["structTypeName"] = structTypeName;
o["parentId"] = QString::number(parentId);
o["offset"] = offset;
o["arrayLen"] = arrayLen;
@@ -271,6 +274,7 @@ struct Node {
n.id = o["id"].toString("0").toULongLong();
n.kind = kindFromString(o["kind"].toString());
n.name = o["name"].toString();
n.structTypeName = o["structTypeName"].toString();
n.parentId = o["parentId"].toString("0").toULongLong();
n.offset = o["offset"].toInt(0);
n.arrayLen = o["arrayLen"].toInt(1);
@@ -305,6 +309,9 @@ struct NodeTree {
return nodes.size() - 1;
}
// Reserve a unique ID atomically (for use before pushing undo commands)
uint64_t reserveId() { return m_nextId++; }
void invalidateIdCache() const { m_idCache.clear(); }
int indexOfId(uint64_t id) const {

View File

@@ -142,11 +142,11 @@ void RcxEditor::setupScintilla() {
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_BASE_ADDR, QColor("#5a8248"));
// Hover span indicator — blue text like a link
// Hover span indicator — muted teal text (distinct from blue keywords)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_HOVER_SPAN, 17 /*INDIC_TEXTFORE*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_HOVER_SPAN, QColor("#569cd6"));
IND_HOVER_SPAN, QColor("#3d9c8a"));
}
void RcxEditor::setupLexer() {
@@ -175,7 +175,7 @@ void RcxEditor::setupLexer() {
}
m_sci->setLexer(m_lexer);
m_sci->setBraceMatching(QsciScintilla::SloppyBraceMatch);
m_sci->setBraceMatching(QsciScintilla::NoBraceMatch); // Disable - this is a structured viewer
// Add type names to keyword set 2 → teal coloring (distinct from identifiers)
QByteArray kw2 = allTypeNamesForUI(/*stripBrackets=*/true).join(' ').toLatin1();
@@ -553,20 +553,55 @@ RcxEditor::EndEditInfo RcxEditor::endInlineEdit() {
// ── Span helpers ──
// Name span for struct/array headers
// Format: "struct TYPENAME NAME {" or "struct NAME {" or "type[N] NAME {"
// Returns span of the last word before " {"
static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header) return {};
int bracePos = lineText.lastIndexOf(QStringLiteral(" {"));
if (bracePos <= 0) return {};
int ind = kFoldCol + lm.depth * 3;
int typeEnd = lineText.indexOf(' ', ind);
if (typeEnd <= ind || typeEnd >= bracePos) return {};
// Find the last space before " {" - the name starts after that
int nameStart = lineText.lastIndexOf(' ', bracePos - 1);
if (nameStart < 0) return {};
nameStart++; // Move past the space
// Don't allow editing array element names like "[0]", "[1]", etc.
QString name = lineText.mid(typeEnd + 1, bracePos - typeEnd - 1).trimmed();
QString name = lineText.mid(nameStart, bracePos - nameStart);
if (name.startsWith('[') && name.endsWith(']'))
return {};
return {typeEnd + 1, bracePos, true};
return {nameStart, bracePos, true};
}
// Type name span for struct headers (not arrays)
// Format: "struct TYPENAME NAME {" - returns span of TYPENAME
// For "struct NAME {" (no typename), returns invalid span
static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header) return {};
if (lm.isArrayHeader) return {}; // Arrays use arrayHeaderTypeSpan instead
int bracePos = lineText.lastIndexOf(QStringLiteral(" {"));
if (bracePos <= 0) return {};
int ind = kFoldCol + lm.depth * 3;
// Find first space (after "struct")
int firstSpace = lineText.indexOf(' ', ind);
if (firstSpace <= ind || firstSpace >= bracePos) return {};
// Find second space (after typename, before name)
int secondSpace = lineText.indexOf(' ', firstSpace + 1);
if (secondSpace <= firstSpace || secondSpace >= bracePos) return {}; // No typename
// Find third space (after name) - if exists, we have typename
int thirdSpace = lineText.indexOf(' ', secondSpace + 1);
if (thirdSpace < 0 || thirdSpace > bracePos) {
// Only two words: "struct NAME {" - no typename to edit
return {};
}
// Three+ words: "struct TYPENAME NAME {" - return typename span
return {firstSpace + 1, secondSpace, true};
}
// Type span for array headers: "int32_t[10]" in "int32_t[10] positions {"
@@ -642,8 +677,11 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
}
// Fallback spans for header lines
if (!s.valid && t == EditTarget::Type)
if (!s.valid && t == EditTarget::Type) {
s = arrayHeaderTypeSpan(*lm, lineText);
if (!s.valid)
s = headerTypeNameSpan(*lm, lineText);
}
if (!s.valid && t == EditTarget::Name)
s = headerNameSpan(*lm, lineText);
@@ -720,8 +758,11 @@ static bool hitTestTarget(QsciScintilla* sci,
ColumnSpan bs = baseAddressSpanFor(lm, lineText); // Base address for root headers
// Fallback spans for header lines
if (!ts.valid)
if (!ts.valid) {
ts = arrayHeaderTypeSpan(lm, lineText);
if (!ts.valid)
ts = headerTypeNameSpan(lm, lineText);
}
if (!ns.valid)
ns = headerNameSpan(lm, lineText);

View File

@@ -102,14 +102,23 @@ QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation) {
// ── Struct header / footer ──
QString fmtStructHeader(const Node& node, int depth) {
return indent(depth) + typeName(node.kind).trimmed() +
QStringLiteral(" ") + node.name + QStringLiteral(" {");
// Format: "struct TypeName name {" or "struct name {" if no type name
QString type = typeName(node.kind).trimmed();
if (!node.structTypeName.isEmpty())
return indent(depth) + type + QStringLiteral(" ") + node.structTypeName +
QStringLiteral(" ") + node.name + QStringLiteral(" {");
return indent(depth) + type + QStringLiteral(" ") + node.name + QStringLiteral(" {");
}
QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress) {
// Format: "struct Name { base: 0x00400000" - single space after {
QString header = indent(depth) + typeName(node.kind).trimmed() +
QStringLiteral(" ") + node.name + QStringLiteral(" { ");
// Format: "struct TypeName Name { // base: 0x..." or "struct Name { // base: 0x..."
QString type = typeName(node.kind).trimmed();
QString header;
if (!node.structTypeName.isEmpty())
header = indent(depth) + type + QStringLiteral(" ") + node.structTypeName +
QStringLiteral(" ") + node.name + QStringLiteral(" { ");
else
header = indent(depth) + type + QStringLiteral(" ") + node.name + QStringLiteral(" { ");
QString baseHex = QStringLiteral("0x") + QString::number(baseAddress, 16).toUpper();
return header + QStringLiteral("// base: ") + baseHex;
}

View File

@@ -414,12 +414,14 @@ void MainWindow::newFile() {
return doc->tree.nodes[idx].id;
};
auto addStruct = [&](uint64_t parent, int offset, const QString& name) -> uint64_t {
auto addStruct = [&](uint64_t parent, int offset, const QString& typeName, const QString& name) -> uint64_t {
Node n;
n.kind = NodeKind::Struct;
n.structTypeName = typeName;
n.name = name;
n.parentId = parent;
n.offset = offset;
n.collapsed = true; // Auto-collapse structs
int idx = doc->tree.addNode(n);
return doc->tree.nodes[idx].id;
};
@@ -432,12 +434,13 @@ void MainWindow::newFile() {
n.offset = offset;
n.arrayLen = count;
n.elementKind = elemKind;
n.collapsed = true; // Auto-collapse arrays
int idx = doc->tree.addNode(n);
return doc->tree.nodes[idx].id;
};
// ── Root: IMAGE_DOS_HEADER ──
uint64_t dosId = addStruct(0, 0x00, "IMAGE_DOS_HEADER");
uint64_t dosId = addStruct(0, 0x00, "IMAGE_DOS_HEADER", "dosHeader");
addField(dosId, 0x00, NodeKind::UInt16, "e_magic");
addField(dosId, 0x02, NodeKind::UInt16, "e_cblp");
addField(dosId, 0x04, NodeKind::UInt16, "e_cp");
@@ -458,7 +461,7 @@ void MainWindow::newFile() {
addField(0, peOff, NodeKind::UInt32, "PE_Signature");
// ── IMAGE_FILE_HEADER ──
uint64_t fhId = addStruct(0, fhOff, "IMAGE_FILE_HEADER");
uint64_t fhId = addStruct(0, fhOff, "IMAGE_FILE_HEADER", "fileHeader");
addField(fhId, 0, NodeKind::UInt16, "Machine");
addField(fhId, 2, NodeKind::UInt16, "NumberOfSections");
addField(fhId, 4, NodeKind::UInt32, "TimeDateStamp");
@@ -468,7 +471,7 @@ void MainWindow::newFile() {
addField(fhId, 18, NodeKind::UInt16, "Characteristics");
// ── IMAGE_OPTIONAL_HEADER64 ──
uint64_t ohId = addStruct(0, ohOff, "IMAGE_OPTIONAL_HEADER64");
uint64_t ohId = addStruct(0, ohOff, "IMAGE_OPTIONAL_HEADER64", "optionalHeader");
addField(ohId, 0, NodeKind::UInt16, "Magic");
addField(ohId, 2, NodeKind::UInt8, "MajorLinkerVersion");
addField(ohId, 3, NodeKind::UInt8, "MinorLinkerVersion");
@@ -508,7 +511,7 @@ void MainWindow::newFile() {
"IAT", "DelayImport", "CLR", "Reserved"
};
for (int i = 0; i < 16; i++) {
uint64_t entryId = addStruct(ddArrId, i * 8, QString("[%1] %2").arg(i).arg(ddNames[i]));
uint64_t entryId = addStruct(ddArrId, i * 8, "IMAGE_DATA_DIRECTORY", QString("%1").arg(ddNames[i]));
addField(entryId, 0, NodeKind::UInt32, "VirtualAddress");
addField(entryId, 4, NodeKind::UInt32, "Size");
}
@@ -517,7 +520,7 @@ void MainWindow::newFile() {
uint64_t shArrId = addArray(0, shOff, "SectionHeaders", 4, NodeKind::Struct);
const char* secNames[4] = {".text", ".rdata", ".data", ".pdata"};
for (int i = 0; i < 4; i++) {
uint64_t secId = addStruct(shArrId, i * 40, QString("[%1] %2").arg(i).arg(secNames[i]));
uint64_t secId = addStruct(shArrId, i * 40, "IMAGE_SECTION_HEADER", QString("%1").arg(secNames[i]));
// Name is 8 bytes - show as UTF8 string
Node nameNode;
nameNode.kind = NodeKind::UTF8;