KindFlags refactor, indicator helpers, auto-padding, sizeof format fix

- Add KindFlags bitmask enum (KF_HexPreview, KF_Container, KF_String, KF_Vector)
- Add helper functions: isHexPreview(), flagsFor(), allTypeNamesForUI()
- Add editor helpers: clearIndicatorLine(), fillIndicatorCols(), resolvedSpanFor()
- Use SCI_FINDCOLUMN for UTF-8 safe column-to-position conversion
- Auto-emit hex padding nodes when changing type to smaller size
- Fix struct footer format: sizeof(Name)=0x... (no spaces)
- Add tests for sizeof recalculation after node deletion
- Add debug output to investigate sizeof refresh issue
- Replace icons.qrc with resources.qrc, add embedded fonts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
sysadmin
2026-02-03 05:55:29 -07:00
parent abe5e3ebd9
commit af183f27f0
14 changed files with 787 additions and 325 deletions

View File

@@ -19,7 +19,7 @@ add_executable(ReclassX
src/controller.cpp src/controller.cpp
src/compose.cpp src/compose.cpp
src/format.cpp src/format.cpp
src/icons.qrc src/resources.qrc
) )
target_include_directories(ReclassX PRIVATE src) target_include_directories(ReclassX PRIVATE src)
@@ -31,7 +31,7 @@ target_link_libraries(ReclassX PRIVATE
dbghelp dbghelp
) )
add_custom_target(screenshot add_custom_target(screenshot ALL
COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
DEPENDS ReclassX DEPENDS ReclassX
WORKING_DIRECTORY ${CMAKE_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}

View File

@@ -192,7 +192,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeKind = node.kind; lm.nodeKind = node.kind;
lm.offsetText = QStringLiteral(" ---"); lm.offsetText = QStringLiteral(" ---");
lm.foldLevel = computeFoldLevel(depth, false); lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = (1u << M_STRUCT_BG); lm.markerMask = 0;
int sz = tree.structSpan(node.id, &state.childMap); int sz = tree.structSpan(node.id, &state.childMap);
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm); state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
} }

View File

