diff --git a/src/compose.cpp b/src/compose.cpp index ca29cfa..55cd72e 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -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; diff --git a/src/controller.cpp b/src/controller.cpp index 5cbf9e1..4734347 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1,4 +1,5 @@ #include "controller.h" +#include #include #include #include @@ -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); } diff --git a/src/core.h b/src/core.h index 8c7078d..23ec5fc 100644 --- a/src/core.h +++ b/src/core.h @@ -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 { diff --git a/src/editor.cpp b/src/editor.cpp index e8fcdb3..6653aec 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -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); diff --git a/src/format.cpp b/src/format.cpp index 8f08e50..b6fd466 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -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; } diff --git a/src/main.cpp b/src/main.cpp index 94665a2..331ca8d 100644 --- a/src/main.cpp +++ b/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;