CommandRow: * icon, SRC: label, remove separator, clean margins

- Replace diamond with * for pixel-perfect fold alignment
- Add SRC: label prefix to command row source field
- Remove vertical bar separator from command row
- Clear footer margin text (no more ---)
- Remove + prefix from offset margin (0x instead of +0x)
- Remove codicon font infrastructure (use editor font chars)
This commit is contained in:
sysadmin
2026-02-05 17:25:51 -07:00
parent 6b9adf03fe
commit 82e1520ded
13 changed files with 690 additions and 353 deletions

View File

@@ -38,7 +38,9 @@ struct ComposeState {
void emitLine(const QString& lineText, LineMeta lm) {
if (currentLine > 0) text += '\n';
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
if (lm.foldHead)
if (lm.lineKind == LineKind::CommandRow)
text += QStringLiteral(" * ");
else if (lm.foldHead)
text += lm.foldCollapsed ? QStringLiteral(" + ") : QStringLiteral(" - ");
else
text += QStringLiteral(" ");
@@ -196,6 +198,10 @@ void composeParent(ComposeState& state, const NodeTree& tree,
// Header line (skip for array element structs - condensed display)
if (!isArrayChild) {
// Get per-scope widths for this header's parent scope
int typeW = state.effectiveTypeW(scopeId);
int nameW = state.effectiveNameW(scopeId);
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
@@ -209,21 +215,20 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.markerMask = (1u << M_STRUCT_BG);
lm.isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct && !state.baseEmitted);
if (lm.isRootHeader) state.baseEmitted = true;
lm.effectiveTypeW = typeW;
lm.effectiveNameW = nameW;
QString headerText;
if (node.kind == NodeKind::Array) {
// Array header with navigation: "uint32_t[16] name { <0/16>"
// Array header with navigation: "uint32_t[16] name {" (no brace when collapsed)
lm.isArrayHeader = true;
lm.elementKind = node.elementKind;
lm.arrayViewIdx = node.viewIndex;
lm.arrayCount = node.arrayLen;
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex);
} else if (lm.isRootHeader) {
// Root structs show base address
headerText = fmt::fmtStructHeaderWithBase(node, depth, tree.baseAddress);
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW);
} else {
// Nested structs show normal header
headerText = fmt::fmtStructHeader(node, depth);
// All structs (root and nested) use the same header format
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW);
}
state.emitLine(headerText, lm);
}
@@ -254,7 +259,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.depth = depth;
lm.lineKind = LineKind::Footer;
lm.nodeKind = node.kind;
lm.offsetText = QStringLiteral(" ---");
lm.offsetText.clear();
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
int sz = tree.structSpan(node.id, &state.childMap);
@@ -341,27 +346,30 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
state.absOffsets[i] = tree.computeOffset(i);
// Compute effective type column width from longest type name
// Include struct/array headers which use "struct TypeName" or "type[count]" format
int maxTypeLen = kMinTypeW;
for (const Node& node : tree.nodes) {
QString typeName;
if (node.kind == NodeKind::Array) {
// Array type: "int32_t[10]", "char[64]", etc.
typeName = fmt::arrayTypeName(node.elementKind, node.arrayLen);
} else if (node.kind == NodeKind::Struct) {
// Struct type: "struct TypeName" or "struct"
typeName = fmt::structTypeName(node);
} else {
typeName = fmt::typeNameRaw(node.kind);
}
maxTypeLen = qMax(maxTypeLen, typeName.size());
maxTypeLen = qMax(maxTypeLen, (int)typeName.size());
}
state.typeW = qBound(kMinTypeW, maxTypeLen, kMaxTypeW);
// Compute effective name column width from longest name
// Include struct/array names - they now use columnar layout too
int maxNameLen = kMinNameW;
for (const Node& node : tree.nodes) {
// Skip hex/padding (they show ASCII preview, not name column)
if (isHexPreview(node.kind)) continue;
// Skip containers (struct/array headers have different layout)
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue;
maxNameLen = qMax(maxNameLen, node.name.size());
maxNameLen = qMax(maxNameLen, (int)node.name.size());
}
state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
@@ -377,17 +385,19 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
for (int childIdx : state.childMap.value(container.id)) {
const Node& child = tree.nodes[childIdx];
// Skip containers - their headers don't use columnar layout
if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
continue;
// Type width - include struct/array headers too (they now use columnar layout)
QString childTypeName;
if (child.kind == NodeKind::Array)
childTypeName = fmt::arrayTypeName(child.elementKind, child.arrayLen);
else if (child.kind == NodeKind::Struct)
childTypeName = fmt::structTypeName(child);
else
childTypeName = fmt::typeNameRaw(child.kind);
scopeMaxType = qMax(scopeMaxType, (int)childTypeName.size());
// Type width
QString childTypeName = fmt::typeNameRaw(child.kind);
scopeMaxType = qMax(scopeMaxType, childTypeName.size());
// Name width (skip hex/padding)
// Name width (skip hex/padding, but include containers)
if (!isHexPreview(child.kind)) {
scopeMaxName = qMax(scopeMaxName, child.name.size());
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
}
}
@@ -396,26 +406,48 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
}
// Compute scope widths for root level (parentId == 0)
// Include struct/array headers - they now use columnar layout too
{
int rootMaxType = kMinTypeW;
int rootMaxName = kMinNameW;
for (int childIdx : state.childMap.value(0)) {
const Node& child = tree.nodes[childIdx];
// Skip containers - their headers don't use columnar layout
if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
continue;
// Type width - include struct/array headers
QString childTypeName;
if (child.kind == NodeKind::Array)
childTypeName = fmt::arrayTypeName(child.elementKind, child.arrayLen);
else if (child.kind == NodeKind::Struct)
childTypeName = fmt::structTypeName(child);
else
childTypeName = fmt::typeNameRaw(child.kind);
rootMaxType = qMax(rootMaxType, (int)childTypeName.size());
QString childTypeName = fmt::typeNameRaw(child.kind);
rootMaxType = qMax(rootMaxType, childTypeName.size());
// Name width (skip hex/padding, include containers)
if (!isHexPreview(child.kind)) {
rootMaxName = qMax(rootMaxName, child.name.size());
rootMaxName = qMax(rootMaxName, (int)child.name.size());
}
}
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, kMaxTypeW);
state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW);
}
// Emit CommandRow as line 0 (synthetic UI line)
{
LineMeta lm;
lm.nodeIdx = -1;
lm.nodeId = kCommandRowId;
lm.depth = 0;
lm.lineKind = LineKind::CommandRow;
lm.foldLevel = SC_FOLDLEVELBASE;
lm.foldHead = false;
lm.offsetText.clear();
lm.markerMask = 0;
lm.effectiveTypeW = state.typeW;
lm.effectiveNameW = state.nameW;
state.emitLine(QStringLiteral("SRC: File : 0x0"), lm);
}
QVector<int> roots = state.childMap.value(0);
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;

View File

