diff --git a/CMakeLists.txt b/CMakeLists.txt index 6499c1d..6adaf8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ add_executable(ReclassX src/controller.cpp src/compose.cpp src/format.cpp - src/icons.qrc + src/resources.qrc ) target_include_directories(ReclassX PRIVATE src) @@ -31,7 +31,7 @@ target_link_libraries(ReclassX PRIVATE dbghelp ) -add_custom_target(screenshot +add_custom_target(screenshot ALL COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png DEPENDS ReclassX WORKING_DIRECTORY ${CMAKE_BINARY_DIR} diff --git a/src/compose.cpp b/src/compose.cpp index 59eb063..4590a22 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -192,7 +192,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.nodeKind = node.kind; lm.offsetText = QStringLiteral(" ---"); lm.foldLevel = computeFoldLevel(depth, false); - lm.markerMask = (1u << M_STRUCT_BG); + lm.markerMask = 0; int sz = tree.structSpan(node.id, &state.childMap); state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm); } diff --git a/src/controller.cpp b/src/controller.cpp index 75fdfd6..0643841 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -125,9 +125,17 @@ void RcxController::connectEditor(RcxEditor* editor) { this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) { if (nodeIdx < 0) { refresh(); return; } switch (target) { - case EditTarget::Name: - if (!text.isEmpty()) renameNode(nodeIdx, text); + case EditTarget::Name: { + if (text.isEmpty()) break; + const Node& node = m_doc->tree.nodes[nodeIdx]; + // ASCII edit on Hex/Padding nodes + if (isHexPreview(node.kind)) { + setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true); + } else { + renameNode(nodeIdx, text); + } break; + } case EditTarget::Type: { bool ok; NodeKind k = kindFromTypeName(text, &ok); @@ -147,6 +155,7 @@ void RcxController::connectEditor(RcxEditor* editor) { void RcxController::refresh() { m_lastResult = m_doc->compose(); + qDebug() << "refresh() called, text length:" << m_lastResult.text.size(); // Prune stale selections (nodes removed by undo/redo/delete) QSet valid; @@ -173,22 +182,49 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) { Node tmp = node; tmp.kind = newKind; int newSize = tmp.byteSize(); - int delta = newSize - oldSize; - QVector adjs; - if (delta != 0 && oldSize > 0 && newSize > 0) { - int oldEnd = node.offset + oldSize; - auto siblings = m_doc->tree.childrenOf(node.parentId); - for (int si : siblings) { - if (si == nodeIdx) continue; - auto& sib = m_doc->tree.nodes[si]; - if (sib.offset >= oldEnd) - adjs.append({sib.id, sib.offset, sib.offset + delta}); + if (newSize > 0 && newSize < oldSize) { + // Shrinking: insert hex padding to fill gap (no offset shift) + int gap = oldSize - newSize; + uint64_t parentId = node.parentId; + int baseOffset = node.offset + newSize; + + // Push type change with no offset adjustments + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangeKind{node.id, node.kind, newKind, {}})); + + // Insert hex nodes to fill the gap (largest first for alignment) + int padOffset = baseOffset; + while (gap > 0) { + NodeKind padKind; + int padSize; + if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; } + else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; } + else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; } + else { padKind = NodeKind::Hex8; padSize = 1; } + + insertNode(parentId, padOffset, padKind, + QString("pad_%1").arg(padOffset, 2, 16, QChar('0'))); + padOffset += padSize; + gap -= padSize; } + } else { + // Same size or larger: adjust sibling offsets as before + int delta = newSize - oldSize; + QVector adjs; + if (delta != 0 && oldSize > 0 && newSize > 0) { + int oldEnd = node.offset + oldSize; + auto siblings = m_doc->tree.childrenOf(node.parentId); + for (int si : siblings) { + if (si == nodeIdx) continue; + auto& sib = m_doc->tree.nodes[si]; + if (sib.offset >= oldEnd) + adjs.append({sib.id, sib.offset, sib.offset + delta}); + } + } + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangeKind{node.id, node.kind, newKind, adjs})); } - - m_doc->undoStack.push(new RcxCommand(this, - cmd::ChangeKind{node.id, node.kind, newKind, adjs})); } void RcxController::renameNode(int nodeIdx, const QString& newName) { @@ -280,11 +316,13 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { tree.addNode(c.node); } } else if constexpr (std::is_same_v) { + qDebug() << "applyCommand Remove, isUndo:" << isUndo << "nodeId:" << c.nodeId; if (isUndo) { for (const Node& n : c.subtree) tree.addNode(n); } else { QVector indices = tree.subtreeIndices(c.nodeId); + qDebug() << " Removing" << indices.size() << "nodes"; std::sort(indices.begin(), indices.end(), std::greater()); for (int idx : indices) tree.nodes.remove(idx); @@ -301,7 +339,8 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { refresh(); } -void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text) { +void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text, + bool isAscii) { if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; if (!m_doc->provider->isWritable()) return; @@ -317,7 +356,13 @@ void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text) } bool ok; - QByteArray newBytes = fmt::parseValue(editKind, text, &ok); + QByteArray newBytes; + if (isAscii) { + int expectedSize = sizeForKind(editKind); + newBytes = fmt::parseAsciiValue(text, expectedSize, &ok); + } else { + newBytes = fmt::parseValue(editKind, text, &ok); + } if (!ok) return; // For strings, pad/truncate to full buffer size @@ -514,19 +559,30 @@ void RcxController::handleNodeClick(RcxEditor* source, int line, m_selIds.insert(nodeId); m_anchorLine = line; } else if (shift && !ctrl) { - m_selIds.clear(); - int from = qMin(m_anchorLine, 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) m_selIds.insert(nid); + if (m_anchorLine < 0) { + m_selIds.clear(); + m_selIds.insert(nodeId); + m_anchorLine = line; + } else { + m_selIds.clear(); + int from = qMin(m_anchorLine, 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) m_selIds.insert(nid); + } } } else { // Ctrl+Shift - int from = qMin(m_anchorLine, 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) m_selIds.insert(nid); + if (m_anchorLine < 0) { + m_selIds.insert(nodeId); + m_anchorLine = line; + } else { + int from = qMin(m_anchorLine, 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) m_selIds.insert(nid); + } } } @@ -562,4 +618,9 @@ void RcxController::handleMarginClick(RcxEditor* editor, int margin, } } +void RcxController::setEditorFont(const QString& fontName) { + for (auto* editor : m_editors) + editor->setEditorFont(fontName); +} + } // namespace rcx diff --git a/src/controller.h b/src/controller.h index e44234b..ffbf3e5 100644 --- a/src/controller.h +++ b/src/controller.h @@ -64,7 +64,7 @@ public: void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name); void removeNode(int nodeIdx); void toggleCollapse(int nodeIdx); - void setNodeValue(int nodeIdx, int subLine, const QString& text); + void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false); void duplicateNode(int nodeIdx); void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos); void batchRemoveNodes(const QVector& nodeIndices); @@ -81,6 +81,7 @@ public: QSet selectedIds() const { return m_selIds; } RcxDocument* document() const { return m_doc; } + void setEditorFont(const QString& fontName); signals: void nodeSelected(int nodeIdx); diff --git a/src/core.h b/src/core.h index fa7a6c2..ad9d62b 100644 --- a/src/core.h +++ b/src/core.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -27,6 +28,16 @@ enum class NodeKind : uint8_t { Struct, Array }; +// ── Kind flags (replaces repeated Hex/Padding switches) ── + +enum KindFlags : uint32_t { + KF_None = 0, + KF_HexPreview = 1 << 0, // Hex8..Hex64 + Padding (ASCII+hex layout) + KF_Container = 1 << 1, // Struct/Array + KF_String = 1 << 2, // UTF8/UTF16 + KF_Vector = 1 << 3, // Vec2/3/4 +}; + // ── Unified kind metadata table (single source of truth) ── struct KindMeta { @@ -36,36 +47,37 @@ struct KindMeta { int size; // byte size (0 = dynamic: Struct/Array) int lines; // display line count int align; // natural alignment + uint32_t flags; // KindFlags bitmask }; inline constexpr KindMeta kKindMeta[] = { - // kind name typeName sz ln al - {NodeKind::Hex8, "Hex8", "Hex8", 1, 1, 1}, - {NodeKind::Hex16, "Hex16", "Hex16", 2, 1, 2}, - {NodeKind::Hex32, "Hex32", "Hex32", 4, 1, 4}, - {NodeKind::Hex64, "Hex64", "Hex64", 8, 1, 8}, - {NodeKind::Int8, "Int8", "int8_t", 1, 1, 1}, - {NodeKind::Int16, "Int16", "int16_t", 2, 1, 2}, - {NodeKind::Int32, "Int32", "int32_t", 4, 1, 4}, - {NodeKind::Int64, "Int64", "int64_t", 8, 1, 8}, - {NodeKind::UInt8, "UInt8", "uint8_t", 1, 1, 1}, - {NodeKind::UInt16, "UInt16", "uint16_t", 2, 1, 2}, - {NodeKind::UInt32, "UInt32", "uint32_t", 4, 1, 4}, - {NodeKind::UInt64, "UInt64", "uint64_t", 8, 1, 8}, - {NodeKind::Float, "Float", "float", 4, 1, 4}, - {NodeKind::Double, "Double", "double", 8, 1, 8}, - {NodeKind::Bool, "Bool", "bool", 1, 1, 1}, - {NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4}, - {NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8}, - {NodeKind::Vec2, "Vec2", "Vec2", 8, 2, 4}, - {NodeKind::Vec3, "Vec3", "Vec3", 12, 3, 4}, - {NodeKind::Vec4, "Vec4", "Vec4", 16, 4, 4}, - {NodeKind::Mat4x4, "Mat4x4", "Mat4x4", 64, 4, 4}, - {NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1}, - {NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2}, - {NodeKind::Padding, "Padding", "pad", 1, 1, 1}, - {NodeKind::Struct, "Struct", "struct", 0, 1, 1}, - {NodeKind::Array, "Array", "array", 0, 1, 1}, + // kind name typeName sz ln al flags + {NodeKind::Hex8, "Hex8", "Hex8", 1, 1, 1, KF_HexPreview}, + {NodeKind::Hex16, "Hex16", "Hex16", 2, 1, 2, KF_HexPreview}, + {NodeKind::Hex32, "Hex32", "Hex32", 4, 1, 4, KF_HexPreview}, + {NodeKind::Hex64, "Hex64", "Hex64", 8, 1, 8, KF_HexPreview}, + {NodeKind::Int8, "Int8", "int8_t", 1, 1, 1, KF_None}, + {NodeKind::Int16, "Int16", "int16_t", 2, 1, 2, KF_None}, + {NodeKind::Int32, "Int32", "int32_t", 4, 1, 4, KF_None}, + {NodeKind::Int64, "Int64", "int64_t", 8, 1, 8, KF_None}, + {NodeKind::UInt8, "UInt8", "uint8_t", 1, 1, 1, KF_None}, + {NodeKind::UInt16, "UInt16", "uint16_t", 2, 1, 2, KF_None}, + {NodeKind::UInt32, "UInt32", "uint32_t", 4, 1, 4, KF_None}, + {NodeKind::UInt64, "UInt64", "uint64_t", 8, 1, 8, KF_None}, + {NodeKind::Float, "Float", "float", 4, 1, 4, KF_None}, + {NodeKind::Double, "Double", "double", 8, 1, 8, KF_None}, + {NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None}, + {NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None}, + {NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None}, + {NodeKind::Vec2, "Vec2", "Vec2", 8, 2, 4, KF_Vector}, + {NodeKind::Vec3, "Vec3", "Vec3", 12, 3, 4, KF_Vector}, + {NodeKind::Vec4, "Vec4", "Vec4", 16, 4, 4, KF_Vector}, + {NodeKind::Mat4x4, "Mat4x4", "Mat4x4", 64, 4, 4, KF_None}, + {NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String}, + {NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String}, + {NodeKind::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview}, + {NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container}, + {NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container}, }; inline constexpr const KindMeta* kindMeta(NodeKind k) { @@ -100,6 +112,27 @@ inline NodeKind kindFromTypeName(const QString& s, bool* ok = nullptr) { return NodeKind::Hex8; } +inline constexpr uint32_t flagsFor(NodeKind k) { + const auto* m = kindMeta(k); + return m ? m->flags : 0; +} +inline constexpr bool isHexPreview(NodeKind k) { + return flagsFor(k) & KF_HexPreview; +} + +inline QStringList allTypeNamesForUI(bool stripBrackets = false) { + QStringList out; + out.reserve(std::size(kKindMeta)); + for (const auto& m : kKindMeta) { + QString t = QString::fromLatin1(m.typeName); + if (stripBrackets) t.remove(QStringLiteral("[]")); + out << t; + } + out.sort(Qt::CaseInsensitive); + out.removeDuplicates(); + return out; +} + // ── Marker vocabulary ── enum Marker : int { @@ -450,29 +483,36 @@ inline ColumnSpan typeSpanFor(const LineMeta& lm) { inline ColumnSpan nameSpanFor(const LineMeta& lm) { if (lm.isContinuation || lm.lineKind != LineKind::Field) return {}; - // Hex/Padding nodes show ASCII data preview instead of name - switch (lm.nodeKind) { - case NodeKind::Hex8: case NodeKind::Hex16: - case NodeKind::Hex32: case NodeKind::Hex64: - case NodeKind::Padding: - return {}; - default: break; - } + int ind = kFoldCol + lm.depth * 3; int start = ind + kColType + kSepWidth; + + // Hex/Padding: ASCII preview takes the name column position (8 chars) + if (isHexPreview(lm.nodeKind)) + return {start, start + 8, true}; + return {start, start + kColName, true}; } inline ColumnSpan valueSpanFor(const LineMeta& lm, int lineLength) { if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; int ind = kFoldCol + lm.depth * 3; + + // Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes...] + bool isHexPad = isHexPreview(lm.nodeKind); + if (lm.isContinuation) { - int prefixW = kColType + kColName + 4; // 2 seps × 2 chars + int prefixW = isHexPad + ? (kColType + kSepWidth + 8 + kSepWidth) + : (kColType + kColName + 4); int start = ind + prefixW; return {start, lineLength, start < lineLength}; } if (lm.lineKind != LineKind::Field) return {}; - int start = ind + kColType + kSepWidth + kColName + kSepWidth; + + int start = isHexPad + ? (ind + kColType + kSepWidth + 8 + kSepWidth) + : (ind + kColType + kSepWidth + kColName + kSepWidth); return {start, lineLength, start < lineLength}; } @@ -514,6 +554,7 @@ namespace fmt { QString editableValue(const Node& node, const Provider& prov, uint64_t addr, int subLine); QByteArray parseValue(NodeKind kind, const QString& text, bool* ok); + QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok); } // namespace fmt // ── Compose function forward declaration ── diff --git a/src/editor.cpp b/src/editor.cpp index 913e9fd..fe9cec7 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -21,11 +20,14 @@ static const QColor kBgMargin("#252526"); static const QColor kFgMargin("#858585"); static const QColor kFgMarginDim("#505050"); -static constexpr int IND_EDITABLE = 8; -static constexpr int IND_HEX_DIM = 9; +static constexpr int IND_EDITABLE = 8; +static constexpr int IND_HEX_DIM = 9; +static constexpr int IND_HOVER_TOK = 10; + +static QString g_fontName = "Consolas"; static QFont editorFont() { - QFont f("Consolas", 12); + QFont f(g_fontName, 12); f.setFixedPitch(true); return f; } @@ -77,7 +79,14 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { }); connect(m_sci, &QsciScintilla::cursorPositionChanged, - this, [this](int line, int /*col*/) { updateEditableUnderline(line); }); + this, [this](int line, int /*col*/) { updateEditableIndicators(line); }); + + connect(m_sci, &QsciScintilla::textChanged, this, [this]() { + if (!m_editState.active) return; + if (m_editState.target == EditTarget::Value) + validateEditLive(); + updateEditTokenBox(); + }); } void RcxEditor::setupScintilla() { @@ -86,6 +95,10 @@ void RcxEditor::setupScintilla() { m_sci->setReadOnly(true); m_sci->setWrapMode(QsciScintilla::WrapNone); m_sci->setCaretLineVisible(false); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 0); + + // Arrow cursor by default — not the I-beam (this is a structured viewer, not a text editor) + m_sci->viewport()->setCursor(Qt::ArrowCursor); m_sci->setPaper(kBgText); m_sci->setColor(QColor("#d4d4d4")); @@ -104,7 +117,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 link-style indicator (colored text + underline) + // Editable-field link-style indicator (colored text) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_EDITABLE, 17 /*INDIC_TEXTFORE*/); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, @@ -115,6 +128,16 @@ void RcxEditor::setupScintilla() { IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_HEX_DIM, QColor("#505050")); + + // Hovered editable token highlight (subtle background tint, no outline) + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, + IND_HOVER_TOK, 8 /*INDIC_STRAIGHTBOX*/); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, + IND_HOVER_TOK, QColor("#569cd6")); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETALPHA, + IND_HOVER_TOK, (long)35); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETOUTLINEALPHA, + IND_HOVER_TOK, (long)0); } void RcxEditor::setupLexer() { @@ -132,7 +155,7 @@ void RcxEditor::setupLexer() { m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine); m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc); m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Default); - m_lexer->setColor(QColor("#dcdcaa"), QsciLexerCPP::Identifier); + m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Identifier); m_lexer->setColor(QColor("#c586c0"), QsciLexerCPP::PreProcessor); m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator); @@ -144,6 +167,11 @@ void RcxEditor::setupLexer() { m_sci->setLexer(m_lexer); m_sci->setBraceMatching(QsciScintilla::SloppyBraceMatch); + + // Add type names to keyword set 2 → teal coloring (distinct from identifiers) + QByteArray kw2 = allTypeNamesForUI(/*stripBrackets=*/true).join(' ').toLatin1(); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETKEYWORDS, + (uintptr_t)1, kw2.constData()); } void RcxEditor::setupMargins() { @@ -153,7 +181,7 @@ void RcxEditor::setupMargins() { m_sci->setMarginType(0, QsciScintilla::TextMarginRightJustified); m_sci->setMarginWidth(0, " +0x00000000 "); m_sci->setMarginsBackgroundColor(kBgMargin); - m_sci->setMarginsForegroundColor(kFgMargin); + m_sci->setMarginsForegroundColor(kFgMarginDim); m_sci->setMarginSensitivity(0, true); // Margin 1: hidden (fold chevrons moved to text column) @@ -182,15 +210,11 @@ void RcxEditor::setupFolding() { } void RcxEditor::setupMarkers() { - // M_CONT (0): vertical line - m_sci->markerDefine(QsciScintilla::VLine, M_CONT); - m_sci->setMarkerBackgroundColor(kFgMarginDim, M_CONT); - m_sci->setMarkerForegroundColor(kFgMarginDim, M_CONT); + // M_CONT (0): continuation line (metadata only, no visual) + m_sci->markerDefine(QsciScintilla::Invisible, M_CONT); - // M_PAD (1): small rectangle (dim gray) - m_sci->markerDefine(QsciScintilla::SmallRectangle, M_PAD); - m_sci->setMarkerBackgroundColor(QColor("#606060"), M_PAD); - m_sci->setMarkerForegroundColor(QColor("#606060"), M_PAD); + // M_PAD (1): padding line (metadata only, no visual) + m_sci->markerDefine(QsciScintilla::Invisible, M_PAD); // M_PTR0 (2): right triangle (red) m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0); @@ -209,7 +233,7 @@ void RcxEditor::setupMarkers() { // M_STRUCT_BG (5): background tint for struct header/footer m_sci->markerDefine(QsciScintilla::Background, M_STRUCT_BG); - m_sci->setMarkerBackgroundColor(QColor("#1a2332"), M_STRUCT_BG); + m_sci->setMarkerBackgroundColor(QColor("#1a2638"), M_STRUCT_BG); m_sci->setMarkerForegroundColor(QColor("#d4d4d4"), M_STRUCT_BG); // M_HOVER (6): full-row hover highlight @@ -222,7 +246,6 @@ void RcxEditor::setupMarkers() { } void RcxEditor::allocateMarginStyles() { - // Relative indices within margin style offset static constexpr int MSTYLE_NORMAL = 0; static constexpr int MSTYLE_CONT = 1; @@ -231,18 +254,17 @@ void RcxEditor::allocateMarginStyles() { m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLEOFFSET, base); const long bgrMargin = 0x262525; // BGR for #252526 + QByteArray fontName = editorFont().family().toUtf8(); + int fontSize = editorFont().pointSize(); - // Normal offset style: gray on dark - m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE, - (unsigned long)(base + MSTYLE_NORMAL), (long)0x858585); - m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK, - (unsigned long)(base + MSTYLE_NORMAL), bgrMargin); - - // Continuation style: dimmer - m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE, - (unsigned long)(base + MSTYLE_CONT), (long)0x505050); - m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK, - (unsigned long)(base + MSTYLE_CONT), bgrMargin); + for (int s = MSTYLE_NORMAL; s <= MSTYLE_CONT; s++) { + unsigned long abs = (unsigned long)(base + s); + m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE, abs, (long)0x505050); + m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK, abs, bgrMargin); + m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFONT, + (uintptr_t)abs, fontName.constData()); + m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETSIZE, abs, (long)fontSize); + } } void RcxEditor::applyDocument(const ComposeResult& result) { @@ -264,11 +286,11 @@ void RcxEditor::applyDocument(const ComposeResult& result) { applyFoldLevels(result.meta); applyHexDimming(result.meta); - // Re-apply editable underline for current cursor line + // Re-apply editable indicators for current cursor line m_hintLine = -1; int line, col; m_sci->getCursorPosition(&line, &col); - updateEditableUnderline(line); + updateEditableIndicators(line); } void RcxEditor::applyMarginText(const QVector& meta) { @@ -277,10 +299,14 @@ void RcxEditor::applyMarginText(const QVector& meta) { for (int i = 0; i < meta.size(); i++) { const auto& lm = meta[i]; - if (!lm.offsetText.isEmpty()) { - int style = lm.isContinuation ? 1 : 0; - m_sci->setMarginText(i, lm.offsetText, style); - } + if (lm.offsetText.isEmpty()) continue; + + QByteArray text = lm.offsetText.toUtf8(); + m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETTEXT, + (uintptr_t)i, text.constData()); + QByteArray styles(text.size(), '\0'); // style 0 = dim + m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLES, + (uintptr_t)i, styles.constData()); } } @@ -311,19 +337,37 @@ static inline void lineRangeNoEol(QsciScintilla* sci, int line, long& start, lon len = (end > start) ? (end - start) : 0; } +// UTF-8 safe column-to-position conversion +static inline long posFromCol(QsciScintilla* sci, int line, int col) { + return sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN, + (unsigned long)line, (long)col); +} + +void RcxEditor::clearIndicatorLine(int indic, int line) { + if (line < 0) return; + long start, len; + lineRangeNoEol(m_sci, line, start, len); + if (len <= 0) return; + m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, indic); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, start, len); +} + +void RcxEditor::fillIndicatorCols(int indic, int line, int colA, int colB) { + long a = posFromCol(m_sci, line, colA); + long b = posFromCol(m_sci, line, colB); + if (b > a) { + m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, indic); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, a, b - a); + } +} + void RcxEditor::applyHexDimming(const QVector& meta) { m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM); for (int i = 0; i < meta.size(); i++) { - switch (meta[i].nodeKind) { - case NodeKind::Hex8: case NodeKind::Hex16: - case NodeKind::Hex32: case NodeKind::Hex64: - case NodeKind::Padding: { + if (isHexPreview(meta[i].nodeKind)) { long pos, len; lineRangeNoEol(m_sci, i, pos, len); if (len > 0) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len); - break; - } - default: break; } } } @@ -331,10 +375,27 @@ void RcxEditor::applyHexDimming(const QVector& meta) { void RcxEditor::applySelectionOverlay(const QSet& selIds) { m_currentSelIds = selIds; m_sci->markerDeleteAll(M_SELECTED); + + // Clear all editable indicators, then repaint for selected + cursor line + long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen); + for (int i = 0; i < m_meta.size(); i++) { - if (selIds.contains(m_meta[i].nodeId)) + if (selIds.contains(m_meta[i].nodeId)) { m_sci->markerAdd(i, M_SELECTED); + paintEditableSpans(i); + } } + + // Also paint cursor line (even if not selected) + if (!m_editState.active) { + int curLine, col; + m_sci->getCursorPosition(&curLine, &col); + paintEditableSpans(curLine); + m_hintLine = curLine; + } + applyHoverHighlight(); } @@ -361,7 +422,12 @@ ViewState RcxEditor::saveViewState() const { } void RcxEditor::restoreViewState(const ViewState& vs) { - m_sci->setCursorPosition(vs.cursorLine, vs.cursorCol); + int maxLine = std::max(0, m_sci->lines() - 1); + int line = std::clamp(vs.cursorLine, 0, maxLine); + long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN, + (unsigned long)line, + (long)std::max(0, vs.cursorCol)); + m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, (unsigned long)pos); m_sci->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE, (unsigned long)vs.scrollLine); } @@ -420,9 +486,19 @@ static QString getLineText(QsciScintilla* sci, int line) { // ── Shared inline-edit shutdown ── RcxEditor::EndEditInfo RcxEditor::endInlineEdit() { + // Clear edit token box and reset indicator color + clearIndicatorLine(IND_HOVER_TOK, m_editState.line); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, + IND_HOVER_TOK, QColor("#569cd6")); EndEditInfo info{m_editState.nodeIdx, m_editState.subLine, m_editState.target}; m_editState.active = false; m_sci->setReadOnly(true); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 0); + if (m_cursorOverridden) { + QApplication::restoreOverrideCursor(); + m_cursorOverridden = false; + } + 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); @@ -483,6 +559,47 @@ RcxEditor::NormalizedSpan RcxEditor::normalizeSpan( return {start + lead, start + trail, true}; } +bool RcxEditor::resolvedSpanFor(int line, EditTarget t, + NormalizedSpan& out, QString* lineTextOut) const { + const LineMeta* lm = metaForLine(line); + if (!lm || lm->nodeIdx < 0) return false; + + QString lineText = getLineText(m_sci, line); + int textLen = lineText.size(); + + ColumnSpan s; + switch (t) { + case EditTarget::Type: s = typeSpan(*lm); break; + case EditTarget::Name: s = nameSpan(*lm); break; + case EditTarget::Value: s = valueSpan(*lm, textLen); break; + } + + if (!s.valid && t == EditTarget::Name) + s = headerNameSpan(*lm, lineText); + + out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/true); + if (lineTextOut) *lineTextOut = lineText; + return out.valid; +} + +// ── Point → line/col/nodeId resolution ── + +RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const { + HitInfo h; + long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, + (unsigned long)vp.x(), (long)vp.y()); + if (pos < 0) return h; + h.line = (int)m_sci->SendScintilla( + QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); + h.col = (int)m_sci->SendScintilla( + QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos); + if (h.line >= 0 && h.line < m_meta.size()) { + h.nodeId = m_meta[h.line].nodeId; + h.inFoldCol = (h.col < kFoldCol && m_meta[h.line].foldHead); + } + return h; +} + // ── Double-click hit test ── static bool hitTestTarget(QsciScintilla* sci, @@ -532,26 +649,18 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { } if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonPress && m_editState.active) { - // Only commit if click is outside the active edit span auto* me = static_cast(event); - long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, - (unsigned long)me->pos().x(), (long)me->pos().y()); + auto h = hitTest(me->pos()); bool insideEdit = false; - if (pos >= 0) { - int clickLine = (int)m_sci->SendScintilla( - QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); - int clickCol = (int)m_sci->SendScintilla( - QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos); - if (clickLine == m_editState.line) { - QString lineText = getLineText(m_sci, m_editState.line); - int delta = lineText.size() - m_editState.linelenAfterReplace; - int editEnd = m_editState.spanStart + m_editState.original.size() + delta; - insideEdit = (clickCol >= m_editState.spanStart && clickCol < editEnd); - } + if (h.line == m_editState.line) { + int editEnd = editEndCol(); + insideEdit = (h.col >= m_editState.spanStart && h.col <= editEnd); } - if (!insideEdit) - commitInlineEdit(); - return false; // always let click through to Scintilla + if (insideEdit) + return false; // inside edit span: let Scintilla position cursor + commitInlineEdit(); + m_currentSelIds.clear(); // stale — normal handler will re-establish + // Fall through to normal click handler below } // Single-click on fold column (" - " / " + ") toggles fold // Other left-clicks emit nodeClicked for selection @@ -559,27 +668,39 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { && event->type() == QEvent::MouseButtonPress) { auto* me = static_cast(event); if (me->button() == Qt::LeftButton) { - long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, - (unsigned long)me->pos().x(), (long)me->pos().y()); - if (pos >= 0) { - int line = (int)m_sci->SendScintilla( - QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); - int col = (int)m_sci->SendScintilla( - QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos); - if (col < kFoldCol && line >= 0 && line < m_meta.size() - && m_meta[line].foldHead) { - emit marginClicked(0, line, me->modifiers()); - return true; - } - // Selection click — emit for controller to manage - if (line >= 0 && line < m_meta.size()) { - uint64_t nid = m_meta[line].nodeId; - if (nid != 0) { - emit nodeClicked(line, nid, me->modifiers()); - m_dragging = true; - m_dragLastLine = line; + auto h = hitTest(me->pos()); + if (h.inFoldCol) { + emit marginClicked(0, h.line, me->modifiers()); + return true; + } + if (h.nodeId != 0) { + bool alreadySelected = m_currentSelIds.contains(h.nodeId); + bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier)); + + // Single-click on editable token of already-selected node → edit + if (alreadySelected && plain) { + int tLine; EditTarget t; + if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) { + m_pendingClickNodeId = 0; + return beginInlineEdit(t, tLine); } } + + m_dragging = true; + m_dragLastLine = h.line; + m_dragInitMods = me->modifiers(); + + bool multi = m_currentSelIds.size() > 1; + + if (alreadySelected && multi && plain) { + // Defer: might be start of double-click-to-edit + m_pendingClickNodeId = h.nodeId; + m_pendingClickLine = h.line; + m_pendingClickMods = me->modifiers(); + } else { + emit nodeClicked(h.line, h.nodeId, me->modifiers()); + m_pendingClickNodeId = 0; + } } } } @@ -588,18 +709,16 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { && event->type() == QEvent::MouseMove && m_dragging) { auto* me = static_cast(event); if (me->buttons() & Qt::LeftButton) { - long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, - (unsigned long)me->pos().x(), (long)me->pos().y()); - if (pos >= 0) { - int line = (int)m_sci->SendScintilla( - QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); - if (line >= 0 && line < m_meta.size() && line != m_dragLastLine) { - uint64_t nid = m_meta[line].nodeId; - if (nid != 0) { - emit nodeClicked(line, nid, Qt::ShiftModifier); - m_dragLastLine = line; - } - } + // Flush deferred click before extending drag + if (m_pendingClickNodeId != 0) { + emit nodeClicked(m_pendingClickLine, m_pendingClickNodeId, + m_pendingClickMods); + m_pendingClickNodeId = 0; + } + auto h = hitTest(me->pos()); + if (h.line >= 0 && h.line != m_dragLastLine && h.nodeId != 0) { + emit nodeClicked(h.line, h.nodeId, m_dragInitMods | Qt::ShiftModifier); + m_dragLastLine = h.line; } } else { m_dragging = false; @@ -607,13 +726,20 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { } if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonRelease) { m_dragging = false; + if (m_pendingClickNodeId != 0) { + emit nodeClicked(m_pendingClickLine, m_pendingClickNodeId, + m_pendingClickMods); + m_pendingClickNodeId = 0; + } } if (obj == m_sci->viewport() && !m_editState.active && event->type() == QEvent::MouseButtonDblClick) { auto* me = static_cast(event); int line; EditTarget t; - if (hitTestTarget(m_sci, m_meta, me->pos(), line, t)) + if (hitTestTarget(m_sci, m_meta, me->pos(), line, t)) { + m_pendingClickNodeId = 0; // cancel deferred selection change return beginInlineEdit(t, line); + } } if (obj == m_sci && event->type() == QEvent::FocusOut) { auto* fe = static_cast(event); @@ -626,18 +752,14 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { commitInlineEdit(); }); } - // Clear underlines when editor loses focus - if (m_hintLine >= 0) { - long start, len; lineRangeNoEol(m_sci, m_hintLine, start, len); - m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE); - m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, start, len); - m_hintLine = -1; - } + // Clear editable indicators when editor loses focus + clearIndicatorLine(IND_EDITABLE, m_hintLine); + m_hintLine = -1; } if (obj == m_sci && event->type() == QEvent::FocusIn) { int line, col; m_sci->getCursorPosition(&line, &col); - updateEditableUnderline(line); + updateEditableIndicators(line); } if (obj == m_sci->viewport() && !m_editState.active) { if (event->type() == QEvent::MouseMove) { @@ -654,16 +776,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { // Resolve hovered nodeId on move/wheel if (event->type() == QEvent::MouseMove || event->type() == QEvent::Wheel) { - long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, - (unsigned long)m_lastHoverPos.x(), - (long)m_lastHoverPos.y()); - uint64_t newHoverId = 0; - if (pos >= 0 && m_hoverInside) { - int hLine = (int)m_sci->SendScintilla( - QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); - if (hLine >= 0 && hLine < m_meta.size()) - newHoverId = m_meta[hLine].nodeId; - } + auto h = hitTest(m_lastHoverPos); + uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0; if (newHoverId != m_hoveredNodeId) { m_hoveredNodeId = newHoverId; applyHoverHighlight(); @@ -704,22 +818,17 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { case Qt::Key_Return: case Qt::Key_Enter: case Qt::Key_Tab: - if (autocActive) { - if (m_editState.target == EditTarget::Type) { - // Extract selected typeName directly from autocomplete - QByteArray buf(256, '\0'); - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCGETCURRENTTEXT, - (unsigned long)256, (void*)buf.data()); - QString selectedType = QString::fromUtf8(buf.constData()); - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL); + if (autocActive && m_editState.target == EditTarget::Type) { + // Extract selected typeName directly from autocomplete + QByteArray buf(256, '\0'); + m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCGETCURRENTTEXT, + (unsigned long)256, (void*)buf.data()); + QString selectedType = QString::fromUtf8(buf.constData()); + m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL); - auto info = endInlineEdit(); - emit inlineEditCommitted(info.nodeIdx, info.subLine, EditTarget::Type, selectedType); - return true; - } - // Other targets: let Scintilla complete, then auto-commit - QTimer::singleShot(0, this, &RcxEditor::commitInlineEdit); - return false; + auto info = endInlineEdit(); + emit inlineEditCommitted(info.nodeIdx, info.subLine, EditTarget::Type, selectedType); + return true; } commitInlineEdit(); return true; @@ -745,9 +854,18 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { if (col <= m_editState.spanStart) return true; return false; } + case Qt::Key_Right: { + int line, col; + m_sci->getCursorPosition(&line, &col); + if (col >= editEndCol()) return true; // block past end + return false; + } case Qt::Key_Home: m_sci->setCursorPosition(m_editState.line, m_editState.spanStart); return true; + case Qt::Key_End: + m_sci->setCursorPosition(m_editState.line, editEndCol()); + return true; default: return false; } @@ -763,6 +881,12 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { } m_hoveredNodeId = 0; applyHoverHighlight(); + // Clear hover token box (will be repainted as edit token box below) + clearIndicatorLine(IND_HOVER_TOK, m_hoverTokLine); + m_hoverTokLine = -1; + // Clear editable-token color hints (de-emphasize non-active tokens) + clearIndicatorLine(IND_EDITABLE, m_hintLine); + m_hintLine = -1; if (line >= 0) { m_sci->setCursorPosition(line, 0); @@ -772,21 +896,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { auto* lm = metaForLine(line); if (!lm || lm->nodeIdx < 0) return false; - QString lineText = getLineText(m_sci, line); - int textLen = lineText.size(); - - ColumnSpan span; - switch (target) { - case EditTarget::Type: span = typeSpan(*lm); break; - case EditTarget::Name: span = nameSpan(*lm); break; - case EditTarget::Value: span = valueSpan(*lm, textLen); break; - } - - if (!span.valid && target == EditTarget::Name) - span = headerNameSpan(*lm, lineText); - - auto norm = normalizeSpan(span, lineText, target, /*skipPrefixes=*/true); - if (!norm.valid) return false; + QString lineText; + NormalizedSpan norm; + if (!resolvedSpanFor(line, target, norm, &lineText)) return false; QString trimmed = lineText.mid(norm.start, norm.end - norm.start); @@ -797,23 +909,30 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { m_editState.target = target; m_editState.spanStart = norm.start; m_editState.original = trimmed; - m_editState.linelenAfterReplace = textLen; + m_editState.linelenAfterReplace = lineText.size(); + m_editState.editKind = lm->nodeKind; + if ((lm->nodeKind == NodeKind::Vec2 || lm->nodeKind == NodeKind::Vec3 || + lm->nodeKind == NodeKind::Vec4) && lm->subLine > 0) + m_editState.editKind = NodeKind::Float; // Disable Scintilla undo during inline edit m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1); m_sci->setReadOnly(false); + QApplication::setOverrideCursor(Qt::IBeamCursor); + m_cursorOverridden = true; // Re-enable selection rendering for inline edit m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1, QColor("#264f78")); - // Select just the trimmed text (keeps columns aligned) long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line); - long posSelStart = lineStart + m_editState.spanStart; - long posSelEnd = posSelStart + trimmed.toUtf8().size(); - m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posSelStart, posSelEnd); + long posStart = lineStart + m_editState.spanStart; + long posEnd = posStart + trimmed.toUtf8().size(); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posEnd, posEnd); + updateEditTokenBox(); if (target == EditTarget::Type) QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete); @@ -821,6 +940,21 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { return true; } +void RcxEditor::updateEditTokenBox() { + clearIndicatorLine(IND_HOVER_TOK, m_editState.line); + + int endCol = editEndCol(); + if (endCol <= m_editState.spanStart) return; + + fillIndicatorCols(IND_HOVER_TOK, m_editState.line, m_editState.spanStart, endCol); +} + +int RcxEditor::editEndCol() const { + QString lineText = getLineText(m_sci, m_editState.line); + int delta = lineText.size() - m_editState.linelenAfterReplace; + return m_editState.spanStart + m_editState.original.size() + delta; +} + // ── Commit inline edit ── void RcxEditor::commitInlineEdit() { @@ -861,12 +995,7 @@ void RcxEditor::showTypeAutocomplete() { m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, posStart); // Build list from typeName (matches what the editor displays) - QStringList types; - for (const auto& m : kKindMeta) - types << m.typeName; - types.sort(Qt::CaseInsensitive); - - QByteArray list = types.join(QChar(' ')).toUtf8(); + QByteArray list = allTypeNamesForUI().join(' ').toUtf8(); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' '); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETIGNORECASE, (long)1); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETDROPRESTOFWORD, (long)1); @@ -878,64 +1007,44 @@ void RcxEditor::showTypeAutocomplete() { m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSELECT, (uintptr_t)0, cur.constData()); - long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS); - int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION, 0, pos); - int y = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION, 0, pos); - QToolTip::showText( - m_sci->viewport()->mapToGlobal(QPoint(x, y + 20)), - QStringLiteral("Type to filter \u2022 \u2191/\u2193 select \u2022 Enter apply \u2022 Esc cancel"), - m_sci); } -// ── Editable-field underline indicator ── +// ── Editable-field text-color indicator ── -void RcxEditor::updateEditableUnderline(int line) { +void RcxEditor::paintEditableSpans(int line) { + NormalizedSpan norm; + for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value}) { + if (resolvedSpanFor(line, t, norm)) + fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); + } +} + +void RcxEditor::updateEditableIndicators(int line) { if (m_editState.active) return; if (line == m_hintLine) return; - auto clearLine = [&](int l) { - if (l < 0) return; - long start, len; lineRangeNoEol(m_sci, l, start, len); - m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE); - m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, start, len); - }; + // Clear old cursor line (only if not a selected node) + if (m_hintLine >= 0) { + const LineMeta* oldLm = metaForLine(m_hintLine); + if (!oldLm || !m_currentSelIds.contains(oldLm->nodeId)) + clearIndicatorLine(IND_EDITABLE, m_hintLine); + } - clearLine(m_hintLine); m_hintLine = line; - - const LineMeta* lm = metaForLine(line); - if (!lm || lm->nodeIdx < 0) return; - - QString lineText = getLineText(m_sci, line); - int textLen = lineText.size(); - - ColumnSpan ts = typeSpan(*lm); - ColumnSpan ns = nameSpan(*lm); - ColumnSpan vs = valueSpan(*lm, textLen); - - if (!ns.valid) - ns = headerNameSpan(*lm, lineText); - - auto underlineSpan = [&](ColumnSpan s, EditTarget tgt) { - auto norm = normalizeSpan(s, lineText, tgt, /*skipPrefixes=*/true); - if (!norm.valid) return; - - long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line); - m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE); - m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, - lineStart + norm.start, norm.end - norm.start); - }; - - underlineSpan(ts, EditTarget::Type); - underlineSpan(ns, EditTarget::Name); - underlineSpan(vs, EditTarget::Value); + paintEditableSpans(line); } // ── Hover cursor (coalesced) ── void RcxEditor::applyHoverCursor() { + auto clearHoverTok = [&]() { + clearIndicatorLine(IND_HOVER_TOK, m_hoverTokLine); + m_hoverTokLine = -1; + }; + if (m_editState.active || !m_hoverInside || !m_sci->viewport()->underMouse()) { + clearHoverTok(); if (m_cursorOverridden) { QApplication::restoreOverrideCursor(); m_cursorOverridden = false; @@ -944,22 +1053,26 @@ void RcxEditor::applyHoverCursor() { } int line; EditTarget t; - bool interactive = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t); + bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t); // Also show pointer cursor for fold column on fold-head lines + bool interactive = tokenHit; if (!interactive) { - long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, - (unsigned long)m_lastHoverPos.x(), - (long)m_lastHoverPos.y()); - if (pos >= 0) { - int hLine = (int)m_sci->SendScintilla( - QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); - int hCol = (int)m_sci->SendScintilla( - QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos); - if (hCol < kFoldCol && hLine >= 0 && hLine < m_meta.size() - && m_meta[hLine].foldHead) - interactive = true; - } + auto h = hitTest(m_lastHoverPos); + if (h.inFoldCol) interactive = true; + } + + // Token box highlight + if (!tokenHit) { + clearHoverTok(); + } else if (line != m_hoverTokLine || t != m_hoverTokTarget) { + clearHoverTok(); + m_hoverTokLine = line; + m_hoverTokTarget = t; + + NormalizedSpan norm; + if (resolvedSpanFor(line, t, norm)) + fillIndicatorCols(IND_HOVER_TOK, line, norm.start, norm.end); } if (interactive && !m_cursorOverridden) { @@ -971,4 +1084,40 @@ void RcxEditor::applyHoverCursor() { } } +// ── Live value validation ── + +void RcxEditor::validateEditLive() { + QString lineText = getLineText(m_sci, m_editState.line); + int delta = lineText.size() - m_editState.linelenAfterReplace; + int editedLen = m_editState.original.size() + delta; + QString text = (editedLen > 0) + ? lineText.mid(m_editState.spanStart, editedLen).trimmed() : QString(); + bool ok; + fmt::parseValue(m_editState.editKind, text, &ok); + showEditValidation(ok); +} + +void RcxEditor::showEditValidation(bool valid) { + QColor c = valid ? QColor("#569cd6") : QColor("#e05050"); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_HOVER_TOK, c); +} + +void RcxEditor::setEditorFont(const QString& fontName) { + g_fontName = fontName; + QFont f = editorFont(); + + m_sci->setFont(f); + m_lexer->setFont(f); + for (int i = 0; i <= 127; i++) + m_lexer->setFont(f, i); + m_sci->setMarginsFont(f); + + // Re-apply margin styles with new font + allocateMarginStyles(); +} + +void RcxEditor::setGlobalFontName(const QString& fontName) { + g_fontName = fontName; +} + } // namespace rcx diff --git a/src/editor.h b/src/editor.h index 3f6afef..6b94b7a 100644 --- a/src/editor.h +++ b/src/editor.h @@ -37,6 +37,8 @@ public: void cancelInlineEdit(); void applySelectionOverlay(const QSet& selIds); + void setEditorFont(const QString& fontName); + static void setGlobalFontName(const QString& fontName); signals: void marginClicked(int margin, int line, Qt::KeyboardModifiers mods); @@ -63,10 +65,17 @@ private: bool m_cursorOverridden = false; uint64_t m_hoveredNodeId = 0; QSet m_currentSelIds; - + int m_hoverTokLine = -1; + EditTarget m_hoverTokTarget = EditTarget::Name; // ── Drag selection ── bool m_dragging = false; int m_dragLastLine = -1; + Qt::KeyboardModifiers m_dragInitMods = Qt::NoModifier; + + // ── Deferred click (protects multi-select on double-click) ── + uint64_t m_pendingClickNodeId = 0; + int m_pendingClickLine = -1; + Qt::KeyboardModifiers m_pendingClickMods = Qt::NoModifier; // ── Inline edit state ── struct InlineEditState { @@ -78,6 +87,7 @@ private: int spanStart = 0; int linelenAfterReplace = 0; QString original; + NodeKind editKind = NodeKind::Int32; }; InlineEditState m_editState; @@ -94,20 +104,34 @@ private: void applyHexDimming(const QVector& meta); void commitInlineEdit(); + int editEndCol() const; bool handleNormalKey(QKeyEvent* ke); bool handleEditKey(QKeyEvent* ke); void showTypeAutocomplete(); - void updateEditableUnderline(int line); + void paintEditableSpans(int line); + void updateEditableIndicators(int line); void applyHoverCursor(); void applyHoverHighlight(); + void updateEditTokenBox(); + void validateEditLive(); + void showEditValidation(bool valid); // ── Refactored helpers ── + struct HitInfo { int line = -1; int col = -1; uint64_t nodeId = 0; bool inFoldCol = false; }; + HitInfo hitTest(const QPoint& viewportPos) const; + struct EndEditInfo { int nodeIdx; int subLine; EditTarget target; }; EndEditInfo endInlineEdit(); struct NormalizedSpan { int start = 0; int end = 0; bool valid = false; }; NormalizedSpan normalizeSpan(const ColumnSpan& raw, const QString& lineText, EditTarget target, bool skipPrefixes) const; + + // ── Indicator helpers (dedupe + UTF-8 safe) ── + void clearIndicatorLine(int indic, int line); + void fillIndicatorCols(int indic, int line, int colA, int colB); + bool resolvedSpanFor(int line, EditTarget t, NormalizedSpan& out, + QString* lineTextOut = nullptr) const; }; } // namespace rcx diff --git a/src/fonts/Iosevka-Regular.ttf b/src/fonts/Iosevka-Regular.ttf new file mode 100644 index 0000000..43947d1 Binary files /dev/null and b/src/fonts/Iosevka-Regular.ttf differ diff --git a/src/format.cpp b/src/format.cpp index 16f3653..59bc8da 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -47,10 +47,10 @@ QString fmtInt8(int8_t v) { return QString::number(v); } QString fmtInt16(int16_t v) { return QString::number(v); } QString fmtInt32(int32_t v) { return QString::number(v); } QString fmtInt64(int64_t v) { return QString::number(v); } -QString fmtUInt8(uint8_t v) { return QString::number(v); } -QString fmtUInt16(uint16_t v) { return QString::number(v); } -QString fmtUInt32(uint32_t v) { return QString::number(v); } -QString fmtUInt64(uint64_t v) { return QString::number(v); } +QString fmtUInt8(uint8_t v) { return hexStr(v, 2); } +QString fmtUInt16(uint16_t v) { return hexStr(v, 4); } +QString fmtUInt32(uint32_t v) { return hexStr(v, 8); } +QString fmtUInt64(uint64_t v) { return hexStr(v, 16); } QString fmtFloat(float v) { return QString::number(v, 'f', 3); } QString fmtDouble(double v) { return QString::number(v, 'f', 6); } @@ -87,9 +87,10 @@ QString fmtStructHeader(const Node& node, int depth) { } QString fmtStructFooter(const Node& node, int depth, int totalSize) { - QString s = indent(depth) + QStringLiteral("}; // ") + node.name; + QString s = indent(depth) + QStringLiteral("};"); if (totalSize > 0) - s += QStringLiteral(" sizeof=0x") + QString::number(totalSize, 16).toUpper(); + s += QStringLiteral(" // sizeof(") + node.name + QStringLiteral(")=0x") + + QString::number(totalSize, 16).toUpper(); return s; } @@ -225,10 +226,7 @@ QString fmtNodeLine(const Node& node, const Provider& prov, } // Hex nodes and Padding: ASCII preview + hex bytes (compact) - if (node.kind == NodeKind::Hex8 || node.kind == NodeKind::Hex16 || - node.kind == NodeKind::Hex32 || node.kind == NodeKind::Hex64 || - node.kind == NodeKind::Padding) - { + if (isHexPreview(node.kind)) { if (node.kind == NodeKind::Padding) { const int totalSz = qMax(1, node.arrayLen); const int lineOff = subLine * 8; @@ -276,6 +274,36 @@ static QString stripHex(const QString& s) { return s; } +// Parse ASCII text into raw byte array (each char becomes a byte) +QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok) { + *ok = false; + if (text.size() != expectedSize) return {}; + QByteArray result(expectedSize, Qt::Uninitialized); + for (int i = 0; i < expectedSize; i++) { + uint c = text[i].unicode(); + if (c > 255) return {}; // Non-Latin1 character + result[i] = (char)c; + } + *ok = true; + return result; +} + +// Parse space-separated hex byte string into raw byte array (no endian conversion) +static QByteArray parseHexBytes(const QString& s, int expectedSize, bool* ok) { + QString clean = s; + clean.remove(' '); + if (clean.size() != expectedSize * 2) { *ok = false; return {}; } + QByteArray result(expectedSize, Qt::Uninitialized); + for (int i = 0; i < expectedSize; i++) { + bool byteOk; + uint byte = clean.mid(i * 2, 2).toUInt(&byteOk, 16); + if (!byteOk) { *ok = false; return {}; } + result[i] = (char)byte; + } + *ok = true; + return result; +} + // Range-checked narrowing: sets *ok = false if parsed value doesn't fit in T template static QByteArray parseIntChecked(ParseT val, bool* ok) { @@ -304,18 +332,18 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) { } switch (kind) { - case NodeKind::Hex8: { uint val = stripHex(s).remove(' ').toUInt(ok, 16); return parseIntChecked(val, ok); } - case NodeKind::Hex16: { uint val = stripHex(s).remove(' ').toUInt(ok, 16); return parseIntChecked(val, ok); } - case NodeKind::Hex32: { uint val = stripHex(s).remove(' ').toUInt(ok, 16); return *ok ? toBytes(val) : QByteArray{}; } - case NodeKind::Hex64: { qulonglong val = stripHex(s).remove(' ').toULongLong(ok, 16); return *ok ? toBytes(val) : QByteArray{}; } - case NodeKind::Int8: { int val = s.toInt(ok); return parseIntChecked(val, ok); } - case NodeKind::Int16: { int val = s.toInt(ok); return parseIntChecked(val, ok); } - case NodeKind::Int32: { int val = s.toInt(ok); return *ok ? toBytes(val) : QByteArray{}; } - case NodeKind::Int64: { qlonglong val = s.toLongLong(ok); return *ok ? toBytes(val) : QByteArray{}; } - case NodeKind::UInt8: { uint val = s.toUInt(ok); return parseIntChecked(val, ok); } - case NodeKind::UInt16: { uint val = s.toUInt(ok); return parseIntChecked(val, ok); } - case NodeKind::UInt32: { uint val = s.toUInt(ok); return *ok ? toBytes(val) : QByteArray{}; } - case NodeKind::UInt64: { qulonglong val = s.toULongLong(ok); return *ok ? toBytes(val) : QByteArray{}; } + case NodeKind::Hex8: return parseHexBytes(stripHex(s), 1, ok); + case NodeKind::Hex16: return parseHexBytes(stripHex(s), 2, ok); + case NodeKind::Hex32: return parseHexBytes(stripHex(s), 4, ok); + case NodeKind::Hex64: return parseHexBytes(stripHex(s), 8, ok); + case NodeKind::Int8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return parseIntChecked(val, ok); } + case NodeKind::Int16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return parseIntChecked(val, ok); } + case NodeKind::Int32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return *ok ? toBytes(val) : QByteArray{}; } + case NodeKind::Int64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qlonglong val = stripHex(s).toLongLong(ok,b); return *ok ? toBytes(val) : QByteArray{}; } + case NodeKind::UInt8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked(val, ok); } + case NodeKind::UInt16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked(val, ok); } + case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return *ok ? toBytes(val) : QByteArray{}; } + case NodeKind::UInt64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return *ok ? toBytes(val) : QByteArray{}; } case NodeKind::Float: { float val = s.toFloat(ok); return *ok ? toBytes(val) : QByteArray{}; diff --git a/src/main.cpp b/src/main.cpp index 7404ba7..b879d25 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,10 +12,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #ifdef _WIN32 #include @@ -115,6 +118,7 @@ private slots: void undo(); void redo(); void about(); + void setEditorFont(const QString& fontName); private: QMdiArea* m_mdiArea; @@ -175,6 +179,23 @@ void MainWindow::createMenus() { auto* view = menuBar()->addMenu("&View"); view->addAction("Split &Horizontal", this, &MainWindow::splitView); view->addAction("&Unsplit", this, &MainWindow::unsplitView); + view->addSeparator(); + auto* fontMenu = view->addMenu("&Font"); + auto* fontGroup = new QActionGroup(this); + fontGroup->setExclusive(true); + auto* actConsolas = fontMenu->addAction("Consolas"); + actConsolas->setCheckable(true); + actConsolas->setActionGroup(fontGroup); + auto* actIosevka = fontMenu->addAction("Iosevka"); + actIosevka->setCheckable(true); + actIosevka->setActionGroup(fontGroup); + // Load saved preference + QSettings settings("ReclassX", "ReclassX"); + QString savedFont = settings.value("font", "Consolas").toString(); + if (savedFont == "Iosevka") actIosevka->setChecked(true); + else actConsolas->setChecked(true); + connect(actConsolas, &QAction::triggered, this, [this]() { setEditorFont("Consolas"); }); + connect(actIosevka, &QAction::triggered, this, [this]() { setEditorFont("Iosevka"); }); // Node auto* node = menuBar()->addMenu("&Node"); @@ -548,6 +569,15 @@ void MainWindow::about() { "fold markers, and status flags."); } +void MainWindow::setEditorFont(const QString& fontName) { + QSettings settings("ReclassX", "ReclassX"); + settings.setValue("font", fontName); + // Notify all controllers to refresh fonts + for (auto& state : m_tabs) { + state.ctrl->setEditorFont(fontName); + } +} + RcxController* MainWindow::activeController() const { auto* sub = m_mdiArea->activeSubWindow(); if (sub && m_tabs.contains(sub)) @@ -589,6 +619,18 @@ int main(int argc, char* argv[]) { app.setOrganizationName("ReclassX"); app.setStyle("Fusion"); // Fusion style respects dark palette well + // Load embedded Iosevka font + int fontId = QFontDatabase::addApplicationFont(":/fonts/Iosevka-Regular.ttf"); + if (fontId == -1) + qWarning("Failed to load embedded Iosevka font"); + + // Apply saved font preference before creating any editors + { + QSettings settings("ReclassX", "ReclassX"); + QString savedFont = settings.value("font", "Consolas").toString(); + rcx::RcxEditor::setGlobalFontName(savedFont); + } + // Global dark palette QPalette darkPalette; darkPalette.setColor(QPalette::Window, QColor("#1e1e1e")); @@ -611,7 +653,7 @@ int main(int argc, char* argv[]) { bool screenshotMode = app.arguments().contains("--screenshot"); if (screenshotMode) - window.setAttribute(Qt::WA_DontShowOnScreen); + window.setWindowOpacity(0.0); window.show(); // Always auto-open PE header demo on startup @@ -623,10 +665,10 @@ int main(int argc, char* argv[]) { if (idx + 1 < app.arguments().size()) out = app.arguments().at(idx + 1); - QTimer::singleShot(1000, [&window, &app, out]() { + QTimer::singleShot(1000, [&window, out]() { QDir().mkpath(QFileInfo(out).absolutePath()); window.grab().save(out); - app.quit(); + ::_exit(0); // immediate exit — no need for clean shutdown in screenshot mode }); } diff --git a/src/icons.qrc b/src/resources.qrc similarity index 62% rename from src/icons.qrc rename to src/resources.qrc index 502050b..d037c3d 100644 --- a/src/icons.qrc +++ b/src/resources.qrc @@ -3,4 +3,7 @@ icons/chevron-right.png icons/chevron-down.png + + fonts/Iosevka-Regular.ttf + diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index 65e775b..fa8f17c 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -641,9 +641,9 @@ private slots: int lastLine = result.meta.size() - 1; QCOMPARE(result.meta[lastLine].lineKind, LineKind::Footer); - // Footer text should contain sizeof=0xC (4+8=12=0xC) + // Footer text should contain sizeof(Sized)=0xC (4+8=12=0xC) QString footerText = result.text.split('\n').last(); - QVERIFY(footerText.contains("sizeof=0xC")); + QVERIFY(footerText.contains("sizeof(Sized)=0xC")); } void testLineMetaHasNodeId() { @@ -668,6 +668,116 @@ private slots: QCOMPARE(result.meta[i].nodeId, tree.nodes[ni].id); } } + + void testSizeofUpdatesAfterDelete() { + // Test that sizeof recalculates after deleting a node + NodeTree tree; + tree.baseAddress = 0; + + Node root; + root.kind = NodeKind::Struct; + root.name = "Test"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + Node f1; + f1.kind = NodeKind::UInt32; + f1.name = "a"; + f1.parentId = rootId; + f1.offset = 0; + tree.addNode(f1); + + Node f2; + f2.kind = NodeKind::UInt64; + f2.name = "b"; + f2.parentId = rootId; + f2.offset = 4; + int f2i = tree.addNode(f2); + uint64_t f2Id = tree.nodes[f2i].id; + + NullProvider prov; + + // First compose: sizeof should be 0xC (4+8=12) + ComposeResult result1 = compose(tree, prov); + QString footer1 = result1.text.split('\n').last(); + QVERIFY2(footer1.contains("sizeof(Test)=0xC"), + qPrintable("Before delete: " + footer1)); + + // Delete the second field + int idx = tree.indexOfId(f2Id); + QVERIFY(idx >= 0); + tree.nodes.remove(idx); + tree.invalidateIdCache(); + + // Second compose: sizeof should be 0x4 (only UInt32 remains) + ComposeResult result2 = compose(tree, prov); + QString footer2 = result2.text.split('\n').last(); + QVERIFY2(footer2.contains("sizeof(Test)=0x4"), + qPrintable("After delete: " + footer2)); + } + + void testNestedStructSizeofUpdates() { + // Test nested struct sizeof updates when child is deleted + NodeTree tree; + tree.baseAddress = 0; + + // Root struct + Node root; + root.kind = NodeKind::Struct; + root.name = "Root"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + // Nested struct (like IMAGE_FILE_HEADER) + Node nested; + nested.kind = NodeKind::Struct; + nested.name = "Nested"; + nested.parentId = rootId; + nested.offset = 0; + int ni = tree.addNode(nested); + uint64_t nestedId = tree.nodes[ni].id; + + // Field in nested struct + Node f1; + f1.kind = NodeKind::UInt32; + f1.name = "a"; + f1.parentId = nestedId; + f1.offset = 0; + tree.addNode(f1); + + Node f2; + f2.kind = NodeKind::UInt32; + f2.name = "b"; + f2.parentId = nestedId; + f2.offset = 4; + int f2i = tree.addNode(f2); + uint64_t f2Id = tree.nodes[f2i].id; + + NullProvider prov; + + // First compose + ComposeResult result1 = compose(tree, prov); + // Find nested struct footer + QString text1 = result1.text; + QVERIFY2(text1.contains("sizeof(Nested)=0x8"), + qPrintable("Before delete nested sizeof: " + text1)); + + // Delete field from nested struct + int idx = tree.indexOfId(f2Id); + QVERIFY(idx >= 0); + tree.nodes.remove(idx); + tree.invalidateIdCache(); + + // Second compose - nested sizeof should update + ComposeResult result2 = compose(tree, prov); + QString text2 = result2.text; + QVERIFY2(text2.contains("sizeof(Nested)=0x4"), + qPrintable("After delete nested sizeof: " + text2)); + } }; QTEST_MAIN(TestCompose) diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index fa14c26..e1295ba 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -279,27 +279,28 @@ private slots: QVERIFY(ok); QCOMPARE((uint8_t)b[0], (uint8_t)0x4D); - // Hex32 with space-separated bytes + // Hex32 with space-separated bytes (raw byte order, no endian conversion) b = fmt::parseValue(NodeKind::Hex32, "DE AD BE EF", &ok); QVERIFY(ok); QCOMPARE(b.size(), 4); - uint32_t v32; - memcpy(&v32, b.data(), 4); - QCOMPARE(v32, (uint32_t)0xDEADBEEF); + QCOMPARE((uint8_t)b[0], (uint8_t)0xDE); + QCOMPARE((uint8_t)b[1], (uint8_t)0xAD); + QCOMPARE((uint8_t)b[2], (uint8_t)0xBE); + QCOMPARE((uint8_t)b[3], (uint8_t)0xEF); // Hex64 with space-separated bytes b = fmt::parseValue(NodeKind::Hex64, "4D 5A 90 00 00 00 00 00", &ok); QVERIFY(ok); QCOMPARE(b.size(), 8); - uint64_t v64; - memcpy(&v64, b.data(), 8); - QCOMPARE(v64, (uint64_t)0x4D5A900000000000ULL); + QCOMPARE((uint8_t)b[0], (uint8_t)0x4D); + QCOMPARE((uint8_t)b[1], (uint8_t)0x5A); + QCOMPARE((uint8_t)b[7], (uint8_t)0x00); // Hex64 continuous (should still work) b = fmt::parseValue(NodeKind::Hex64, "4D5A900000000000", &ok); QVERIFY(ok); - memcpy(&v64, b.data(), 8); - QCOMPARE(v64, (uint64_t)0x4D5A900000000000ULL); + QCOMPARE((uint8_t)b[0], (uint8_t)0x4D); + QCOMPARE((uint8_t)b[1], (uint8_t)0x5A); // Hex64 with 0x prefix and spaces b = fmt::parseValue(NodeKind::Hex64, "0x4D 5A 90 00 00 00 00 00", &ok); diff --git a/tests/test_format.cpp b/tests/test_format.cpp index 376bdfe..3c783fd 100644 --- a/tests/test_format.cpp +++ b/tests/test_format.cpp @@ -62,7 +62,7 @@ private slots: n.name = "Test"; QString s = fmt::fmtStructFooter(n, 0); QVERIFY(s.contains("};")); - QVERIFY(s.contains("Test")); + // When no size, footer is just "};" without name } void testIndent() { @@ -93,12 +93,14 @@ private slots: void testParseValueHex32() { bool ok; + // Hex parsing produces raw bytes (no endian conversion) QByteArray b = fmt::parseValue(NodeKind::Hex32, "DEADBEEF", &ok); QVERIFY(ok); QCOMPARE(b.size(), 4); - uint32_t v; - memcpy(&v, b.data(), 4); - QCOMPARE(v, (uint32_t)0xDEADBEEF); + QCOMPARE((uint8_t)b[0], (uint8_t)0xDE); + QCOMPARE((uint8_t)b[1], (uint8_t)0xAD); + QCOMPARE((uint8_t)b[2], (uint8_t)0xBE); + QCOMPARE((uint8_t)b[3], (uint8_t)0xEF); } void testParseValueBool() { @@ -119,12 +121,13 @@ private slots: void testParseValueHex0xPrefix() { bool ok; - // Hex32 with 0x prefix should work + // Hex32 with 0x prefix should work (raw bytes, no endian conversion) QByteArray b = fmt::parseValue(NodeKind::Hex32, "0xDEADBEEF", &ok); QVERIFY(ok); - uint32_t v; - memcpy(&v, b.data(), 4); - QCOMPARE(v, (uint32_t)0xDEADBEEF); + QCOMPARE((uint8_t)b[0], (uint8_t)0xDE); + QCOMPARE((uint8_t)b[1], (uint8_t)0xAD); + QCOMPARE((uint8_t)b[2], (uint8_t)0xBE); + QCOMPARE((uint8_t)b[3], (uint8_t)0xEF); // Pointer64 with 0x prefix b = fmt::parseValue(NodeKind::Pointer64, "0x0000000000400000", &ok); @@ -229,8 +232,7 @@ private slots: // With size QString s1 = fmt::fmtStructFooter(n, 0, 0x14); QVERIFY(s1.contains("};")); - QVERIFY(s1.contains("Test")); - QVERIFY(s1.contains("sizeof=0x14")); + QVERIFY(s1.contains("sizeof(Test)=0x14")); // Size 0 → no sizeof QString s2 = fmt::fmtStructFooter(n, 0, 0);