@@ -125,9 +125,17 @@ void RcxController::connectEditor(RcxEditor* editor) {
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) { this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) {
if (nodeIdx < 0) { refresh(); return; } if (nodeIdx < 0) { refresh(); return; }
switch (target) { switch (target) {
case EditTarget::Name: case EditTarget::Name: {
if (!text.isEmpty()) renameNode(nodeIdx, text); 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; break;
}
case EditTarget::Type: { case EditTarget::Type: {
bool ok; bool ok;
NodeKind k = kindFromTypeName(text, &ok); NodeKind k = kindFromTypeName(text, &ok);
@@ -147,6 +155,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
void RcxController::refresh() { void RcxController::refresh() {
m_lastResult = m_doc->compose(); m_lastResult = m_doc->compose();
qDebug() << "refresh() called, text length:" << m_lastResult.text.size();
// Prune stale selections (nodes removed by undo/redo/delete) // Prune stale selections (nodes removed by undo/redo/delete)
QSet<uint64_t> valid; QSet<uint64_t> valid;
@@ -173,22 +182,49 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
Node tmp = node; Node tmp = node;
tmp.kind = newKind; tmp.kind = newKind;
int newSize = tmp.byteSize(); int newSize = tmp.byteSize();
int delta = newSize - oldSize;
QVector<cmd::OffsetAdj> adjs; if (newSize > 0 && newSize < oldSize) {
if (delta != 0 && oldSize > 0 && newSize > 0) { // Shrinking: insert hex padding to fill gap (no offset shift)
int oldEnd = node.offset + oldSize; int gap = oldSize - newSize;
auto siblings = m_doc->tree.childrenOf(node.parentId); uint64_t parentId = node.parentId;
for (int si : siblings) { int baseOffset = node.offset + newSize;
if (si == nodeIdx) continue;
auto& sib = m_doc->tree.nodes[si]; // Push type change with no offset adjustments
if (sib.offset >= oldEnd) m_doc->undoStack.push(new RcxCommand(this,
adjs.append({sib.id, sib.offset, sib.offset + delta}); 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<cmd::OffsetAdj> 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) { void RcxController::renameNode(int nodeIdx, const QString& newName) {
@@ -280,11 +316,13 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
tree.addNode(c.node); tree.addNode(c.node);
} }
} else if constexpr (std::is_same_v<T, cmd::Remove>) { } else if constexpr (std::is_same_v<T, cmd::Remove>) {
qDebug() << "applyCommand Remove, isUndo:" << isUndo << "nodeId:" << c.nodeId;
if (isUndo) { if (isUndo) {
for (const Node& n : c.subtree) for (const Node& n : c.subtree)
tree.addNode(n); tree.addNode(n);
} else { } else {
QVector<int> indices = tree.subtreeIndices(c.nodeId); QVector<int> indices = tree.subtreeIndices(c.nodeId);
qDebug() << " Removing" << indices.size() << "nodes";
std::sort(indices.begin(), indices.end(), std::greater<int>()); std::sort(indices.begin(), indices.end(), std::greater<int>());
for (int idx : indices) for (int idx : indices)
tree.nodes.remove(idx); tree.nodes.remove(idx);
@@ -301,7 +339,8 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
refresh(); 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 (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
if (!m_doc->provider->isWritable()) return; if (!m_doc->provider->isWritable()) return;
@@ -317,7 +356,13 @@ void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text)
} }
bool ok; 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; if (!ok) return;
// For strings, pad/truncate to full buffer size // For strings, pad/truncate to full buffer size
@@ -514,19 +559,30 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
m_selIds.insert(nodeId); m_selIds.insert(nodeId);
m_anchorLine = line; m_anchorLine = line;
} else if (shift && !ctrl) { } else if (shift && !ctrl) {
m_selIds.clear(); if (m_anchorLine < 0) {
int from = qMin(m_anchorLine, line); m_selIds.clear();
int to = qMax(m_anchorLine, line); m_selIds.insert(nodeId);
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) { m_anchorLine = line;
uint64_t nid = m_lastResult.meta[i].nodeId; } else {
if (nid != 0) m_selIds.insert(nid); 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 } else { // Ctrl+Shift
int from = qMin(m_anchorLine, line); if (m_anchorLine < 0) {
int to = qMax(m_anchorLine, line); m_selIds.insert(nodeId);
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) { m_anchorLine = line;
uint64_t nid = m_lastResult.meta[i].nodeId; } else {
if (nid != 0) m_selIds.insert(nid); 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 } // namespace rcx

View File

@@ -64,7 +64,7 @@ public:
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name); void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
void removeNode(int nodeIdx); void removeNode(int nodeIdx);
void toggleCollapse(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 duplicateNode(int nodeIdx);
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos); void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
void batchRemoveNodes(const QVector<int>& nodeIndices); void batchRemoveNodes(const QVector<int>& nodeIndices);
@@ -81,6 +81,7 @@ public:
QSet<uint64_t> selectedIds() const { return m_selIds; } QSet<uint64_t> selectedIds() const { return m_selIds; }
RcxDocument* document() const { return m_doc; } RcxDocument* document() const { return m_doc; }
void setEditorFont(const QString& fontName);
signals: signals:
void nodeSelected(int nodeIdx); void nodeSelected(int nodeIdx);

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <QString> #include <QString>
#include <QStringList>
#include <QVector> #include <QVector>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
@@ -27,6 +28,16 @@ enum class NodeKind : uint8_t {
Struct, Array 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) ── // ── Unified kind metadata table (single source of truth) ──
struct KindMeta { struct KindMeta {
@@ -36,36 +47,37 @@ struct KindMeta {
int size; // byte size (0 = dynamic: Struct/Array) int size; // byte size (0 = dynamic: Struct/Array)
int lines; // display line count int lines; // display line count
int align; // natural alignment int align; // natural alignment
uint32_t flags; // KindFlags bitmask
}; };
inline constexpr KindMeta kKindMeta[] = { inline constexpr KindMeta kKindMeta[] = {
// kind name typeName sz ln al // kind name typeName sz ln al flags
{NodeKind::Hex8, "Hex8", "Hex8", 1, 1, 1}, {NodeKind::Hex8, "Hex8", "Hex8", 1, 1, 1, KF_HexPreview},
{NodeKind::Hex16, "Hex16", "Hex16", 2, 1, 2}, {NodeKind::Hex16, "Hex16", "Hex16", 2, 1, 2, KF_HexPreview},
{NodeKind::Hex32, "Hex32", "Hex32", 4, 1, 4}, {NodeKind::Hex32, "Hex32", "Hex32", 4, 1, 4, KF_HexPreview},
{NodeKind::Hex64, "Hex64", "Hex64", 8, 1, 8}, {NodeKind::Hex64, "Hex64", "Hex64", 8, 1, 8, KF_HexPreview},
{NodeKind::Int8, "Int8", "int8_t", 1, 1, 1}, {NodeKind::Int8, "Int8", "int8_t", 1, 1, 1, KF_None},
{NodeKind::Int16, "Int16", "int16_t", 2, 1, 2}, {NodeKind::Int16, "Int16", "int16_t", 2, 1, 2, KF_None},
{NodeKind::Int32, "Int32", "int32_t", 4, 1, 4}, {NodeKind::Int32, "Int32", "int32_t", 4, 1, 4, KF_None},
{NodeKind::Int64, "Int64", "int64_t", 8, 1, 8}, {NodeKind::Int64, "Int64", "int64_t", 8, 1, 8, KF_None},
{NodeKind::UInt8, "UInt8", "uint8_t", 1, 1, 1}, {NodeKind::UInt8, "UInt8", "uint8_t", 1, 1, 1, KF_None},
{NodeKind::UInt16, "UInt16", "uint16_t", 2, 1, 2}, {NodeKind::UInt16, "UInt16", "uint16_t", 2, 1, 2, KF_None},
{NodeKind::UInt32, "UInt32", "uint32_t", 4, 1, 4}, {NodeKind::UInt32, "UInt32", "uint32_t", 4, 1, 4, KF_None},
{NodeKind::UInt64, "UInt64", "uint64_t", 8, 1, 8}, {NodeKind::UInt64, "UInt64", "uint64_t", 8, 1, 8, KF_None},
{NodeKind::Float, "Float", "float", 4, 1, 4}, {NodeKind::Float, "Float", "float", 4, 1, 4, KF_None},
{NodeKind::Double, "Double", "double", 8, 1, 8}, {NodeKind::Double, "Double", "double", 8, 1, 8, KF_None},
{NodeKind::Bool, "Bool", "bool", 1, 1, 1}, {NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None},
{NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4}, {NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None},
{NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8}, {NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None},
{NodeKind::Vec2, "Vec2", "Vec2", 8, 2, 4}, {NodeKind::Vec2, "Vec2", "Vec2", 8, 2, 4, KF_Vector},
{NodeKind::Vec3, "Vec3", "Vec3", 12, 3, 4}, {NodeKind::Vec3, "Vec3", "Vec3", 12, 3, 4, KF_Vector},
{NodeKind::Vec4, "Vec4", "Vec4", 16, 4, 4}, {NodeKind::Vec4, "Vec4", "Vec4", 16, 4, 4, KF_Vector},
{NodeKind::Mat4x4, "Mat4x4", "Mat4x4", 64, 4, 4}, {NodeKind::Mat4x4, "Mat4x4", "Mat4x4", 64, 4, 4, KF_None},
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1}, {NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String},
{NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2}, {NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String},
{NodeKind::Padding, "Padding", "pad", 1, 1, 1}, {NodeKind::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview},
{NodeKind::Struct, "Struct", "struct", 0, 1, 1}, {NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
{NodeKind::Array, "Array", "array", 0, 1, 1}, {NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
}; };
inline constexpr const KindMeta* kindMeta(NodeKind k) { inline constexpr const KindMeta* kindMeta(NodeKind k) {
@@ -100,6 +112,27 @@ inline NodeKind kindFromTypeName(const QString& s, bool* ok = nullptr) {
return NodeKind::Hex8; 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 ── // ── Marker vocabulary ──
enum Marker : int { enum Marker : int {
@@ -450,29 +483,36 @@ inline ColumnSpan typeSpanFor(const LineMeta& lm) {
inline ColumnSpan nameSpanFor(const LineMeta& lm) { inline ColumnSpan nameSpanFor(const LineMeta& lm) {
if (lm.isContinuation || lm.lineKind != LineKind::Field) return {}; 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 ind = kFoldCol + lm.depth * 3;
int start = ind + kColType + kSepWidth; 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}; return {start, start + kColName, true};
} }
inline ColumnSpan valueSpanFor(const LineMeta& lm, int lineLength) { inline ColumnSpan valueSpanFor(const LineMeta& lm, int lineLength) {
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
int ind = kFoldCol + lm.depth * 3; int ind = kFoldCol + lm.depth * 3;
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes...]
bool isHexPad = isHexPreview(lm.nodeKind);
if (lm.isContinuation) { 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; int start = ind + prefixW;
return {start, lineLength, start < lineLength}; return {start, lineLength, start < lineLength};
} }
if (lm.lineKind != LineKind::Field) return {}; 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}; return {start, lineLength, start < lineLength};
} }
@@ -514,6 +554,7 @@ namespace fmt {
QString editableValue(const Node& node, const Provider& prov, QString editableValue(const Node& node, const Provider& prov,
uint64_t addr, int subLine); uint64_t addr, int subLine);
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok); QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
} // namespace fmt } // namespace fmt
// ── Compose function forward declaration ── // ── Compose function forward declaration ──

View File

@@ -8,7 +8,6 @@
#include <QKeyEvent> #include <QKeyEvent>
#include <QMouseEvent> #include <QMouseEvent>
#include <QFocusEvent> #include <QFocusEvent>
#include <QToolTip>
#include <QTimer> #include <QTimer>
#include <QCursor> #include <QCursor>
#include <QApplication> #include <QApplication>
@@ -21,11 +20,14 @@ static const QColor kBgMargin("#252526");
static const QColor kFgMargin("#858585"); static const QColor kFgMargin("#858585");
static const QColor kFgMarginDim("#505050"); static const QColor kFgMarginDim("#505050");
static constexpr int IND_EDITABLE = 8; static constexpr int IND_EDITABLE = 8;
static constexpr int IND_HEX_DIM = 9; static constexpr int IND_HEX_DIM = 9;
static constexpr int IND_HOVER_TOK = 10;
static QString g_fontName = "Consolas";
static QFont editorFont() { static QFont editorFont() {
QFont f("Consolas", 12); QFont f(g_fontName, 12);
f.setFixedPitch(true); f.setFixedPitch(true);
return f; return f;
} }
@@ -77,7 +79,14 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
}); });
connect(m_sci, &QsciScintilla::cursorPositionChanged, 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() { void RcxEditor::setupScintilla() {
@@ -86,6 +95,10 @@ void RcxEditor::setupScintilla() {
m_sci->setReadOnly(true); m_sci->setReadOnly(true);
m_sci->setWrapMode(QsciScintilla::WrapNone); m_sci->setWrapMode(QsciScintilla::WrapNone);
m_sci->setCaretLineVisible(false); 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->setPaper(kBgText);
m_sci->setColor(QColor("#d4d4d4")); 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_SETSELFORE, (long)0, (long)0);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (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, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_EDITABLE, 17 /*INDIC_TEXTFORE*/); IND_EDITABLE, 17 /*INDIC_TEXTFORE*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
@@ -115,6 +128,16 @@ void RcxEditor::setupScintilla() {
IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/); IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_HEX_DIM, QColor("#505050")); 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() { void RcxEditor::setupLexer() {
@@ -132,7 +155,7 @@ void RcxEditor::setupLexer() {
m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine); m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine);
m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc); m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc);
m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Default); 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("#c586c0"), QsciLexerCPP::PreProcessor);
m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator); m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator);
@@ -144,6 +167,11 @@ void RcxEditor::setupLexer() {
m_sci->setLexer(m_lexer); m_sci->setLexer(m_lexer);
m_sci->setBraceMatching(QsciScintilla::SloppyBraceMatch); 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() { void RcxEditor::setupMargins() {
@@ -153,7 +181,7 @@ void RcxEditor::setupMargins() {
m_sci->setMarginType(0, QsciScintilla::TextMarginRightJustified); m_sci->setMarginType(0, QsciScintilla::TextMarginRightJustified);
m_sci->setMarginWidth(0, " +0x00000000 "); m_sci->setMarginWidth(0, " +0x00000000 ");
m_sci->setMarginsBackgroundColor(kBgMargin); m_sci->setMarginsBackgroundColor(kBgMargin);
m_sci->setMarginsForegroundColor(kFgMargin); m_sci->setMarginsForegroundColor(kFgMarginDim);
m_sci->setMarginSensitivity(0, true); m_sci->setMarginSensitivity(0, true);
// Margin 1: hidden (fold chevrons moved to text column) // Margin 1: hidden (fold chevrons moved to text column)
@@ -182,15 +210,11 @@ void RcxEditor::setupFolding() {
} }
void RcxEditor::setupMarkers() { void RcxEditor::setupMarkers() {
// M_CONT (0): vertical line // M_CONT (0): continuation line (metadata only, no visual)
m_sci->markerDefine(QsciScintilla::VLine, M_CONT); m_sci->markerDefine(QsciScintilla::Invisible, M_CONT);
m_sci->setMarkerBackgroundColor(kFgMarginDim, M_CONT);
m_sci->setMarkerForegroundColor(kFgMarginDim, M_CONT);
// M_PAD (1): small rectangle (dim gray) // M_PAD (1): padding line (metadata only, no visual)
m_sci->markerDefine(QsciScintilla::SmallRectangle, M_PAD); m_sci->markerDefine(QsciScintilla::Invisible, M_PAD);
m_sci->setMarkerBackgroundColor(QColor("#606060"), M_PAD);
m_sci->setMarkerForegroundColor(QColor("#606060"), M_PAD);
// M_PTR0 (2): right triangle (red) // M_PTR0 (2): right triangle (red)
m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0); 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_STRUCT_BG (5): background tint for struct header/footer
m_sci->markerDefine(QsciScintilla::Background, M_STRUCT_BG); 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_sci->setMarkerForegroundColor(QColor("#d4d4d4"), M_STRUCT_BG);
// M_HOVER (6): full-row hover highlight // M_HOVER (6): full-row hover highlight
@@ -222,7 +246,6 @@ void RcxEditor::setupMarkers() {
} }
void RcxEditor::allocateMarginStyles() { void RcxEditor::allocateMarginStyles() {
// Relative indices within margin style offset
static constexpr int MSTYLE_NORMAL = 0; static constexpr int MSTYLE_NORMAL = 0;
static constexpr int MSTYLE_CONT = 1; static constexpr int MSTYLE_CONT = 1;
@@ -231,18 +254,17 @@ void RcxEditor::allocateMarginStyles() {
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLEOFFSET, base); m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLEOFFSET, base);
const long bgrMargin = 0x262525; // BGR for #252526 const long bgrMargin = 0x262525; // BGR for #252526
QByteArray fontName = editorFont().family().toUtf8();
int fontSize = editorFont().pointSize();
// Normal offset style: gray on dark for (int s = MSTYLE_NORMAL; s <= MSTYLE_CONT; s++) {
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE, unsigned long abs = (unsigned long)(base + s);
(unsigned long)(base + MSTYLE_NORMAL), (long)0x858585); m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE, abs, (long)0x505050);
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK, m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK, abs, bgrMargin);
(unsigned long)(base + MSTYLE_NORMAL), bgrMargin); m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFONT,
(uintptr_t)abs, fontName.constData());
// Continuation style: dimmer m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETSIZE, abs, (long)fontSize);
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);
} }
void RcxEditor::applyDocument(const ComposeResult& result) { void RcxEditor::applyDocument(const ComposeResult& result) {
@@ -264,11 +286,11 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
applyFoldLevels(result.meta); applyFoldLevels(result.meta);
applyHexDimming(result.meta); applyHexDimming(result.meta);
// Re-apply editable underline for current cursor line // Re-apply editable indicators for current cursor line
m_hintLine = -1; m_hintLine = -1;
int line, col; int line, col;
m_sci->getCursorPosition(&line, &col); m_sci->getCursorPosition(&line, &col);
updateEditableUnderline(line); updateEditableIndicators(line);
} }
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) { void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
@@ -277,10 +299,14 @@ void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
for (int i = 0; i < meta.size(); i++) { for (int i = 0; i < meta.size(); i++) {
const auto& lm = meta[i]; const auto& lm = meta[i];
if (!lm.offsetText.isEmpty()) { if (lm.offsetText.isEmpty()) continue;
int style = lm.isContinuation ? 1 : 0;
m_sci->setMarginText(i, lm.offsetText, style); 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; 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<LineMeta>& meta) { void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM); m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM);
for (int i = 0; i < meta.size(); i++) { for (int i = 0; i < meta.size(); i++) {
switch (meta[i].nodeKind) { if (isHexPreview(meta[i].nodeKind)) {
case NodeKind::Hex8: case NodeKind::Hex16:
case NodeKind::Hex32: case NodeKind::Hex64:
case NodeKind::Padding: {
long pos, len; lineRangeNoEol(m_sci, i, pos, len); long pos, len; lineRangeNoEol(m_sci, i, pos, len);
if (len > 0) if (len > 0)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len);
break;
}
default: break;
} }
} }
} }
@@ -331,10 +375,27 @@ void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) { void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
m_currentSelIds = selIds; m_currentSelIds = selIds;
m_sci->markerDeleteAll(M_SELECTED); 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++) { 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); 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(); applyHoverHighlight();
} }
@@ -361,7 +422,12 @@ ViewState RcxEditor::saveViewState() const {
} }
void RcxEditor::restoreViewState(const ViewState& vs) { 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, m_sci->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE,
(unsigned long)vs.scrollLine); (unsigned long)vs.scrollLine);
} }
@@ -420,9 +486,19 @@ static QString getLineText(QsciScintilla* sci, int line) {
// ── Shared inline-edit shutdown ── // ── Shared inline-edit shutdown ──
RcxEditor::EndEditInfo RcxEditor::endInlineEdit() { 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}; EndEditInfo info{m_editState.nodeIdx, m_editState.subLine, m_editState.target};
m_editState.active = false; m_editState.active = false;
m_sci->setReadOnly(true); 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 // Disable selection rendering again
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (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}; 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 ── // ── Double-click hit test ──
static bool hitTestTarget(QsciScintilla* sci, 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 if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonPress
&& m_editState.active) { && m_editState.active) {
// Only commit if click is outside the active edit span
auto* me = static_cast<QMouseEvent*>(event); auto* me = static_cast<QMouseEvent*>(event);
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, auto h = hitTest(me->pos());
(unsigned long)me->pos().x(), (long)me->pos().y());
bool insideEdit = false; bool insideEdit = false;
if (pos >= 0) { if (h.line == m_editState.line) {
int clickLine = (int)m_sci->SendScintilla( int editEnd = editEndCol();
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); insideEdit = (h.col >= m_editState.spanStart && h.col <= editEnd);
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 (!insideEdit) if (insideEdit)
commitInlineEdit(); return false; // inside edit span: let Scintilla position cursor
return false; // always let click through to Scintilla commitInlineEdit();
m_currentSelIds.clear(); // stale — normal handler will re-establish
// Fall through to normal click handler below
} }
// Single-click on fold column (" - " / " + ") toggles fold // Single-click on fold column (" - " / " + ") toggles fold
// Other left-clicks emit nodeClicked for selection // Other left-clicks emit nodeClicked for selection
@@ -559,27 +668,39 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
&& event->type() == QEvent::MouseButtonPress) { && event->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(event); auto* me = static_cast<QMouseEvent*>(event);
if (me->button() == Qt::LeftButton) { if (me->button() == Qt::LeftButton) {
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, auto h = hitTest(me->pos());
(unsigned long)me->pos().x(), (long)me->pos().y()); if (h.inFoldCol) {
if (pos >= 0) { emit marginClicked(0, h.line, me->modifiers());
int line = (int)m_sci->SendScintilla( return true;
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); }
int col = (int)m_sci->SendScintilla( if (h.nodeId != 0) {
QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos); bool alreadySelected = m_currentSelIds.contains(h.nodeId);
if (col < kFoldCol && line >= 0 && line < m_meta.size() bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier));
&& m_meta[line].foldHead) {
emit marginClicked(0, line, me->modifiers()); // Single-click on editable token of already-selected node → edit
return true; if (alreadySelected && plain) {
} int tLine; EditTarget t;
// Selection click — emit for controller to manage if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) {
if (line >= 0 && line < m_meta.size()) { m_pendingClickNodeId = 0;
uint64_t nid = m_meta[line].nodeId; return beginInlineEdit(t, tLine);
if (nid != 0) {
emit nodeClicked(line, nid, me->modifiers());
m_dragging = true;
m_dragLastLine = line;
} }
} }
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) { && event->type() == QEvent::MouseMove && m_dragging) {
auto* me = static_cast<QMouseEvent*>(event); auto* me = static_cast<QMouseEvent*>(event);
if (me->buttons() & Qt::LeftButton) { if (me->buttons() & Qt::LeftButton) {
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, // Flush deferred click before extending drag
(unsigned long)me->pos().x(), (long)me->pos().y()); if (m_pendingClickNodeId != 0) {
if (pos >= 0) { emit nodeClicked(m_pendingClickLine, m_pendingClickNodeId,
int line = (int)m_sci->SendScintilla( m_pendingClickMods);
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); m_pendingClickNodeId = 0;
if (line >= 0 && line < m_meta.size() && line != m_dragLastLine) { }
uint64_t nid = m_meta[line].nodeId; auto h = hitTest(me->pos());
if (nid != 0) { if (h.line >= 0 && h.line != m_dragLastLine && h.nodeId != 0) {
emit nodeClicked(line, nid, Qt::ShiftModifier); emit nodeClicked(h.line, h.nodeId, m_dragInitMods | Qt::ShiftModifier);
m_dragLastLine = line; m_dragLastLine = h.line;
}
}
} }
} else { } else {
m_dragging = false; m_dragging = false;
@@ -607,13 +726,20 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
} }
if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonRelease) { if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonRelease) {
m_dragging = false; 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 if (obj == m_sci->viewport() && !m_editState.active
&& event->type() == QEvent::MouseButtonDblClick) { && event->type() == QEvent::MouseButtonDblClick) {
auto* me = static_cast<QMouseEvent*>(event); auto* me = static_cast<QMouseEvent*>(event);
int line; EditTarget t; 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); return beginInlineEdit(t, line);
}
} }
if (obj == m_sci && event->type() == QEvent::FocusOut) { if (obj == m_sci && event->type() == QEvent::FocusOut) {
auto* fe = static_cast<QFocusEvent*>(event); auto* fe = static_cast<QFocusEvent*>(event);
@@ -626,18 +752,14 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
commitInlineEdit(); commitInlineEdit();
}); });
} }
// Clear underlines when editor loses focus // Clear editable indicators when editor loses focus
if (m_hintLine >= 0) { clearIndicatorLine(IND_EDITABLE, m_hintLine);
long start, len; lineRangeNoEol(m_sci, m_hintLine, start, len); m_hintLine = -1;
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, start, len);
m_hintLine = -1;
}
} }
if (obj == m_sci && event->type() == QEvent::FocusIn) { if (obj == m_sci && event->type() == QEvent::FocusIn) {
int line, col; int line, col;
m_sci->getCursorPosition(&line, &col); m_sci->getCursorPosition(&line, &col);
updateEditableUnderline(line); updateEditableIndicators(line);
} }
if (obj == m_sci->viewport() && !m_editState.active) { if (obj == m_sci->viewport() && !m_editState.active) {
if (event->type() == QEvent::MouseMove) { if (event->type() == QEvent::MouseMove) {
@@ -654,16 +776,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
// Resolve hovered nodeId on move/wheel // Resolve hovered nodeId on move/wheel
if (event->type() == QEvent::MouseMove if (event->type() == QEvent::MouseMove
|| event->type() == QEvent::Wheel) { || event->type() == QEvent::Wheel) {
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, auto h = hitTest(m_lastHoverPos);
(unsigned long)m_lastHoverPos.x(), uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0;
(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;
}
if (newHoverId != m_hoveredNodeId) { if (newHoverId != m_hoveredNodeId) {
m_hoveredNodeId = newHoverId; m_hoveredNodeId = newHoverId;
applyHoverHighlight(); applyHoverHighlight();
@@ -704,22 +818,17 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
case Qt::Key_Return: case Qt::Key_Return:
case Qt::Key_Enter: case Qt::Key_Enter:
case Qt::Key_Tab: case Qt::Key_Tab:
if (autocActive) { if (autocActive && m_editState.target == EditTarget::Type) {
if (m_editState.target == EditTarget::Type) { // Extract selected typeName directly from autocomplete
// Extract selected typeName directly from autocomplete QByteArray buf(256, '\0');
QByteArray buf(256, '\0'); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCGETCURRENTTEXT,
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCGETCURRENTTEXT, (unsigned long)256, (void*)buf.data());
(unsigned long)256, (void*)buf.data()); QString selectedType = QString::fromUtf8(buf.constData());
QString selectedType = QString::fromUtf8(buf.constData()); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL);
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL);
auto info = endInlineEdit(); auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, EditTarget::Type, selectedType); emit inlineEditCommitted(info.nodeIdx, info.subLine, EditTarget::Type, selectedType);
return true; return true;
}
// Other targets: let Scintilla complete, then auto-commit
QTimer::singleShot(0, this, &RcxEditor::commitInlineEdit);
return false;
} }
commitInlineEdit(); commitInlineEdit();
return true; return true;
@@ -745,9 +854,18 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
if (col <= m_editState.spanStart) return true; if (col <= m_editState.spanStart) return true;
return false; 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: case Qt::Key_Home:
m_sci->setCursorPosition(m_editState.line, m_editState.spanStart); m_sci->setCursorPosition(m_editState.line, m_editState.spanStart);
return true; return true;
case Qt::Key_End:
m_sci->setCursorPosition(m_editState.line, editEndCol());
return true;
default: default:
return false; return false;
} }
@@ -763,6 +881,12 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
} }
m_hoveredNodeId = 0; m_hoveredNodeId = 0;
applyHoverHighlight(); 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) { if (line >= 0) {
m_sci->setCursorPosition(line, 0); m_sci->setCursorPosition(line, 0);
@@ -772,21 +896,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
auto* lm = metaForLine(line); auto* lm = metaForLine(line);
if (!lm || lm->nodeIdx < 0) return false; if (!lm || lm->nodeIdx < 0) return false;
QString lineText = getLineText(m_sci, line); QString lineText;
int textLen = lineText.size(); NormalizedSpan norm;
if (!resolvedSpanFor(line, target, norm, &lineText)) return false;
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 trimmed = lineText.mid(norm.start, norm.end - norm.start); 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.target = target;
m_editState.spanStart = norm.start; m_editState.spanStart = norm.start;
m_editState.original = trimmed; 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 // Disable Scintilla undo during inline edit
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1);
m_sci->setReadOnly(false); m_sci->setReadOnly(false);
QApplication::setOverrideCursor(Qt::IBeamCursor);
m_cursorOverridden = true;
// Re-enable selection rendering for inline edit // Re-enable selection rendering for inline edit
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1, m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1,
QColor("#264f78")); QColor("#264f78"));
// Select just the trimmed text (keeps columns aligned)
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
(unsigned long)line); (unsigned long)line);
long posSelStart = lineStart + m_editState.spanStart; long posStart = lineStart + m_editState.spanStart;
long posSelEnd = posSelStart + trimmed.toUtf8().size(); long posEnd = posStart + trimmed.toUtf8().size();
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posSelStart, posSelEnd); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posEnd, posEnd);
updateEditTokenBox();
if (target == EditTarget::Type) if (target == EditTarget::Type)
QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete); QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete);
@@ -821,6 +940,21 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
return true; 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 ── // ── Commit inline edit ──
void RcxEditor::commitInlineEdit() { void RcxEditor::commitInlineEdit() {
@@ -861,12 +995,7 @@ void RcxEditor::showTypeAutocomplete() {
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, posStart); m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, posStart);
// Build list from typeName (matches what the editor displays) // Build list from typeName (matches what the editor displays)
QStringList types; QByteArray list = allTypeNamesForUI().join(' ').toUtf8();
for (const auto& m : kKindMeta)
types << m.typeName;
types.sort(Qt::CaseInsensitive);
QByteArray list = types.join(QChar(' ')).toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' '); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' ');
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETIGNORECASE, (long)1); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETIGNORECASE, (long)1);
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETDROPRESTOFWORD, (long)1); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETDROPRESTOFWORD, (long)1);
@@ -878,64 +1007,44 @@ void RcxEditor::showTypeAutocomplete() {
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSELECT, m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSELECT,
(uintptr_t)0, cur.constData()); (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 (m_editState.active) return;
if (line == m_hintLine) return; if (line == m_hintLine) return;
auto clearLine = [&](int l) { // Clear old cursor line (only if not a selected node)
if (l < 0) return; if (m_hintLine >= 0) {
long start, len; lineRangeNoEol(m_sci, l, start, len); const LineMeta* oldLm = metaForLine(m_hintLine);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE); if (!oldLm || !m_currentSelIds.contains(oldLm->nodeId))
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, start, len); clearIndicatorLine(IND_EDITABLE, m_hintLine);
}; }
clearLine(m_hintLine);
m_hintLine = line; m_hintLine = line;
paintEditableSpans(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);
} }
// ── Hover cursor (coalesced) ── // ── Hover cursor (coalesced) ──
void RcxEditor::applyHoverCursor() { void RcxEditor::applyHoverCursor() {
auto clearHoverTok = [&]() {
clearIndicatorLine(IND_HOVER_TOK, m_hoverTokLine);
m_hoverTokLine = -1;
};
if (m_editState.active || !m_hoverInside if (m_editState.active || !m_hoverInside
|| !m_sci->viewport()->underMouse()) { || !m_sci->viewport()->underMouse()) {
clearHoverTok();
if (m_cursorOverridden) { if (m_cursorOverridden) {
QApplication::restoreOverrideCursor(); QApplication::restoreOverrideCursor();
m_cursorOverridden = false; m_cursorOverridden = false;
@@ -944,22 +1053,26 @@ void RcxEditor::applyHoverCursor() {
} }
int line; EditTarget t; 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 // Also show pointer cursor for fold column on fold-head lines
bool interactive = tokenHit;
if (!interactive) { if (!interactive) {
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, auto h = hitTest(m_lastHoverPos);
(unsigned long)m_lastHoverPos.x(), if (h.inFoldCol) interactive = true;
(long)m_lastHoverPos.y()); }
if (pos >= 0) {
int hLine = (int)m_sci->SendScintilla( // Token box highlight
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos); if (!tokenHit) {
int hCol = (int)m_sci->SendScintilla( clearHoverTok();
QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos); } else if (line != m_hoverTokLine || t != m_hoverTokTarget) {
if (hCol < kFoldCol && hLine >= 0 && hLine < m_meta.size() clearHoverTok();
&& m_meta[hLine].foldHead) m_hoverTokLine = line;
interactive = true; m_hoverTokTarget = t;
}
NormalizedSpan norm;
if (resolvedSpanFor(line, t, norm))
fillIndicatorCols(IND_HOVER_TOK, line, norm.start, norm.end);
} }
if (interactive && !m_cursorOverridden) { 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 } // namespace rcx

View File

@@ -37,6 +37,8 @@ public:
void cancelInlineEdit(); void cancelInlineEdit();
void applySelectionOverlay(const QSet<uint64_t>& selIds); void applySelectionOverlay(const QSet<uint64_t>& selIds);
void setEditorFont(const QString& fontName);
static void setGlobalFontName(const QString& fontName);
signals: signals:
void marginClicked(int margin, int line, Qt::KeyboardModifiers mods); void marginClicked(int margin, int line, Qt::KeyboardModifiers mods);
@@ -63,10 +65,17 @@ private:
bool m_cursorOverridden = false; bool m_cursorOverridden = false;
uint64_t m_hoveredNodeId = 0; uint64_t m_hoveredNodeId = 0;
QSet<uint64_t> m_currentSelIds; QSet<uint64_t> m_currentSelIds;
int m_hoverTokLine = -1;
EditTarget m_hoverTokTarget = EditTarget::Name;
// ── Drag selection ── // ── Drag selection ──
bool m_dragging = false; bool m_dragging = false;
int m_dragLastLine = -1; 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 ── // ── Inline edit state ──
struct InlineEditState { struct InlineEditState {
@@ -78,6 +87,7 @@ private:
int spanStart = 0; int spanStart = 0;
int linelenAfterReplace = 0; int linelenAfterReplace = 0;
QString original; QString original;
NodeKind editKind = NodeKind::Int32;
}; };
InlineEditState m_editState; InlineEditState m_editState;
@@ -94,20 +104,34 @@ private:
void applyHexDimming(const QVector<LineMeta>& meta); void applyHexDimming(const QVector<LineMeta>& meta);
void commitInlineEdit(); void commitInlineEdit();
int editEndCol() const;
bool handleNormalKey(QKeyEvent* ke); bool handleNormalKey(QKeyEvent* ke);
bool handleEditKey(QKeyEvent* ke); bool handleEditKey(QKeyEvent* ke);
void showTypeAutocomplete(); void showTypeAutocomplete();
void updateEditableUnderline(int line); void paintEditableSpans(int line);
void updateEditableIndicators(int line);
void applyHoverCursor(); void applyHoverCursor();
void applyHoverHighlight(); void applyHoverHighlight();
void updateEditTokenBox();
void validateEditLive();
void showEditValidation(bool valid);
// ── Refactored helpers ── // ── 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; }; struct EndEditInfo { int nodeIdx; int subLine; EditTarget target; };
EndEditInfo endInlineEdit(); EndEditInfo endInlineEdit();
struct NormalizedSpan { int start = 0; int end = 0; bool valid = false; }; struct NormalizedSpan { int start = 0; int end = 0; bool valid = false; };
NormalizedSpan normalizeSpan(const ColumnSpan& raw, const QString& lineText, NormalizedSpan normalizeSpan(const ColumnSpan& raw, const QString& lineText,
EditTarget target, bool skipPrefixes) const; 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 } // namespace rcx

Binary file not shown.

View File

@@ -47,10 +47,10 @@ QString fmtInt8(int8_t v) { return QString::number(v); }
QString fmtInt16(int16_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 fmtInt32(int32_t v) { return QString::number(v); }
QString fmtInt64(int64_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 fmtUInt8(uint8_t v) { return hexStr(v, 2); }
QString fmtUInt16(uint16_t v) { return QString::number(v); } QString fmtUInt16(uint16_t v) { return hexStr(v, 4); }
QString fmtUInt32(uint32_t v) { return QString::number(v); } QString fmtUInt32(uint32_t v) { return hexStr(v, 8); }
QString fmtUInt64(uint64_t v) { return QString::number(v); } QString fmtUInt64(uint64_t v) { return hexStr(v, 16); }
QString fmtFloat(float v) { return QString::number(v, 'f', 3); } QString fmtFloat(float v) { return QString::number(v, 'f', 3); }
QString fmtDouble(double v) { return QString::number(v, 'f', 6); } 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 fmtStructFooter(const Node& node, int depth, int totalSize) {
QString s = indent(depth) + QStringLiteral("}; // ") + node.name; QString s = indent(depth) + QStringLiteral("};");
if (totalSize > 0) 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; return s;
} }
@@ -225,10 +226,7 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
} }
// Hex nodes and Padding: ASCII preview + hex bytes (compact) // Hex nodes and Padding: ASCII preview + hex bytes (compact)
if (node.kind == NodeKind::Hex8 || node.kind == NodeKind::Hex16 || if (isHexPreview(node.kind)) {
node.kind == NodeKind::Hex32 || node.kind == NodeKind::Hex64 ||
node.kind == NodeKind::Padding)
{
if (node.kind == NodeKind::Padding) { if (node.kind == NodeKind::Padding) {
const int totalSz = qMax(1, node.arrayLen); const int totalSz = qMax(1, node.arrayLen);
const int lineOff = subLine * 8; const int lineOff = subLine * 8;
@@ -276,6 +274,36 @@ static QString stripHex(const QString& s) {
return 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 // Range-checked narrowing: sets *ok = false if parsed value doesn't fit in T
template<class T, class ParseT> template<class T, class ParseT>
static QByteArray parseIntChecked(ParseT val, bool* ok) { static QByteArray parseIntChecked(ParseT val, bool* ok) {
@@ -304,18 +332,18 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
} }
switch (kind) { switch (kind) {
case NodeKind::Hex8: { uint val = stripHex(s).remove(' ').toUInt(ok, 16); return parseIntChecked<uint8_t>(val, ok); } case NodeKind::Hex8: return parseHexBytes(stripHex(s), 1, ok);
case NodeKind::Hex16: { uint val = stripHex(s).remove(' ').toUInt(ok, 16); return parseIntChecked<uint16_t>(val, ok); } case NodeKind::Hex16: return parseHexBytes(stripHex(s), 2, ok);
case NodeKind::Hex32: { uint val = stripHex(s).remove(' ').toUInt(ok, 16); return *ok ? toBytes<uint32_t>(val) : QByteArray{}; } case NodeKind::Hex32: return parseHexBytes(stripHex(s), 4, ok);
case NodeKind::Hex64: { qulonglong val = stripHex(s).remove(' ').toULongLong(ok, 16); return *ok ? toBytes<uint64_t>(val) : QByteArray{}; } case NodeKind::Hex64: return parseHexBytes(stripHex(s), 8, ok);
case NodeKind::Int8: { int val = s.toInt(ok); return parseIntChecked<int8_t>(val, ok); } case NodeKind::Int8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return parseIntChecked<int8_t>(val, ok); }
case NodeKind::Int16: { int val = s.toInt(ok); return parseIntChecked<int16_t>(val, ok); } case NodeKind::Int16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return parseIntChecked<int16_t>(val, ok); }
case NodeKind::Int32: { int val = s.toInt(ok); return *ok ? toBytes<int32_t>(val) : QByteArray{}; } case NodeKind::Int32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return *ok ? toBytes<int32_t>(val) : QByteArray{}; }
case NodeKind::Int64: { qlonglong val = s.toLongLong(ok); return *ok ? toBytes<int64_t>(val) : QByteArray{}; } case NodeKind::Int64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qlonglong val = stripHex(s).toLongLong(ok,b); return *ok ? toBytes<int64_t>(val) : QByteArray{}; }
case NodeKind::UInt8: { uint val = s.toUInt(ok); return parseIntChecked<uint8_t>(val, ok); } case NodeKind::UInt8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked<uint8_t>(val, ok); }
case NodeKind::UInt16: { uint val = s.toUInt(ok); return parseIntChecked<uint16_t>(val, ok); } case NodeKind::UInt16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked<uint16_t>(val, ok); }
case NodeKind::UInt32: { uint val = s.toUInt(ok); return *ok ? toBytes<uint32_t>(val) : QByteArray{}; } case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return *ok ? toBytes<uint32_t>(val) : QByteArray{}; }
case NodeKind::UInt64: { qulonglong val = s.toULongLong(ok); return *ok ? toBytes<uint64_t>(val) : QByteArray{}; } case NodeKind::UInt64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return *ok ? toBytes<uint64_t>(val) : QByteArray{}; }
case NodeKind::Float: { case NodeKind::Float: {
float val = s.toFloat(ok); float val = s.toFloat(ok);
return *ok ? toBytes<float>(val) : QByteArray{}; return *ok ? toBytes<float>(val) : QByteArray{};

View File

@@ -12,10 +12,13 @@
#include <QFileInfo> #include <QFileInfo>
#include <QMessageBox> #include <QMessageBox>
#include <QAction> #include <QAction>
#include <QActionGroup>
#include <QMap> #include <QMap>
#include <QTimer> #include <QTimer>
#include <QDir> #include <QDir>
#include <QMetaObject> #include <QMetaObject>
#include <QFontDatabase>
#include <QSettings>
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> #include <windows.h>
@@ -115,6 +118,7 @@ private slots:
void undo(); void undo();
void redo(); void redo();
void about(); void about();
void setEditorFont(const QString& fontName);
private: private:
QMdiArea* m_mdiArea; QMdiArea* m_mdiArea;
@@ -175,6 +179,23 @@ void MainWindow::createMenus() {
auto* view = menuBar()->addMenu("&View"); auto* view = menuBar()->addMenu("&View");
view->addAction("Split &Horizontal", this, &MainWindow::splitView); view->addAction("Split &Horizontal", this, &MainWindow::splitView);
view->addAction("&Unsplit", this, &MainWindow::unsplitView); 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 // Node
auto* node = menuBar()->addMenu("&Node"); auto* node = menuBar()->addMenu("&Node");
@@ -548,6 +569,15 @@ void MainWindow::about() {
"fold markers, and status flags."); "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 { RcxController* MainWindow::activeController() const {
auto* sub = m_mdiArea->activeSubWindow(); auto* sub = m_mdiArea->activeSubWindow();
if (sub && m_tabs.contains(sub)) if (sub && m_tabs.contains(sub))
@@ -589,6 +619,18 @@ int main(int argc, char* argv[]) {
app.setOrganizationName("ReclassX"); app.setOrganizationName("ReclassX");
app.setStyle("Fusion"); // Fusion style respects dark palette well 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 // Global dark palette
QPalette darkPalette; QPalette darkPalette;
darkPalette.setColor(QPalette::Window, QColor("#1e1e1e")); darkPalette.setColor(QPalette::Window, QColor("#1e1e1e"));
@@ -611,7 +653,7 @@ int main(int argc, char* argv[]) {
bool screenshotMode = app.arguments().contains("--screenshot"); bool screenshotMode = app.arguments().contains("--screenshot");
if (screenshotMode) if (screenshotMode)
window.setAttribute(Qt::WA_DontShowOnScreen); window.setWindowOpacity(0.0);
window.show(); window.show();
// Always auto-open PE header demo on startup // Always auto-open PE header demo on startup
@@ -623,10 +665,10 @@ int main(int argc, char* argv[]) {
if (idx + 1 < app.arguments().size()) if (idx + 1 < app.arguments().size())
out = app.arguments().at(idx + 1); out = app.arguments().at(idx + 1);
QTimer::singleShot(1000, [&window, &app, out]() { QTimer::singleShot(1000, [&window, out]() {
QDir().mkpath(QFileInfo(out).absolutePath()); QDir().mkpath(QFileInfo(out).absolutePath());
window.grab().save(out); window.grab().save(out);
app.quit(); ::_exit(0); // immediate exit — no need for clean shutdown in screenshot mode
}); });
} }

View File

@@ -3,4 +3,7 @@
<file alias="chevron-right.png">icons/chevron-right.png</file> <file alias="chevron-right.png">icons/chevron-right.png</file>
<file alias="chevron-down.png">icons/chevron-down.png</file> <file alias="chevron-down.png">icons/chevron-down.png</file>
</qresource> </qresource>
<qresource prefix="/fonts">
<file alias="Iosevka-Regular.ttf">fonts/Iosevka-Regular.ttf</file>
</qresource>
</RCC> </RCC>

View File

@@ -641,9 +641,9 @@ private slots:
int lastLine = result.meta.size() - 1; int lastLine = result.meta.size() - 1;
QCOMPARE(result.meta[lastLine].lineKind, LineKind::Footer); 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(); QString footerText = result.text.split('\n').last();
QVERIFY(footerText.contains("sizeof=0xC")); QVERIFY(footerText.contains("sizeof(Sized)=0xC"));
} }
void testLineMetaHasNodeId() { void testLineMetaHasNodeId() {
@@ -668,6 +668,116 @@ private slots:
QCOMPARE(result.meta[i].nodeId, tree.nodes[ni].id); 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) QTEST_MAIN(TestCompose)

View File

@@ -279,27 +279,28 @@ private slots:
QVERIFY(ok); QVERIFY(ok);
QCOMPARE((uint8_t)b[0], (uint8_t)0x4D); 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); b = fmt::parseValue(NodeKind::Hex32, "DE AD BE EF", &ok);
QVERIFY(ok); QVERIFY(ok);
QCOMPARE(b.size(), 4); QCOMPARE(b.size(), 4);
uint32_t v32; QCOMPARE((uint8_t)b[0], (uint8_t)0xDE);
memcpy(&v32, b.data(), 4); QCOMPARE((uint8_t)b[1], (uint8_t)0xAD);
QCOMPARE(v32, (uint32_t)0xDEADBEEF); QCOMPARE((uint8_t)b[2], (uint8_t)0xBE);
QCOMPARE((uint8_t)b[3], (uint8_t)0xEF);
// Hex64 with space-separated bytes // Hex64 with space-separated bytes
b = fmt::parseValue(NodeKind::Hex64, "4D 5A 90 00 00 00 00 00", &ok); b = fmt::parseValue(NodeKind::Hex64, "4D 5A 90 00 00 00 00 00", &ok);
QVERIFY(ok); QVERIFY(ok);
QCOMPARE(b.size(), 8); QCOMPARE(b.size(), 8);
uint64_t v64; QCOMPARE((uint8_t)b[0], (uint8_t)0x4D);
memcpy(&v64, b.data(), 8); QCOMPARE((uint8_t)b[1], (uint8_t)0x5A);
QCOMPARE(v64, (uint64_t)0x4D5A900000000000ULL); QCOMPARE((uint8_t)b[7], (uint8_t)0x00);
// Hex64 continuous (should still work) // Hex64 continuous (should still work)
b = fmt::parseValue(NodeKind::Hex64, "4D5A900000000000", &ok); b = fmt::parseValue(NodeKind::Hex64, "4D5A900000000000", &ok);
QVERIFY(ok); QVERIFY(ok);
memcpy(&v64, b.data(), 8); QCOMPARE((uint8_t)b[0], (uint8_t)0x4D);
QCOMPARE(v64, (uint64_t)0x4D5A900000000000ULL); QCOMPARE((uint8_t)b[1], (uint8_t)0x5A);
// Hex64 with 0x prefix and spaces // Hex64 with 0x prefix and spaces
b = fmt::parseValue(NodeKind::Hex64, "0x4D 5A 90 00 00 00 00 00", &ok); b = fmt::parseValue(NodeKind::Hex64, "0x4D 5A 90 00 00 00 00 00", &ok);

View File

@@ -62,7 +62,7 @@ private slots:
n.name = "Test"; n.name = "Test";
QString s = fmt::fmtStructFooter(n, 0); QString s = fmt::fmtStructFooter(n, 0);
QVERIFY(s.contains("};")); QVERIFY(s.contains("};"));
QVERIFY(s.contains("Test")); // When no size, footer is just "};" without name
} }
void testIndent() { void testIndent() {
@@ -93,12 +93,14 @@ private slots:
void testParseValueHex32() { void testParseValueHex32() {
bool ok; bool ok;
// Hex parsing produces raw bytes (no endian conversion)
QByteArray b = fmt::parseValue(NodeKind::Hex32, "DEADBEEF", &ok); QByteArray b = fmt::parseValue(NodeKind::Hex32, "DEADBEEF", &ok);
QVERIFY(ok); QVERIFY(ok);
QCOMPARE(b.size(), 4); QCOMPARE(b.size(), 4);
uint32_t v; QCOMPARE((uint8_t)b[0], (uint8_t)0xDE);
memcpy(&v, b.data(), 4); QCOMPARE((uint8_t)b[1], (uint8_t)0xAD);
QCOMPARE(v, (uint32_t)0xDEADBEEF); QCOMPARE((uint8_t)b[2], (uint8_t)0xBE);
QCOMPARE((uint8_t)b[3], (uint8_t)0xEF);
} }
void testParseValueBool() { void testParseValueBool() {
@@ -119,12 +121,13 @@ private slots:
void testParseValueHex0xPrefix() { void testParseValueHex0xPrefix() {
bool ok; 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); QByteArray b = fmt::parseValue(NodeKind::Hex32, "0xDEADBEEF", &ok);
QVERIFY(ok); QVERIFY(ok);
uint32_t v; QCOMPARE((uint8_t)b[0], (uint8_t)0xDE);
memcpy(&v, b.data(), 4); QCOMPARE((uint8_t)b[1], (uint8_t)0xAD);
QCOMPARE(v, (uint32_t)0xDEADBEEF); QCOMPARE((uint8_t)b[2], (uint8_t)0xBE);
QCOMPARE((uint8_t)b[3], (uint8_t)0xEF);
// Pointer64 with 0x prefix // Pointer64 with 0x prefix
b = fmt::parseValue(NodeKind::Pointer64, "0x0000000000400000", &ok); b = fmt::parseValue(NodeKind::Pointer64, "0x0000000000400000", &ok);
@@ -229,8 +232,7 @@ private slots:
// With size // With size
QString s1 = fmt::fmtStructFooter(n, 0, 0x14); QString s1 = fmt::fmtStructFooter(n, 0, 0x14);
QVERIFY(s1.contains("};")); QVERIFY(s1.contains("};"));
QVERIFY(s1.contains("Test")); QVERIFY(s1.contains("sizeof(Test)=0x14"));
QVERIFY(s1.contains("sizeof=0x14"));
// Size 0 → no sizeof // Size 0 → no sizeof
QString s2 = fmt::fmtStructFooter(n, 0, 0); QString s2 = fmt::fmtStructFooter(n, 0, 0);