mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
15
src/main.cpp
15
src/main.cpp
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user