@@ -2,6 +2,7 @@
#include <Qsci/qsciscintilla.h>
#include <QSplitter>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
@@ -9,12 +10,36 @@
#include <QInputDialog>
#include <QClipboard>
#include <QApplication>
#include <QFileDialog>
namespace rcx {
// Footer selection ID: set high bit to distinguish footer-only selections from node selections
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
static QString elide(QString s, int max) {
if (max <= 0) return {};
if (s.size() <= max) return s;
if (max == 1) return QStringLiteral("\u2026");
return s.left(max - 1) + QChar(0x2026);
}
static QString crumbFor(const rcx::NodeTree& t, uint64_t nodeId) {
QStringList parts;
QSet<uint64_t> seen;
uint64_t cur = nodeId;
while (cur != 0 && !seen.contains(cur)) {
seen.insert(cur);
int idx = t.indexOfId(cur);
if (idx < 0) break;
const auto& n = t.nodes[idx];
parts << (n.name.isEmpty() ? QStringLiteral("<unnamed>") : n.name);
cur = n.parentId;
}
std::reverse(parts.begin(), parts.end());
return parts.join(QStringLiteral(" > "));
}
// ── RcxDocument ──
RcxDocument::RcxDocument(QObject* parent)
@@ -62,6 +87,7 @@ void RcxDocument::loadData(const QString& binaryPath) {
return;
undoStack.clear();
provider = std::make_unique<FileProvider>(file.readAll());
dataPath = binaryPath;
tree.baseAddress = 0;
emit documentChanged();
}
@@ -102,6 +128,7 @@ RcxEditor* RcxController::addSplitEditor(QSplitter* splitter) {
if (!m_lastResult.text.isEmpty()) {
editor->applyDocument(m_lastResult);
}
updateCommandRow();
return editor;
}
@@ -127,7 +154,8 @@ void RcxController::connectEditor(RcxEditor* editor) {
// Inline editing signals
connect(editor, &RcxEditor::inlineEditCommitted,
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) {
if (nodeIdx < 0) { refresh(); return; }
// CommandRow BaseAddress/Source edit has nodeIdx=-1
if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source) { refresh(); return; }
switch (target) {
case EditTarget::Name: {
if (text.isEmpty()) break;
@@ -224,6 +252,15 @@ void RcxController::connectEditor(RcxEditor* editor) {
}
break;
}
case EditTarget::Source: {
if (text == QStringLiteral("File")) {
auto* w = qobject_cast<QWidget*>(parent());
QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)");
if (!path.isEmpty()) m_doc->loadData(path);
}
// "Process" is a placeholder — no action yet
break;
}
case EditTarget::ArrayIndex:
case EditTarget::ArrayCount:
// Array navigation removed - these cases are unreachable
@@ -242,8 +279,9 @@ void RcxController::refresh() {
// Prune stale selections (nodes removed by undo/redo/delete)
QSet<uint64_t> valid;
for (uint64_t id : m_selIds) {
if (m_doc->tree.indexOfId(id) >= 0)
valid.insert(id);
uint64_t nodeId = id & ~kFooterIdBit; // Strip footer bit for lookup
if (m_doc->tree.indexOfId(nodeId) >= 0)
valid.insert(id); // Keep original ID (with footer bit if present)
}
m_selIds = valid;
@@ -253,6 +291,7 @@ void RcxController::refresh() {
editor->restoreViewState(vs);
}
applySelectionOverlays();
updateCommandRow();
}
void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
@@ -445,7 +484,8 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
m_doc->provider->writeBytes(c.addr, bytes);
if (!m_doc->provider->writeBytes(c.addr, bytes))
qWarning() << "WriteBytes failed at address" << Qt::hex << c.addr;
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
int idx = tree.indexOfId(c.nodeId);
if (idx >= 0) {
@@ -732,6 +772,7 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
}
applySelectionOverlays();
updateCommandRow();
if (m_selIds.size() == 1) {
uint64_t sid = *m_selIds.begin();
@@ -745,6 +786,7 @@ void RcxController::clearSelection() {
m_selIds.clear();
m_anchorLine = -1;
applySelectionOverlays();
updateCommandRow();
}
void RcxController::applySelectionOverlays() {
@@ -752,6 +794,32 @@ void RcxController::applySelectionOverlays() {
editor->applySelectionOverlay(m_selIds);
}
void RcxController::updateCommandRow() {
QString src;
if (!m_doc->filePath.isEmpty())
src = QFileInfo(m_doc->filePath).fileName();
else
src = QStringLiteral("File");
if (!m_doc->dataPath.isEmpty())
src += QStringLiteral(" @ ") + QFileInfo(m_doc->dataPath).fileName();
QString addr = QStringLiteral("0x") +
QString::number(m_doc->tree.baseAddress, 16).toUpper();
QString path;
if (m_selIds.size() == 1) {
uint64_t sid = *m_selIds.begin();
int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit);
if (idx >= 0)
path = crumbFor(m_doc->tree, m_doc->tree.nodes[idx].id);
}
QString row = QStringLiteral(" * SRC: %1 : %2 %3")
.arg(elide(src, 40), elide(addr, 24), elide(path, 120));
for (auto* ed : m_editors)
ed->setCommandRowText(row);
}
void RcxController::handleMarginClick(RcxEditor* editor, int margin,
int line, Qt::KeyboardModifiers) {
const LineMeta* lm = editor->metaForLine(line);

View File

@@ -23,6 +23,7 @@ public:
std::unique_ptr<Provider> provider;
QUndoStack undoStack;
QString filePath;
QString dataPath;
bool modified = false;
ComposeResult compose() const;
@@ -95,6 +96,7 @@ private:
void connectEditor(RcxEditor* editor);
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
void updateCommandRow();
};
} // namespace rcx

View File

@@ -243,6 +243,7 @@ struct Node {
NodeKind elementKind = NodeKind::UInt8; // Array: element type
int viewIndex = 0; // Array: current view offset (transient)
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
int byteSize() const {
switch (kind) {
case NodeKind::UTF8: return strLen;
@@ -388,7 +389,14 @@ struct NodeTree {
}
int structSpan(uint64_t structId,
const QHash<uint64_t, QVector<int>>* childMap = nullptr) const {
const QHash<uint64_t, QVector<int>>* childMap = nullptr,
QSet<uint64_t>* visited = nullptr) const {
QSet<uint64_t> localVisited;
if (!visited) visited = &localVisited;
if (visited->contains(structId)) return 0; // Cycle detected
visited->insert(structId);
int idx = indexOfId(structId);
if (idx < 0) return 0;
@@ -400,7 +408,7 @@ struct NodeTree {
for (int ci : kids) {
const Node& c = nodes[ci];
int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array)
? structSpan(c.id, childMap) : c.byteSize();
? structSpan(c.id, childMap, visited) : c.byteSize();
int end = c.offset + sz;
if (end > maxEnd) maxEnd = end;
}
@@ -440,9 +448,14 @@ struct NodeTree {
// ── LineMeta ──
enum class LineKind : uint8_t {
CommandRow, // line 0 only, synthetic UI
Header, Field, Continuation, Footer, ArrayElementSeparator
};
static constexpr uint64_t kCommandRowId = UINT64_MAX;
static constexpr int kCommandRowLine = 0;
static constexpr int kFirstDataLine = 1;
struct LineMeta {
int nodeIdx = -1;
uint64_t nodeId = 0;
@@ -466,6 +479,10 @@ struct LineMeta {
int effectiveNameW = 22; // Per-line name column width used for rendering
};
inline bool isSyntheticLine(const LineMeta& lm) {
return lm.lineKind == LineKind::CommandRow;
}
// ── Layout Info ──
struct LayoutInfo {
@@ -513,7 +530,7 @@ struct ColumnSpan {
bool valid = false;
};
enum class EditTarget { Name, Type, Value, BaseAddress, ArrayIndex, ArrayCount };
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount };
// Column layout constants (shared with format.cpp span computation)
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
@@ -524,9 +541,9 @@ inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
inline constexpr int kSepWidth = 1;
inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t")
inline constexpr int kMaxTypeW = 14; // Maximum type column width (fits "uint64_t[999]")
inline constexpr int kMaxTypeW = 128; // Maximum type column width
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
inline constexpr int kMaxNameW = 22; // Maximum name column width (= kColName)
inline constexpr int kMaxNameW = 128; // Maximum name column width
inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) {
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
@@ -592,31 +609,29 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
return {start, lineLength, start < lineLength};
}
// Base address span (only valid for root struct headers)
// Line format: " - struct Name { // base: 0x00400000"
inline ColumnSpan baseAddressSpanFor(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {};
// Find "// base: " after the opening brace
int baseIdx = lineText.indexOf(QStringLiteral("// base: "));
if (baseIdx < 0) return {};
int startPos = baseIdx + 9; // after "// base: "
// Value goes to end of line
int endPos = lineText.size();
while (endPos > startPos && lineText[endPos-1].isSpace())
endPos--;
if (endPos <= startPos) return {};
return {startPos, endPos, true};
// ── CommandRow spans ──
// Line format: " * SRC: File : 0x140000000 path > here"
inline ColumnSpan commandRowSrcSpan(const QString& lineText) {
int idx = lineText.indexOf(QStringLiteral(" : "));
if (idx < 0) return {};
// Skip past "SRC: " label to expose just the source name
int srcTag = lineText.indexOf(QStringLiteral("SRC: "));
int start = (srcTag >= 0 && srcTag < idx) ? srcTag + 5 : 0;
while (start < idx && !lineText[start].isLetterOrNumber()) start++;
if (start >= idx) return {};
return {start, idx, true};
}
// Full "// base: 0x..." span for coloring (includes "// base: " prefix)
inline ColumnSpan baseAddressFullSpanFor(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {};
int baseIdx = lineText.indexOf(QStringLiteral("// base: "));
if (baseIdx < 0) return {};
int endPos = lineText.size();
while (endPos > baseIdx && lineText[endPos-1].isSpace())
endPos--;
return {baseIdx, endPos, true};
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
int idx = lineText.indexOf(QStringLiteral(" : "));
if (idx < 0) return {};
int start = idx + 3; // after " : "
int end = lineText.indexOf(QStringLiteral(" "), start); // next double-space
if (end < 0) end = lineText.size();
while (end > start && lineText[end-1].isSpace()) end--;
if (end <= start) return {};
return {start, end, true};
}
// ── Array navigation spans ──
@@ -683,11 +698,11 @@ namespace fmt {
QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine = 0,
const QString& comment = {}, int colType = kColType, int colName = kColName);
QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation);
QString fmtStructHeader(const Node& node, int depth);
QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress);
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation);
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName);
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
QString fmtArrayHeader(const Node& node, int depth, int viewIdx);
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName);
QString structTypeName(const Node& node); // Full type string for struct headers
QString arrayTypeName(NodeKind elemKind, int count);
QString validateBaseAddress(const QString& text);
QString indent(int depth);

View File

@@ -77,7 +77,9 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
connect(m_sci, &QsciScintilla::userListActivated,
this, [this](int id, const QString& text) {
if (id == 1 && m_editState.active && m_editState.target == EditTarget::Type) {
if (!m_editState.active) return;
if ((id == 1 && m_editState.target == EditTarget::Type) ||
(id == 2 && m_editState.target == EditTarget::Source)) {
auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text);
}
@@ -88,6 +90,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
connect(m_sci, &QsciScintilla::textChanged, this, [this]() {
if (!m_editState.active) return;
if (m_updatingComment) return; // Skip queuing during comment update
if (m_editState.target == EditTarget::Value)
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
if (m_editState.target == EditTarget::Type)
@@ -147,6 +150,7 @@ void RcxEditor::setupScintilla() {
IND_HOVER_SPAN, 17 /*INDIC_TEXTFORE*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_HOVER_SPAN, QColor("#3d9c8a"));
}
void RcxEditor::setupLexer() {
@@ -188,7 +192,7 @@ void RcxEditor::setupMargins() {
// Margin 0: Offset text
m_sci->setMarginType(0, QsciScintilla::TextMarginRightJustified);
m_sci->setMarginWidth(0, " +0x00000000 ");
m_sci->setMarginWidth(0, " 0x00000000 ");
m_sci->setMarginsBackgroundColor(kBgMargin);
m_sci->setMarginsForegroundColor(kFgMarginDim);
m_sci->setMarginSensitivity(0, true);
@@ -307,6 +311,7 @@ void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
m_sci->clearMarginText(-1);
for (int i = 0; i < meta.size(); i++) {
if (isSyntheticLine(meta[i])) continue;
const auto& lm = meta[i];
if (lm.offsetText.isEmpty()) continue;
@@ -324,6 +329,7 @@ void RcxEditor::applyMarkers(const QVector<LineMeta>& meta) {
m_sci->markerDeleteAll(m);
}
for (int i = 0; i < meta.size(); i++) {
if (isSyntheticLine(meta[i])) continue;
uint32_t mask = meta[i].markerMask;
for (int m = M_CONT; m <= M_STRUCT_BG; m++) {
if (mask & (1u << m)) {
@@ -391,6 +397,7 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
for (int i = 0; i < m_meta.size(); i++) {
if (isSyntheticLine(m_meta[i])) continue;
uint64_t nodeId = m_meta[i].nodeId;
bool isFooter = (m_meta[i].lineKind == LineKind::Footer);
@@ -511,16 +518,16 @@ static QString getLineText(QsciScintilla* sci, int line) {
void RcxEditor::applyBaseAddressColoring(const QVector<LineMeta>& meta) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_BASE_ADDR);
for (int i = 0; i < meta.size(); i++) {
const LineMeta& lm = meta[i];
if (!lm.isRootHeader) continue;
QString lineText = getLineText(m_sci, i);
ColumnSpan span = baseAddressFullSpanFor(lm, lineText);
if (!span.valid) continue;
long posA = posFromCol(m_sci, i, span.start);
long posB = posFromCol(m_sci, i, span.end);
if (posB > posA)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA);
// Color the ADDR span on CommandRow (line 0)
if (!meta.isEmpty() && meta[0].lineKind == LineKind::CommandRow) {
QString lineText = getLineText(m_sci, 0);
ColumnSpan span = commandRowAddrSpan(lineText);
if (span.valid) {
long posA = posFromCol(m_sci, 0, span.start);
long posB = posFromCol(m_sci, 0, span.end);
if (posB > posA)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA);
}
}
}
@@ -553,55 +560,63 @@ RcxEditor::EndEditInfo RcxEditor::endInlineEdit() {
// ── Span helpers ──
// Name span for struct/array headers
// Format: "struct TYPENAME NAME {" or "struct NAME {" or "type[N] NAME {"
// Returns span of the last word before " {"
// Name span for struct/array headers - uses column-based positioning
// Format: [fold][indent][type col][sep][name col][sep][suffix]
static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header) return {};
int bracePos = lineText.lastIndexOf(QStringLiteral(" {"));
if (bracePos <= 0) return {};
// Find the last space before " {" - the name starts after that
int nameStart = lineText.lastIndexOf(' ', bracePos - 1);
if (nameStart < 0) return {};
nameStart++; // Move past the space
int ind = kFoldCol + lm.depth * 3;
int typeW = lm.effectiveTypeW;
int nameW = lm.effectiveNameW;
int nameStart = ind + typeW + kSepWidth;
int nameEnd = nameStart + nameW;
// Clamp to line length
if (nameStart >= lineText.size()) return {};
if (nameEnd > lineText.size()) nameEnd = lineText.size();
// Don't allow editing array element names like "[0]", "[1]", etc.
QString name = lineText.mid(nameStart, bracePos - nameStart);
QString name = lineText.mid(nameStart, nameEnd - nameStart).trimmed();
if (name.startsWith('[') && name.endsWith(']'))
return {};
return {nameStart, bracePos, true};
return {nameStart, nameEnd, true};
}
// Type name span for struct headers (not arrays)
// Format: "struct TYPENAME NAME {" - returns span of TYPENAME
// Format: "struct TYPENAME NAME {" or collapsed variants
// For "struct NAME {" (no typename), returns invalid span
static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header) return {};
if (lm.isArrayHeader) return {}; // Arrays use arrayHeaderTypeSpan instead
int bracePos = lineText.lastIndexOf(QStringLiteral(" {"));
if (bracePos <= 0) return {};
int ind = kFoldCol + lm.depth * 3;
int typeW = lm.effectiveTypeW;
int typeEnd = ind + typeW;
// Clamp to actual line content
if (typeEnd > lineText.size()) typeEnd = lineText.size();
// Extract the type column text and check if it has a typename
// Format: "struct" or "struct TYPENAME"
QString typeCol = lineText.mid(ind, typeEnd - ind).trimmed();
// Find first space (after "struct")
int firstSpace = lineText.indexOf(' ', ind);
if (firstSpace <= ind || firstSpace >= bracePos) return {};
int firstSpace = typeCol.indexOf(' ');
if (firstSpace < 0) return {}; // Just "struct", no typename
// Find second space (after typename, before name)
int secondSpace = lineText.indexOf(' ', firstSpace + 1);
if (secondSpace <= firstSpace || secondSpace >= bracePos) return {}; // No typename
// If there's content after "struct ", that's the typename
QString typename_ = typeCol.mid(firstSpace + 1).trimmed();
if (typename_.isEmpty()) return {};
// Find third space (after name) - if exists, we have typename
int thirdSpace = lineText.indexOf(' ', secondSpace + 1);
if (thirdSpace < 0 || thirdSpace > bracePos) {
// Only two words: "struct NAME {" - no typename to edit
return {};
}
// Return span of the typename within the type column
int typenameStart = ind + firstSpace + 1;
// Find where the typename actually ends (skip padding)
int typenameEnd = typenameStart;
while (typenameEnd < typeEnd && lineText[typenameEnd] != ' ')
typenameEnd++;
// Three+ words: "struct TYPENAME NAME {" - return typename span
return {firstSpace + 1, secondSpace, true};
return {typenameStart, typenameEnd, true};
}
// Type span for array headers: "int32_t[10]" in "int32_t[10] positions {"
@@ -656,7 +671,21 @@ RcxEditor::NormalizedSpan RcxEditor::normalizeSpan(
bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
NormalizedSpan& out, QString* lineTextOut) const {
const LineMeta* lm = metaForLine(line);
if (!lm || lm->nodeIdx < 0) return false;
if (!lm) return false;
// CommandRow: BaseAddress (ADDR) and Source (SRC) editing
if (lm->lineKind == LineKind::CommandRow) {
if (t != EditTarget::BaseAddress && t != EditTarget::Source) return false;
QString lineText = getLineText(m_sci, line);
ColumnSpan s = (t == EditTarget::Source)
? commandRowSrcSpan(lineText)
: commandRowAddrSpan(lineText);
out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/(t == EditTarget::BaseAddress));
if (lineTextOut) *lineTextOut = lineText;
return out.valid;
}
if (lm->nodeIdx < 0) return false;
QString lineText = getLineText(m_sci, line);
int textLen = lineText.size();
@@ -670,7 +699,7 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
case EditTarget::Type: s = typeSpan(*lm, typeW); break;
case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break;
case EditTarget::Value: s = valueSpan(*lm, textLen, typeW, nameW); break;
case EditTarget::BaseAddress: s = baseAddressSpanFor(*lm, lineText); break;
case EditTarget::BaseAddress: break; // No longer on header lines
case EditTarget::ArrayIndex:
case EditTarget::ArrayCount:
break; // Array navigation removed
@@ -741,21 +770,28 @@ static bool hitTestTarget(QsciScintilla* sci,
const LineMeta& lm = meta[line];
// Array element separators are not interactive
if (lm.lineKind == LineKind::ArrayElementSeparator) return false;
// Use per-line effective widths from LineMeta
int typeW = lm.effectiveTypeW;
int nameW = lm.effectiveNameW;
auto inSpan = [&](const ColumnSpan& s) {
return s.valid && col >= s.start && col < s.end;
};
// CommandRow: SRC and ADDR fields are interactive
if (lm.lineKind == LineKind::CommandRow) {
ColumnSpan ss = commandRowSrcSpan(lineText);
if (inSpan(ss)) { outTarget = EditTarget::Source; outLine = line; return true; }
ColumnSpan as = commandRowAddrSpan(lineText);
if (inSpan(as)) { outTarget = EditTarget::BaseAddress; outLine = line; return true; }
return false;
}
// Use per-line effective widths from LineMeta
int typeW = lm.effectiveTypeW;
int nameW = lm.effectiveNameW;
ColumnSpan ts = RcxEditor::typeSpan(lm, typeW);
ColumnSpan ns = RcxEditor::nameSpan(lm, typeW, nameW);
ColumnSpan vs = RcxEditor::valueSpan(lm, textLen, typeW, nameW);
ColumnSpan bs = baseAddressSpanFor(lm, lineText); // Base address for root headers
// Fallback spans for header lines
if (!ts.valid) {
@@ -766,8 +802,7 @@ static bool hitTestTarget(QsciScintilla* sci,
if (!ns.valid)
ns = headerNameSpan(lm, lineText);
if (inSpan(bs)) outTarget = EditTarget::BaseAddress;
else if (inSpan(ts)) outTarget = EditTarget::Type;
if (inSpan(ts)) outTarget = EditTarget::Type;
else if (inSpan(ns)) outTarget = EditTarget::Name;
else if (inSpan(vs)) outTarget = EditTarget::Value;
else return false;
@@ -807,7 +842,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
case EditTarget::Type: raw = typeSpan(*lm, typeW); break;
case EditTarget::Name: raw = nameSpan(*lm, typeW, nameW); break;
case EditTarget::Value: raw = valueSpan(*lm, lineText.size(), typeW, nameW); break;
case EditTarget::BaseAddress: raw = baseAddressSpanFor(*lm, lineText); break;
case EditTarget::BaseAddress: raw = commandRowAddrSpan(lineText); break;
case EditTarget::Source: raw = commandRowSrcSpan(lineText); break;
case EditTarget::ArrayIndex: raw = arrayIndexSpanFor(*lm, lineText); break;
case EditTarget::ArrayCount: raw = arrayCountSpanFor(*lm, lineText); break;
}
@@ -845,6 +881,13 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
emit marginClicked(0, h.line, me->modifiers());
return true;
}
// CommandRow: try ADDR edit or consume
if (h.nodeId == kCommandRowId) {
int tLine; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t))
beginInlineEdit(t, tLine);
return true; // consume all CommandRow clicks
}
if (h.nodeId != 0) {
bool alreadySelected = m_currentSelIds.contains(h.nodeId);
bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier));
@@ -1024,8 +1067,12 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
case Qt::Key_PageUp:
case Qt::Key_PageDown:
return true; // block line navigation
case Qt::Key_Delete:
return true; // block to prevent eating trailing content
case Qt::Key_Delete: {
int line, col;
m_sci->getCursorPosition(&line, &col);
if (col >= editEndCol()) return true; // block at end
return false; // allow delete within span
}
case Qt::Key_Left:
case Qt::Key_Backspace: {
int line, col;
@@ -1067,7 +1114,11 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
int col;
m_sci->getCursorPosition(&line, &col);
auto* lm = metaForLine(line);
if (!lm || lm->nodeIdx < 0) return false;
if (!lm) return false;
// Allow nodeIdx=-1 only for CommandRow BaseAddress/Source editing
if (lm->nodeIdx < 0 && !(lm->lineKind == LineKind::CommandRow &&
(target == EditTarget::BaseAddress || target == EditTarget::Source)))
return false;
QString lineText;
NormalizedSpan norm;
@@ -1134,6 +1185,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
if (target == EditTarget::Type)
QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete);
if (target == EditTarget::Source)
QTimer::singleShot(0, this, &RcxEditor::showSourcePicker);
return true;
}
@@ -1147,9 +1200,8 @@ int RcxEditor::editEndCol() const {
void RcxEditor::clampEditSelection() {
if (!m_editState.active) return;
static bool s_clamping = false;
if (s_clamping) return;
s_clamping = true;
if (m_clampingSelection) return;
m_clampingSelection = true;
int selStartLine, selStartCol, selEndLine, selEndCol;
m_sci->getSelection(&selStartLine, &selStartCol, &selEndLine, &selEndCol);
@@ -1159,7 +1211,7 @@ void RcxEditor::clampEditSelection() {
// Don't fight cursor positioning - only clamp actual selections
if (isCursor) {
s_clamping = false;
m_clampingSelection = false;
return;
}
@@ -1170,7 +1222,7 @@ void RcxEditor::clampEditSelection() {
if (selStartLine != m_editState.line || selEndLine != m_editState.line) {
m_sci->setSelection(m_editState.line, m_editState.spanStart,
m_editState.line, editEnd);
s_clamping = false;
m_clampingSelection = false;
return;
}
@@ -1182,7 +1234,7 @@ void RcxEditor::clampEditSelection() {
if (clamped)
m_sci->setSelection(selStartLine, selStartCol, selEndLine, selEndCol);
s_clamping = false;
m_clampingSelection = false;
}
// ── Commit inline edit ──
@@ -1254,6 +1306,13 @@ void RcxEditor::showTypeListFiltered(const QString& filter) {
// Arrow cursor for popup is handled by applyHoverCursor() via isListActive()
}
void RcxEditor::showSourcePicker() {
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart);
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' ');
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
(uintptr_t)2, "File Process");
}
void RcxEditor::updateTypeListFilter() {
if (!m_editState.active || m_editState.target != EditTarget::Type)
return;
@@ -1279,9 +1338,19 @@ void RcxEditor::updateTypeListFilter() {
// ── Editable-field text-color indicator ──
void RcxEditor::paintEditableSpans(int line) {
const LineMeta* lm = metaForLine(line);
if (!lm) return;
// CommandRow: paint Source and BaseAddress spans
if (isSyntheticLine(*lm)) {
NormalizedSpan norm;
if (resolvedSpanFor(line, EditTarget::Source, norm))
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
if (resolvedSpanFor(line, EditTarget::BaseAddress, norm))
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
return;
}
NormalizedSpan norm;
for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value,
EditTarget::BaseAddress}) {
for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value}) {
if (resolvedSpanFor(line, t, norm))
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
}
@@ -1403,9 +1472,8 @@ void RcxEditor::setEditComment(const QString& comment) {
if (m_editState.commentCol < 0) return;
// Prevent re-entrancy from textChanged signal
static bool s_updating = false;
if (s_updating) return;
s_updating = true;
if (m_updatingComment) return;
m_updatingComment = true;
QString lineText = getLineText(m_sci, m_editState.line);
@@ -1414,7 +1482,7 @@ void RcxEditor::setEditComment(const QString& comment) {
int startCol = valueEnd + 2; // 2 spaces after value
int endCol = lineText.size();
int availWidth = endCol - startCol;
if (availWidth <= 0) { s_updating = false; return; }
if (availWidth <= 0) { m_updatingComment = false; return; }
// Format as "//<comment>" (no space after //)
QString formatted = QStringLiteral("//") + comment;
@@ -1434,7 +1502,7 @@ void RcxEditor::setEditComment(const QString& comment) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_BASE_ADDR);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA);
s_updating = false;
m_updatingComment = false;
}
void RcxEditor::validateEditLive() {
@@ -1443,7 +1511,9 @@ void RcxEditor::validateEditLive() {
int editedLen = m_editState.original.size() + delta;
QString text = (editedLen > 0)
? lineText.mid(m_editState.spanStart, editedLen).trimmed() : QString();
QString errorMsg = fmt::validateValue(m_editState.editKind, text);
QString errorMsg = (m_editState.target == EditTarget::BaseAddress)
? fmt::validateBaseAddress(text)
: fmt::validateValue(m_editState.editKind, text);
const LineMeta* lm = metaForLine(m_editState.line);
const bool isSelected = lm && m_currentSelIds.contains(lm->nodeId);
@@ -1466,6 +1536,35 @@ void RcxEditor::validateEditLive() {
}
}
void RcxEditor::setCommandRowText(const QString& line) {
if (m_sci->lines() <= 0) return;
QString s = line;
s.replace('\n', ' ');
s.replace('\r', ' ');
bool wasReadOnly = m_sci->isReadOnly();
bool wasModified = m_sci->SendScintilla(QsciScintillaBase::SCI_GETMODIFY);
long savedPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
long savedAnchor = m_sci->SendScintilla(QsciScintillaBase::SCI_GETANCHOR);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 0);
m_sci->setReadOnly(false);
long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0);
long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 0);
QByteArray utf8 = s.toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, start);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, end);
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData());
if (wasReadOnly) m_sci->setReadOnly(true);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 1);
if (!wasModified) m_sci->SendScintilla(QsciScintillaBase::SCI_SETSAVEPOINT);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCURRENTPOS, savedPos);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETANCHOR, savedAnchor);
m_sci->SendScintilla(QsciScintillaBase::SCI_COLOURISE, start, start + utf8.size());
}
void RcxEditor::setEditorFont(const QString& fontName) {
g_fontName = fontName;
QFont f = editorFont();

View File

@@ -37,6 +37,7 @@ public:
void cancelInlineEdit();
void applySelectionOverlay(const QSet<uint64_t>& selIds);
void setCommandRowText(const QString& line);
void setEditorFont(const QString& fontName);
static void setGlobalFontName(const QString& fontName);
@@ -98,6 +99,10 @@ private:
};
InlineEditState m_editState;
// ── Reentrancy guards ──
bool m_clampingSelection = false;
bool m_updatingComment = false;
void setupScintilla();
void setupLexer();
void setupMargins();
@@ -116,6 +121,7 @@ private:
bool handleNormalKey(QKeyEvent* ke);
bool handleEditKey(QKeyEvent* ke);
void showTypeAutocomplete();
void showSourcePicker();
void showTypeListFiltered(const QString& filter);
void updateTypeListFilter();
void paintEditableSpans(int line);

BIN
src/fonts/codicon.ttf Normal file

Binary file not shown.

View File

@@ -92,35 +92,30 @@ QString indent(int depth) {
// ── Offset margin ──
QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation) {
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation) {
if (isContinuation) return QStringLiteral(" \u00B7");
if (relativeOffset < 0)
return QStringLiteral("-0x") + QString::number(-relativeOffset, 16).toUpper();
return QStringLiteral("+0x") + QString::number(relativeOffset, 16).toUpper();
return QStringLiteral("0x") + QString::number(absoluteOffset, 16).toUpper();
}
// ── Struct type name (for width calculation) ──
QString structTypeName(const Node& node) {
// Full type string: "struct TypeName" or just "struct" if no typename
QString base = typeName(node.kind).trimmed(); // "struct"
if (!node.structTypeName.isEmpty())
return base + QStringLiteral(" ") + node.structTypeName;
return base;
}
// ── Struct header / footer ──
QString fmtStructHeader(const Node& node, int depth) {
// Format: "struct TypeName name {" or "struct name {" if no type name
QString type = typeName(node.kind).trimmed();
if (!node.structTypeName.isEmpty())
return indent(depth) + type + QStringLiteral(" ") + node.structTypeName +
QStringLiteral(" ") + node.name + QStringLiteral(" {");
return indent(depth) + type + QStringLiteral(" ") + node.name + QStringLiteral(" {");
}
QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress) {
// Format: "struct TypeName Name { // base: 0x..." or "struct Name { // base: 0x..."
QString type = typeName(node.kind).trimmed();
QString header;
if (!node.structTypeName.isEmpty())
header = indent(depth) + type + QStringLiteral(" ") + node.structTypeName +
QStringLiteral(" ") + node.name + QStringLiteral(" { ");
else
header = indent(depth) + type + QStringLiteral(" ") + node.name + QStringLiteral(" { ");
QString baseHex = QStringLiteral("0x") + QString::number(baseAddress, 16).toUpper();
return header + QStringLiteral("// base: ") + baseHex;
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName) {
// Columnar format: <type> <name> { (or no brace when collapsed)
QString ind = indent(depth);
QString type = fit(structTypeName(node), colType);
QString name = fit(node.name, colName);
QString suffix = collapsed ? QString() : QStringLiteral("{");
return ind + type + SEP + name + SEP + suffix;
}
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
@@ -128,10 +123,13 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
}
// ── Array header ──
// Format: "uint32_t[16] myArray {" (like struct header, no fixed columns)
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/) {
QString type = arrayTypeName(node.elementKind, node.arrayLen);
return indent(depth) + type + QStringLiteral(" ") + node.name + QStringLiteral(" {");
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName) {
QString ind = indent(depth);
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen), colType);
QString name = fit(node.name, colName);
QString suffix = collapsed ? QString() : QStringLiteral("{");
return ind + type + SEP + name + SEP + suffix;
}
// ── Hex / ASCII preview ──
@@ -189,6 +187,10 @@ enum class ValueMode { Display, Editable };
static QString readValueImpl(const Node& node, const Provider& prov,
uint64_t addr, int subLine, ValueMode mode) {
int sz = node.byteSize();
if (sz > 0 && !prov.isReadable(addr, sz))
return (mode == ValueMode::Display) ? QStringLiteral("???") : QString();
const bool display = (mode == ValueMode::Display);
switch (node.kind) {
case NodeKind::Hex8: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
@@ -396,16 +398,31 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
switch (kind) {
case NodeKind::Hex8: return parseHexBytes(stripHex(s), 1, ok);
case NodeKind::Hex16: {
uint val = stripHex(s).toUInt(ok, 16);
QString cleaned = stripHex(s);
// Space-separated bytes → raw byte order (display order preserved)
if (cleaned.contains(' '))
return parseHexBytes(cleaned, 2, ok);
// Single value → native-endian
uint val = cleaned.toUInt(ok, 16);
if (*ok && val > 0xFFFF) *ok = false;
return *ok ? toBytes<uint16_t>(static_cast<uint16_t>(val)) : QByteArray{};
}
case NodeKind::Hex32: {
uint val = stripHex(s).toUInt(ok, 16);
QString cleaned = stripHex(s);
// Space-separated bytes → raw byte order (display order preserved)
if (cleaned.contains(' '))
return parseHexBytes(cleaned, 4, ok);
// Single value → native-endian
uint val = cleaned.toUInt(ok, 16);
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
}
case NodeKind::Hex64: {
qulonglong val = stripHex(s).toULongLong(ok, 16);
QString cleaned = stripHex(s);
// Space-separated bytes → raw byte order (display order preserved)
if (cleaned.contains(' '))
return parseHexBytes(cleaned, 8, ok);
// Single value → native-endian
qulonglong val = cleaned.toULongLong(ok, 16);
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
}
case NodeKind::Int8: {
@@ -453,7 +470,7 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* 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: { 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: { 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::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return parseIntChecked<uint32_t>(val, ok); }
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: {
QString n = s; n.replace(',', '.'); // Accept EU decimal separator

View File

@@ -440,7 +440,7 @@ void MainWindow::newFile() {
};
// ── Root: IMAGE_DOS_HEADER ──
uint64_t dosId = addStruct(0, 0x00, "IMAGE_DOS_HEADER", "dosHeader");
uint64_t dosId = addStruct(0, 0x00, "IMAGE_DOS_HEADER", "DosHeader");
addField(dosId, 0x00, NodeKind::UInt16, "e_magic");
addField(dosId, 0x02, NodeKind::UInt16, "e_cblp");
addField(dosId, 0x04, NodeKind::UInt16, "e_cp");
@@ -458,10 +458,10 @@ void MainWindow::newFile() {
addField(dosId, 0x3C, NodeKind::UInt32, "e_lfanew");
// ── PE Signature ──
addField(0, peOff, NodeKind::UInt32, "PE_Signature");
addField(0, peOff, NodeKind::UInt32, "Signature");
// ── IMAGE_FILE_HEADER ──
uint64_t fhId = addStruct(0, fhOff, "IMAGE_FILE_HEADER", "fileHeader");
uint64_t fhId = addStruct(0, fhOff, "IMAGE_FILE_HEADER", "FileHeader");
addField(fhId, 0, NodeKind::UInt16, "Machine");
addField(fhId, 2, NodeKind::UInt16, "NumberOfSections");
addField(fhId, 4, NodeKind::UInt32, "TimeDateStamp");
@@ -471,7 +471,7 @@ void MainWindow::newFile() {
addField(fhId, 18, NodeKind::UInt16, "Characteristics");
// ── IMAGE_OPTIONAL_HEADER64 ──
uint64_t ohId = addStruct(0, ohOff, "IMAGE_OPTIONAL_HEADER64", "optionalHeader");
uint64_t ohId = addStruct(0, ohOff, "IMAGE_OPTIONAL_HEADER64", "OptionalHeader");
addField(ohId, 0, NodeKind::UInt16, "Magic");
addField(ohId, 2, NodeKind::UInt8, "MajorLinkerVersion");
addField(ohId, 3, NodeKind::UInt8, "MinorLinkerVersion");
@@ -483,8 +483,8 @@ void MainWindow::newFile() {
addField(ohId, 24, NodeKind::UInt64, "ImageBase");
addField(ohId, 32, NodeKind::UInt32, "SectionAlignment");
addField(ohId, 36, NodeKind::UInt32, "FileAlignment");
addField(ohId, 40, NodeKind::UInt16, "MajorOSVersion");
addField(ohId, 42, NodeKind::UInt16, "MinorOSVersion");
addField(ohId, 40, NodeKind::UInt16, "MajorOperatingSystemVersion");
addField(ohId, 42, NodeKind::UInt16, "MinorOperatingSystemVersion");
addField(ohId, 44, NodeKind::UInt16, "MajorImageVersion");
addField(ohId, 46, NodeKind::UInt16, "MinorImageVersion");
addField(ohId, 48, NodeKind::UInt16, "MajorSubsystemVersion");
@@ -540,6 +540,13 @@ void MainWindow::newFile() {
addField(secId, 36, NodeKind::UInt32, "Characteristics");
}
// ── Hex64 fields after headers ──
const int tailOff = shOff + 4 * 40; // 0x228
addField(0, tailOff + 0, NodeKind::Hex64, "RawData0");
addField(0, tailOff + 8, NodeKind::Hex64, "RawData1");
addField(0, tailOff + 16, NodeKind::Hex64, "RawData2");
addField(0, tailOff + 24, NodeKind::Hex64, "RawData3");
createTab(doc);
}
@@ -715,11 +722,10 @@ int main(int argc, char* argv[]) {
app.setOrganizationName("ReclassX");
app.setStyle("Fusion"); // Fusion style respects dark palette well
// Load embedded Iosevka font
// Load embedded fonts
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");

View File

@@ -5,5 +5,6 @@
</qresource>
<qresource prefix="/fonts">
<file alias="Iosevka-Regular.ttf">fonts/Iosevka-Regular.ttf</file>
<file alias="codicon.ttf">fonts/codicon.ttf</file>
</qresource>
</RCC>

View File

@@ -35,27 +35,30 @@ private slots:
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Header + 2 fields + footer = 4 lines
QCOMPARE(result.meta.size(), 4);
// CommandRow + Header + 2 fields + footer = 5 lines
QCOMPARE(result.meta.size(), 5);
// Line 0 is CommandRow
QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow);
// Header is fold head
QVERIFY(result.meta[0].foldHead);
QCOMPARE(result.meta[0].lineKind, LineKind::Header);
QVERIFY(result.meta[1].foldHead);
QCOMPARE(result.meta[1].lineKind, LineKind::Header);
// Fields are not fold heads
QVERIFY(!result.meta[1].foldHead);
QVERIFY(!result.meta[2].foldHead);
QVERIFY(!result.meta[3].foldHead);
// Footer
QCOMPARE(result.meta[3].lineKind, LineKind::Footer);
QCOMPARE(result.meta[4].lineKind, LineKind::Footer);
// Offset text
QCOMPARE(result.meta[0].offsetText, QString("+0x0"));
QCOMPARE(result.meta[1].offsetText, QString("+0x0"));
QCOMPARE(result.meta[2].offsetText, QString("+0x4"));
QCOMPARE(result.meta[1].offsetText, QString("0x0"));
QCOMPARE(result.meta[2].offsetText, QString("0x0"));
QCOMPARE(result.meta[3].offsetText, QString("0x4"));
// Header is expanded by default (fold indicator in line text)
QVERIFY(!result.meta[0].foldCollapsed);
QVERIFY(!result.meta[1].foldCollapsed);
}
void testVec3Continuation() {
@@ -79,22 +82,22 @@ private slots:
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Header + 3 Vec3 lines + footer = 5 lines
QCOMPARE(result.meta.size(), 5);
// CommandRow + Header + 3 Vec3 lines + footer = 6 lines
QCOMPARE(result.meta.size(), 6);
// Line 1 (first Vec3 component): not continuation
QVERIFY(!result.meta[1].isContinuation);
QCOMPARE(result.meta[1].offsetText, QString("+0x0"));
// Line 2 (first Vec3 component): not continuation
QVERIFY(!result.meta[2].isContinuation);
QCOMPARE(result.meta[2].offsetText, QString("0x0"));
// Lines 2-3: continuation
QVERIFY(result.meta[2].isContinuation);
QCOMPARE(result.meta[2].offsetText, QString(" \u00B7"));
// Lines 3-4: continuation
QVERIFY(result.meta[3].isContinuation);
QCOMPARE(result.meta[3].offsetText, QString(" \u00B7"));
QVERIFY(result.meta[4].isContinuation);
QCOMPARE(result.meta[4].offsetText, QString(" \u00B7"));
// Continuation marker
QVERIFY(result.meta[2].markerMask & (1u << M_CONT));
QVERIFY(result.meta[3].markerMask & (1u << M_CONT));
QVERIFY(result.meta[4].markerMask & (1u << M_CONT));
}
void testPaddingMarker() {
@@ -118,9 +121,9 @@ private slots:
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Header + padding + footer = 3
QCOMPARE(result.meta.size(), 3);
QVERIFY(result.meta[1].markerMask & (1u << M_PAD));
// CommandRow + Header + padding + footer = 4
QCOMPARE(result.meta.size(), 4);
QVERIFY(result.meta[2].markerMask & (1u << M_PAD));
}
void testNullPointerMarker() {
@@ -146,8 +149,8 @@ private slots:
FileProvider prov(data);
ComposeResult result = compose(tree, prov);
QCOMPARE(result.meta.size(), 3);
QVERIFY(result.meta[1].markerMask & (1u << M_PTR0));
QCOMPARE(result.meta.size(), 4);
QVERIFY(result.meta[2].markerMask & (1u << M_PTR0));
}
void testCollapsedStruct() {
@@ -172,9 +175,10 @@ private slots:
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Collapsed: header + footer only = 2 lines
// Collapsed: CommandRow + header only (no children, no footer)
QCOMPARE(result.meta.size(), 2);
QVERIFY(result.meta[0].foldHead);
QVERIFY(result.meta[1].foldHead);
QVERIFY(result.meta[1].foldCollapsed);
}
void testUnreadablePointerNoRead() {
@@ -201,10 +205,10 @@ private slots:
FileProvider prov(data);
ComposeResult result = compose(tree, prov);
QCOMPARE(result.meta.size(), 3);
QCOMPARE(result.meta.size(), 4);
// Should have M_ERR, should NOT have M_PTR0
QVERIFY(result.meta[1].markerMask & (1u << M_ERR));
QVERIFY(!(result.meta[1].markerMask & (1u << M_PTR0)));
QVERIFY(result.meta[2].markerMask & (1u << M_ERR));
QVERIFY(!(result.meta[2].markerMask & (1u << M_PTR0)));
}
void testFoldLevels() {
@@ -237,16 +241,16 @@ private slots:
ComposeResult result = compose(tree, prov);
// Root header (depth 0, head) -> 0x400 | 0x2000
QCOMPARE(result.meta[0].foldLevel, 0x400 | 0x2000);
QCOMPARE(result.meta[0].depth, 0);
QCOMPARE(result.meta[1].foldLevel, 0x400 | 0x2000);
QCOMPARE(result.meta[1].depth, 0);
// Child header (depth 1, head) -> 0x401 | 0x2000
QCOMPARE(result.meta[1].foldLevel, 0x401 | 0x2000);
QCOMPARE(result.meta[1].depth, 1);
QCOMPARE(result.meta[2].foldLevel, 0x401 | 0x2000);
QCOMPARE(result.meta[2].depth, 1);
// Leaf (depth 2, not head) -> 0x402
QCOMPARE(result.meta[2].foldLevel, 0x402);
QCOMPARE(result.meta[2].depth, 2);
QCOMPARE(result.meta[3].foldLevel, 0x402);
QCOMPARE(result.meta[3].depth, 2);
}
void testNestedStruct() {
@@ -293,36 +297,36 @@ private slots:
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Outer header + flags + Inner header + x + y + Inner footer + Outer footer = 7
QCOMPARE(result.meta.size(), 7);
// CommandRow + Outer header + flags + Inner header + x + y + Inner footer + Outer footer = 8
QCOMPARE(result.meta.size(), 8);
// Outer header
QCOMPARE(result.meta[0].lineKind, LineKind::Header);
QCOMPARE(result.meta[0].depth, 0);
QVERIFY(result.meta[0].foldHead);
QCOMPARE(result.meta[1].lineKind, LineKind::Header);
QCOMPARE(result.meta[1].depth, 0);
QVERIFY(result.meta[1].foldHead);
// flags field
QCOMPARE(result.meta[1].lineKind, LineKind::Field);
QCOMPARE(result.meta[1].depth, 1);
QCOMPARE(result.meta[2].lineKind, LineKind::Field);
QCOMPARE(result.meta[2].depth, 1);
// Inner header
QCOMPARE(result.meta[2].lineKind, LineKind::Header);
QCOMPARE(result.meta[2].depth, 1);
QVERIFY(result.meta[2].foldHead);
QCOMPARE(result.meta[2].foldLevel, 0x401 | 0x2000);
QCOMPARE(result.meta[3].lineKind, LineKind::Header);
QCOMPARE(result.meta[3].depth, 1);
QVERIFY(result.meta[3].foldHead);
QCOMPARE(result.meta[3].foldLevel, 0x401 | 0x2000);
// Inner fields at depth 2
QCOMPARE(result.meta[3].depth, 2);
QCOMPARE(result.meta[3].foldLevel, 0x402);
QCOMPARE(result.meta[4].depth, 2);
QCOMPARE(result.meta[4].foldLevel, 0x402);
QCOMPARE(result.meta[5].depth, 2);
// Inner footer
QCOMPARE(result.meta[5].lineKind, LineKind::Footer);
QCOMPARE(result.meta[5].depth, 1);
QCOMPARE(result.meta[6].lineKind, LineKind::Footer);
QCOMPARE(result.meta[6].depth, 1);
// Outer footer
QCOMPARE(result.meta[6].lineKind, LineKind::Footer);
QCOMPARE(result.meta[6].depth, 0);
QCOMPARE(result.meta[7].lineKind, LineKind::Footer);
QCOMPARE(result.meta[7].depth, 0);
}
void testPointerDerefExpansion() {
@@ -390,40 +394,40 @@ private slots:
ComposeResult result = compose(tree, prov);
// Main: header + magic + ptr(fold head) + VTable header + fn1 + fn2 + VTable footer + Main footer = 8
// CommandRow + Main: header + magic + ptr(fold head) + VTable header + fn1 + fn2 + VTable footer + Main footer = 9
// VTable standalone: header + fn1 + fn2 + footer = 4
// Total = 12
QCOMPARE(result.meta.size(), 12);
// Total = 13
QCOMPARE(result.meta.size(), 13);
// Main header
QCOMPARE(result.meta[0].lineKind, LineKind::Header);
QCOMPARE(result.meta[0].depth, 0);
QCOMPARE(result.meta[1].lineKind, LineKind::Header);
QCOMPARE(result.meta[1].depth, 0);
// magic field
QCOMPARE(result.meta[1].lineKind, LineKind::Field);
QCOMPARE(result.meta[1].depth, 1);
// Pointer as fold head
QCOMPARE(result.meta[2].lineKind, LineKind::Field);
QCOMPARE(result.meta[2].depth, 1);
QVERIFY(result.meta[2].foldHead);
QCOMPARE(result.meta[2].nodeKind, NodeKind::Pointer64);
// Pointer as fold head
QCOMPARE(result.meta[3].lineKind, LineKind::Field);
QCOMPARE(result.meta[3].depth, 1);
QVERIFY(result.meta[3].foldHead);
QCOMPARE(result.meta[3].nodeKind, NodeKind::Pointer64);
// Expanded VTable header at depth 2
QCOMPARE(result.meta[3].lineKind, LineKind::Header);
QCOMPARE(result.meta[3].depth, 2);
QCOMPARE(result.meta[4].lineKind, LineKind::Header);
QCOMPARE(result.meta[4].depth, 2);
// Expanded fields at depth 3
QCOMPARE(result.meta[4].depth, 3);
QCOMPARE(result.meta[5].depth, 3);
QCOMPARE(result.meta[6].depth, 3);
// Expanded VTable footer
QCOMPARE(result.meta[6].lineKind, LineKind::Footer);
QCOMPARE(result.meta[6].depth, 2);
QCOMPARE(result.meta[7].lineKind, LineKind::Footer);
QCOMPARE(result.meta[7].depth, 2);
// Main footer
QCOMPARE(result.meta[7].lineKind, LineKind::Footer);
QCOMPARE(result.meta[7].depth, 0);
QCOMPARE(result.meta[8].lineKind, LineKind::Footer);
QCOMPARE(result.meta[8].depth, 0);
}
void testPointerDerefNull() {
@@ -467,18 +471,18 @@ private slots:
ComposeResult result = compose(tree, prov);
// Main: header + ptr(fold head, no expansion) + footer = 3
// CommandRow + Main: header + ptr(fold head, no expansion) + footer = 4
// Target standalone: header + field + footer = 3
// Total = 6
QCOMPARE(result.meta.size(), 6);
// Total = 7
QCOMPARE(result.meta.size(), 7);
// Pointer is fold head but has no children (null ptr)
QCOMPARE(result.meta[1].lineKind, LineKind::Field);
QVERIFY(result.meta[1].foldHead);
QCOMPARE(result.meta[2].lineKind, LineKind::Field);
QVERIFY(result.meta[2].foldHead);
// Next line is Main footer (no expansion)
QCOMPARE(result.meta[2].lineKind, LineKind::Footer);
QCOMPARE(result.meta[2].depth, 0);
QCOMPARE(result.meta[3].lineKind, LineKind::Footer);
QCOMPARE(result.meta[3].depth, 0);
}
void testPointerDerefCollapsed() {
@@ -525,17 +529,17 @@ private slots:
ComposeResult result = compose(tree, prov);
// Main: header + ptr(fold head, collapsed) + footer = 3
// CommandRow + Main: header + ptr(fold head, collapsed) + footer = 4
// Target standalone: header + field + footer = 3
// Total = 6
QCOMPARE(result.meta.size(), 6);
// Total = 7
QCOMPARE(result.meta.size(), 7);
// Pointer is fold head
QVERIFY(result.meta[1].foldHead);
QVERIFY(result.meta[2].foldHead);
// No expansion — next is Main footer
QCOMPARE(result.meta[2].lineKind, LineKind::Footer);
QCOMPARE(result.meta[2].depth, 0);
QCOMPARE(result.meta[3].lineKind, LineKind::Footer);
QCOMPARE(result.meta[3].depth, 0);
}
void testPointerDerefCycle() {
@@ -598,14 +602,14 @@ private slots:
QVERIFY(result.meta.size() > 0);
QVERIFY(result.meta.size() < 100); // sanity: bounded output
// First expansion happens: Main header + ptr fold head + Recursive header + data + backPtr fold head
// First expansion happens: CommandRow + Main header + ptr fold head + Recursive header + data + backPtr fold head
// Second expansion blocked by cycle guard: no children under backPtr
// Then: Recursive footer + Main footer
// Plus standalone Recursive rendering
// The exact count depends on cycle guard behavior but must be finite
QCOMPARE(result.meta[0].lineKind, LineKind::Header); // Main header
QVERIFY(result.meta[1].foldHead); // ptr fold head
QCOMPARE(result.meta[2].lineKind, LineKind::Header); // Recursive header (expansion)
QCOMPARE(result.meta[1].lineKind, LineKind::Header); // Main header
QVERIFY(result.meta[2].foldHead); // ptr fold head
QCOMPARE(result.meta[3].lineKind, LineKind::Header); // Recursive header (expansion)
}
void testStructFooterSimple() {
@@ -655,6 +659,12 @@ private slots:
ComposeResult result = compose(tree, prov);
for (int i = 0; i < result.meta.size(); i++) {
// Skip CommandRow (synthetic line with sentinel nodeId)
if (result.meta[i].lineKind == LineKind::CommandRow) {
QCOMPARE(result.meta[i].nodeId, kCommandRowId);
QCOMPARE(result.meta[i].nodeIdx, -1);
continue;
}
QVERIFY2(result.meta[i].nodeId != 0,
qPrintable(QString("Line %1 has nodeId=0").arg(i)));
int ni = result.meta[i].nodeIdx;

View File

@@ -25,43 +25,89 @@ static FileProvider makeTestProvider() {
return FileProvider(data);
}
// Build a tree covering 0x6000 bytes with Hex64 fields
// Build a PE-like test tree with IMAGE_FILE_HEADER fields
static NodeTree makeTestTree() {
NodeTree tree;
tree.baseAddress = 0;
tree.baseAddress = 0x140000000;
// Root struct: IMAGE_FILE_HEADER
Node root;
root.kind = NodeKind::Struct;
root.name = "TestStruct";
root.structTypeName = "IMAGE_FILE_HEADER";
root.name = "FileHeader";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// First two fields for existing tests
Node f1;
f1.kind = NodeKind::UInt16;
f1.name = "field_u16";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
int offset = 0;
Node f2;
f2.kind = NodeKind::Hex64;
f2.name = "field_hex";
f2.parentId = rootId;
f2.offset = 8;
tree.addNode(f2);
// IMAGE_FILE_HEADER fields (matches Windows PE format)
Node machine;
machine.kind = NodeKind::UInt16;
machine.name = "Machine";
machine.parentId = rootId;
machine.offset = offset;
tree.addNode(machine);
offset += 2;
// Fill remaining 0x6000 bytes with Hex64 fields (8 bytes each)
// Start at offset 16 (0x10), go to 0x6000
for (int off = 0x10; off < 0x6000; off += 8) {
Node f;
f.kind = NodeKind::Hex64;
f.name = QString("data_%1").arg(off, 4, 16, QChar('0'));
f.parentId = rootId;
f.offset = off;
tree.addNode(f);
Node numSections;
numSections.kind = NodeKind::UInt16;
numSections.name = "NumberOfSections";
numSections.parentId = rootId;
numSections.offset = offset;
tree.addNode(numSections);
offset += 2;
Node timestamp;
timestamp.kind = NodeKind::Hex32;
timestamp.name = "TimeDateStamp";
timestamp.parentId = rootId;
timestamp.offset = offset;
tree.addNode(timestamp);
offset += 4;
Node ptrSymbols;
ptrSymbols.kind = NodeKind::Hex32;
ptrSymbols.name = "PointerToSymbolTable";
ptrSymbols.parentId = rootId;
ptrSymbols.offset = offset;
tree.addNode(ptrSymbols);
offset += 4;
Node numSymbols;
numSymbols.kind = NodeKind::UInt32;
numSymbols.name = "NumberOfSymbols";
numSymbols.parentId = rootId;
numSymbols.offset = offset;
tree.addNode(numSymbols);
offset += 4;
Node optHeaderSize;
optHeaderSize.kind = NodeKind::UInt16;
optHeaderSize.name = "SizeOfOptionalHeader";
optHeaderSize.parentId = rootId;
optHeaderSize.offset = offset;
tree.addNode(optHeaderSize);
offset += 2;
Node characteristics;
characteristics.kind = NodeKind::Hex16;
characteristics.name = "Characteristics";
characteristics.parentId = rootId;
characteristics.offset = offset;
tree.addNode(characteristics);
offset += 2;
// 8 Hex64 fields for additional test coverage
for (int i = 0; i < 8; i++) {
Node hex;
hex.kind = NodeKind::Hex64;
hex.name = QString("Reserved%1").arg(i);
hex.parentId = rootId;
hex.offset = offset;
tree.addNode(hex);
offset += 8;
}
return tree;
@@ -90,16 +136,50 @@ private slots:
delete m_editor;
}
// ── Test: CommandRow at line 0 rejects non-ADDR edits ──
void testCommandRowLineRejectsEdits() {
m_editor->applyDocument(m_result);
// Line 0 should be the CommandRow
const LineMeta* lm = m_editor->metaForLine(0);
QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::CommandRow);
QCOMPARE(lm->nodeId, kCommandRowId);
QCOMPARE(lm->nodeIdx, -1);
// Type/Name/Value should be rejected on CommandRow
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Type, 0));
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Name, 0));
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, 0));
QVERIFY(!m_editor->isEditing());
// Set CommandRow text with an ADDR value (simulates controller.updateCommandRow)
m_editor->setCommandRowText(
QStringLiteral(" * SRC: File : 0x140000000"));
// BaseAddress should be ALLOWED on CommandRow (ADDR field)
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
QVERIFY2(ok, "BaseAddress edit should be allowed on CommandRow");
QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit();
// Source should be ALLOWED on CommandRow (SRC field)
ok = m_editor->beginInlineEdit(EditTarget::Source, 0);
QVERIFY2(ok, "Source edit should be allowed on CommandRow");
QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit();
}
// ── Test: inline edit lifecycle (begin → commit → re-edit) ──
void testInlineEditReEntry() {
// Move cursor to line 1 (first field inside struct)
m_editor->scintilla()->setCursorPosition(1, 0);
// Move cursor to line 2 (first field inside struct; line 0=CommandRow, 1=header)
m_editor->scintilla()->setCursorPosition(2, 0);
// Should not be editing
QVERIFY(!m_editor->isEditing());
// Begin edit on Name column
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 1);
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
@@ -111,7 +191,7 @@ private slots:
m_editor->applyDocument(m_result);
// Should be able to edit again
ok = m_editor->beginInlineEdit(EditTarget::Name, 1);
ok = m_editor->beginInlineEdit(EditTarget::Name, 2);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
@@ -123,10 +203,10 @@ private slots:
// ── Test: commit inline edit then re-edit same line ──
void testCommitThenReEdit() {
m_editor->applyDocument(m_result);
m_editor->scintilla()->setCursorPosition(1, 0);
m_editor->scintilla()->setCursorPosition(2, 0);
// Begin value edit
bool ok = m_editor->beginInlineEdit(EditTarget::Value, 1);
bool ok = m_editor->beginInlineEdit(EditTarget::Value, 2);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
@@ -143,7 +223,7 @@ private slots:
m_editor->applyDocument(m_result);
// Must be able to edit the same line again
ok = m_editor->beginInlineEdit(EditTarget::Value, 1);
ok = m_editor->beginInlineEdit(EditTarget::Value, 2);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
@@ -154,7 +234,7 @@ private slots:
void testMouseClickCommitsEdit() {
m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 1);
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
@@ -179,7 +259,7 @@ private slots:
m_editor->scintilla()->setFocus();
QApplication::processEvents();
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 1);
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
@@ -207,7 +287,7 @@ private slots:
m_editor->applyDocument(m_result);
// Begin type edit on a field line
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
@@ -226,22 +306,23 @@ private slots:
QVERIFY(!m_editor->isEditing());
}
// ── Test: edit on header line (Name is valid, Type/Value invalid) ──
// ── Test: edit on header line (Name and Type valid, Value invalid) ──
void testHeaderLineEdit() {
m_editor->applyDocument(m_result);
// Line 0 should be the struct header
const LineMeta* lm = m_editor->metaForLine(0);
// Line 1 should be the struct header (line 0 is CommandRow)
const LineMeta* lm = m_editor->metaForLine(1);
QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::Header);
// Type edit on header should fail (no type span)
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 0);
QVERIFY(!ok);
QVERIFY(!m_editor->isEditing());
// Type edit on header should succeed (has typename IMAGE_FILE_HEADER)
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit();
// Name edit on header should succeed (dynamic span)
ok = m_editor->beginInlineEdit(EditTarget::Name, 0);
// Name edit on header should succeed
ok = m_editor->beginInlineEdit(EditTarget::Name, 1);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit();
@@ -271,7 +352,7 @@ private slots:
void testTypeAutocompleteShows() {
m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2);
QVERIFY(ok);
// Process deferred timer (autocomplete is deferred)
@@ -313,11 +394,12 @@ private slots:
QCOMPARE((uint8_t)b[1], (uint8_t)0x5A);
QCOMPARE((uint8_t)b[7], (uint8_t)0x00);
// Hex64 continuous (should still work)
// Hex64 continuous - stores as native-endian (numeric value preserved)
b = fmt::parseValue(NodeKind::Hex64, "4D5A900000000000", &ok);
QVERIFY(ok);
QCOMPARE((uint8_t)b[0], (uint8_t)0x4D);
QCOMPARE((uint8_t)b[1], (uint8_t)0x5A);
uint64_t v64;
memcpy(&v64, b.data(), 8);
QCOMPARE(v64, (uint64_t)0x4D5A900000000000);
// Hex64 with 0x prefix and spaces
b = fmt::parseValue(NodeKind::Hex64, "0x4D 5A 90 00 00 00 00 00", &ok);
@@ -328,7 +410,7 @@ private slots:
void testTypeAutocompleteTypingAndCommit() {
m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2);
QVERIFY(ok);
// Process deferred autocomplete
@@ -368,7 +450,7 @@ private slots:
void testTypeEditClickAwayNoChange() {
m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2);
QVERIFY(ok);
// Process deferred autocomplete
@@ -397,8 +479,8 @@ private slots:
void testColumnSpanHitTest() {
m_editor->applyDocument(m_result);
// Line 1 is a field line (UInt16), verify spans are valid
const LineMeta* lm = m_editor->metaForLine(1);
// Line 2 is a field line (UInt16), verify spans are valid (line 0=CommandRow, 1=header)
const LineMeta* lm = m_editor->metaForLine(2);
QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::Field);
@@ -415,7 +497,7 @@ private slots:
// Value span should be valid for field lines
QString lineText;
int len = (int)m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1);
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)2);
QVERIFY(len > 0);
ColumnSpan vs = RcxEditor::valueSpan(*lm, len);
QVERIFY(vs.valid);
@@ -444,20 +526,19 @@ private slots:
void testSelectedNodeIndices() {
m_editor->applyDocument(m_result);
// Put cursor on first field line
m_editor->scintilla()->setCursorPosition(1, 0);
// Put cursor on first field line (line 2; 0=CommandRow, 1=header)
m_editor->scintilla()->setCursorPosition(2, 0);
QSet<int> sel = m_editor->selectedNodeIndices();
QCOMPARE(sel.size(), 1);
// The node index should match the first field
const LineMeta* lm = m_editor->metaForLine(1);
const LineMeta* lm = m_editor->metaForLine(2);
QVERIFY(lm);
QVERIFY(sel.contains(lm->nodeIdx));
}
// ── Test: base address changes affect header display ──
// ── Test: header line no longer contains "// base:" ──
void testBaseAddressDisplay() {
// Create tree with base address 0x10
NodeTree tree = makeTestTree();
tree.baseAddress = 0x10;
FileProvider prov = makeTestProvider();
@@ -465,50 +546,45 @@ private slots:
m_editor->applyDocument(result);
// Line 0 should be the struct header with isRootHeader=true
const LineMeta* lm = m_editor->metaForLine(0);
// Line 1 should be the struct header (line 0 is CommandRow)
const LineMeta* lm = m_editor->metaForLine(1);
QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::Header);
QVERIFY(lm->isRootHeader);
// Get header line text - should contain "0x10"
// Get header line text should NOT contain "// base:" (consolidated into cmd bar)
QString lineText;
int len = (int)m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0);
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1);
if (len > 0) {
QByteArray buf(len + 1, '\0');
m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_GETLINE, (unsigned long)0, (void*)buf.data());
QsciScintillaBase::SCI_GETLINE, (unsigned long)1, (void*)buf.data());
lineText = QString::fromUtf8(buf.constData(), len).trimmed();
}
// Verify base address appears in header
QVERIFY2(lineText.contains("0x10") || lineText.contains("0X10"),
qPrintable("Header should contain base address 0x10, got: " + lineText));
// Verify struct keyword is present
QVERIFY2(!lineText.contains("// base:"),
qPrintable("Header should no longer contain '// base:', got: " + lineText));
QVERIFY2(lineText.contains("struct"),
qPrintable("Header should contain 'struct', got: " + lineText));
// Reset to original result
m_editor->applyDocument(m_result);
}
// ── Test: base address span is valid for root headers ──
// ── Test: CommandRow ADDR span is valid ──
void testBaseAddressSpan() {
NodeTree tree = makeTestTree();
tree.baseAddress = 0x140000000; // Large address to test span width
FileProvider prov = makeTestProvider();
ComposeResult result = compose(tree, prov);
m_editor->applyDocument(m_result);
m_editor->applyDocument(result);
// Set CommandRow text with ADDR value (simulates controller)
m_editor->setCommandRowText(
QStringLiteral(" * SRC: File : 0x140000000"));
// Line 0 should be root header
// Line 0 is CommandRow
const LineMeta* lm = m_editor->metaForLine(0);
QVERIFY(lm);
QVERIFY(lm->isRootHeader);
QCOMPARE(lm->lineKind, LineKind::CommandRow);
// Get line text for span calculation
// Get CommandRow line text
QString lineText;
int len = (int)m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0);
@@ -521,32 +597,30 @@ private slots:
lineText.chop(1);
}
// Base address span should be valid
ColumnSpan bs = baseAddressSpanFor(*lm, lineText);
QVERIFY2(bs.valid, "Base address span should be valid for root header");
QVERIFY(bs.start < bs.end);
// ADDR span should be valid (uses commandRowAddrSpan)
ColumnSpan as = commandRowAddrSpan(lineText);
QVERIFY2(as.valid, "ADDR span should be valid on CommandRow");
QVERIFY(as.start < as.end);
// The span should cover the hex address
QString spanText = lineText.mid(bs.start, bs.end - bs.start);
QString spanText = lineText.mid(as.start, as.end - as.start);
QVERIFY2(spanText.contains("0x") || spanText.startsWith("0X"),
qPrintable("Span should contain hex address, got: " + spanText));
// Reset
m_editor->applyDocument(m_result);
}
// ── Test: base address edit begins on root header ──
// ── Test: base address edit begins on CommandRow (line 0) ──
void testBaseAddressEditBegins() {
NodeTree tree = makeTestTree();
tree.baseAddress = 0x10;
FileProvider prov = makeTestProvider();
ComposeResult result = compose(tree, prov);
m_editor->applyDocument(m_result);
m_editor->applyDocument(result);
// Set CommandRow text with ADDR value (simulates controller)
m_editor->setCommandRowText(
QStringLiteral(" * SRC: File : 0x140000000"));
// Begin base address edit on line 0 (root header)
// Begin base address edit on line 0 (CommandRow ADDR field)
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
QVERIFY2(ok, "Should be able to begin base address edit on root header");
QVERIFY2(ok, "Should be able to begin base address edit on CommandRow");
QVERIFY(m_editor->isEditing());
// Cancel and reset

View File

@@ -39,8 +39,8 @@ private slots:
}
void testFmtOffsetMargin_primary() {
QCOMPARE(fmt::fmtOffsetMargin(0x10, false), QString("+0x10"));
QCOMPARE(fmt::fmtOffsetMargin(0, false), QString("+0x0"));
QCOMPARE(fmt::fmtOffsetMargin(0x10, false), QString("0x10"));
QCOMPARE(fmt::fmtOffsetMargin(0, false), QString("0x0"));
}
void testFmtOffsetMargin_continuation() {
@@ -51,10 +51,17 @@ private slots:
Node n;
n.kind = NodeKind::Struct;
n.name = "Test";
QString s = fmt::fmtStructHeader(n, 0);
// Expanded header should contain opening brace
QString s = fmt::fmtStructHeader(n, 0, /*collapsed=*/false);
QVERIFY(s.contains("struct"));
QVERIFY(s.contains("Test"));
QVERIFY(s.contains("{"));
// Collapsed header should not contain opening brace
QString collapsed = fmt::fmtStructHeader(n, 0, /*collapsed=*/true);
QVERIFY(collapsed.contains("struct"));
QVERIFY(collapsed.contains("Test"));
QVERIFY(!collapsed.contains("{"));
}
void testFmtStructFooter() {