Initial commit: ReclassX structured binary editor

This commit is contained in:
sysadmin
2026-02-01 11:37:32 -07:00
commit 0be67c8396
786 changed files with 473499 additions and 0 deletions

323
src/compose.cpp Normal file
View File

@@ -0,0 +1,323 @@
#include "core.h"
#include <algorithm>
namespace rcx {
namespace {
// Scintilla fold constants (avoid including Scintilla headers in core)
constexpr int SC_FOLDLEVELBASE = 0x400;
constexpr int SC_FOLDLEVELHEADERFLAG = 0x2000;
constexpr uint64_t kGoldenRatio = 0x9E3779B97F4A7C15ULL;
struct ComposeState {
QString text;
QVector<LineMeta> meta;
QSet<uint64_t> visiting; // cycle detection for struct recursion
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
int currentLine = 0;
// Precomputed for O(1) lookups
QHash<uint64_t, QVector<int>> childMap;
QVector<int64_t> absOffsets; // indexed by node index
void emitLine(const QString& lineText, LineMeta lm) {
if (currentLine > 0) text += '\n';
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
if (lm.foldHead)
text += lm.foldCollapsed ? QStringLiteral(" + ") : QStringLiteral(" - ");
else
text += QStringLiteral(" ");
text += lineText;
meta.append(lm);
currentLine++;
}
};
int computeFoldLevel(int depth, bool isHead) {
int level = SC_FOLDLEVELBASE + depth;
if (isHead) level |= SC_FOLDLEVELHEADERFLAG;
return level;
}
uint32_t computeMarkers(const Node& node, const Provider& prov,
uint64_t addr, bool isCont, int depth) {
uint32_t mask = 0;
if (isCont) mask |= (1u << M_CONT);
if (node.kind == NodeKind::Padding) mask |= (1u << M_PAD);
if (prov.isValid()) {
int sz = node.byteSize();
if (sz > 0 && !prov.isReadable(addr, sz)) {
mask |= (1u << M_ERR);
} else if (sz > 0) {
if (node.kind == NodeKind::Pointer32 && prov.readU32(addr) == 0)
mask |= (1u << M_PTR0);
if (node.kind == NodeKind::Pointer64 && prov.readU64(addr) == 0)
mask |= (1u << M_PTR0);
}
}
return mask;
}
static inline uint64_t ptrToProviderAddr(const NodeTree& tree, uint64_t ptr) {
if (tree.baseAddress && ptr >= tree.baseAddress) return ptr - tree.baseAddress;
return ptr;
}
static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) {
int64_t total = 0;
QSet<int> visited;
int cur = idx;
while (cur >= 0 && cur < tree.nodes.size()) {
if (visited.contains(cur)) break;
visited.insert(cur);
const Node& n = tree.nodes[cur];
if (n.id == rootId) break;
total += n.offset;
if (n.parentId == 0) break;
cur = tree.indexOfId(n.parentId);
}
return total;
}
static inline uint64_t resolveAddr(const ComposeState& state,
const NodeTree& tree,
int nodeIdx,
uint64_t base, uint64_t rootId) {
if (rootId != 0)
return base + relOffsetFromRoot(tree, nodeIdx, rootId);
return state.absOffsets[nodeIdx];
}
void composeLeaf(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx,
int depth, uint64_t absAddr) {
const Node& node = tree.nodes[nodeIdx];
// Line count: padding wraps at 8 bytes per line
int numLines;
if (node.kind == NodeKind::Padding) {
int totalBytes = qMax(1, node.arrayLen);
numLines = (totalBytes + 7) / 8;
} else {
numLines = linesForKind(node.kind);
}
for (int sub = 0; sub < numLines; sub++) {
bool isCont = (sub > 0);
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.subLine = sub;
lm.depth = depth;
lm.isContinuation = isCont;
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
lm.nodeKind = node.kind;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont);
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
lm.foldLevel = computeFoldLevel(depth, false);
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub);
state.emitLine(lineText, lm);
}
}
// Forward declarations (base/rootId default to 0 = use precomputed offsets)
void composeNode(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx, int depth,
uint64_t base = 0, uint64_t rootId = 0);
void composeParent(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx, int depth,
uint64_t base = 0, uint64_t rootId = 0);
void composeParent(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx, int depth,
uint64_t base, uint64_t rootId) {
const Node& node = tree.nodes[nodeIdx];
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId);
// Cycle detection
if (state.visiting.contains(node.id)) {
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Field;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
lm.nodeKind = node.kind;
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
lm.foldLevel = computeFoldLevel(depth, false);
state.emitLine(fmt::indent(depth) + QStringLiteral("/* CYCLE: ") +
node.name + QStringLiteral(" */"), lm);
return;
}
state.visiting.insert(node.id);
// Header line
{
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
lm.nodeKind = node.kind;
lm.foldHead = true;
lm.foldCollapsed = node.collapsed;
lm.foldLevel = computeFoldLevel(depth, true);
lm.markerMask = (1u << M_STRUCT_BG);
state.emitLine(fmt::fmtStructHeader(node, depth), lm);
}
if (!node.collapsed) {
QVector<int> children = state.childMap.value(node.id);
std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
for (int childIdx : children) {
composeNode(state, tree, prov, childIdx, depth + 1, base, rootId);
}
}
// Footer line
{
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Footer;
lm.nodeKind = node.kind;
lm.offsetText = QStringLiteral(" ---");
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = (1u << M_STRUCT_BG);
int sz = tree.structSpan(node.id, &state.childMap);
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
}
state.visiting.remove(node.id);
}
void composeNode(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx, int depth,
uint64_t base, uint64_t rootId) {
const Node& node = tree.nodes[nodeIdx];
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId);
// Pointer deref expansion
if ((node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64)
&& node.refId != 0) {
{
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Field;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
lm.nodeKind = node.kind;
lm.foldHead = true;
lm.foldCollapsed = node.collapsed;
lm.foldLevel = computeFoldLevel(depth, true);
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
state.emitLine(fmt::fmtNodeLine(node, prov, absAddr, depth, 0), lm);
}
if (!node.collapsed) {
int sz = node.byteSize();
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
uint64_t ptrVal = (node.kind == NodeKind::Pointer32)
? (uint64_t)prov.readU32(absAddr) : prov.readU64(absAddr);
if (ptrVal != 0) {
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
if (!state.ptrVisiting.contains(key)) {
state.ptrVisiting.insert(key);
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
const Node& ref = tree.nodes[refIdx];
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array)
composeParent(state, tree, prov, refIdx,
depth + 1, pBase, ref.id);
}
state.ptrVisiting.remove(key);
}
}
}
}
return;
}
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
composeParent(state, tree, prov, nodeIdx, depth, base, rootId);
} else {
composeLeaf(state, tree, prov, nodeIdx, depth, absAddr);
}
}
} // anonymous namespace
ComposeResult compose(const NodeTree& tree, const Provider& prov) {
ComposeState state;
// Precompute parent→children map
for (int i = 0; i < tree.nodes.size(); i++)
state.childMap[tree.nodes[i].parentId].append(i);
// Precompute absolute offsets
state.absOffsets.resize(tree.nodes.size());
for (int i = 0; i < tree.nodes.size(); i++)
state.absOffsets[i] = tree.computeOffset(i);
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;
});
for (int idx : roots) {
composeNode(state, tree, prov, idx, 0);
}
return { state.text, state.meta };
}
QSet<uint64_t> NodeTree::normalizePreferAncestors(const QSet<uint64_t>& ids) const {
QSet<uint64_t> result;
for (uint64_t id : ids) {
int idx = indexOfId(id);
if (idx < 0) continue;
bool ancestorSelected = false;
uint64_t cur = nodes[idx].parentId;
QSet<uint64_t> visited;
while (cur != 0 && !visited.contains(cur)) {
visited.insert(cur);
if (ids.contains(cur)) { ancestorSelected = true; break; }
int pi = indexOfId(cur);
if (pi < 0) break;
cur = nodes[pi].parentId;
}
if (!ancestorSelected)
result.insert(id);
}
return result;
}
QSet<uint64_t> NodeTree::normalizePreferDescendants(const QSet<uint64_t>& ids) const {
QSet<uint64_t> result;
for (uint64_t id : ids) {
QVector<int> sub = subtreeIndices(id);
bool hasSelectedDescendant = false;
for (int si : sub) {
uint64_t sid = nodes[si].id;
if (sid != id && ids.contains(sid)) {
hasSelectedDescendant = true;
break;
}
}
if (!hasSelectedDescendant)
result.insert(id);
}
return result;
}
} // namespace rcx

565
src/controller.cpp Normal file
View File

@@ -0,0 +1,565 @@
#include "controller.h"
#include <QSplitter>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QMenu>
#include <QInputDialog>
#include <QClipboard>
#include <QApplication>
namespace rcx {
// ── RcxDocument ──
RcxDocument::RcxDocument(QObject* parent)
: QObject(parent)
, provider(std::make_unique<NullProvider>())
{
connect(&undoStack, &QUndoStack::cleanChanged, this, [this](bool clean) {
modified = !clean;
});
}
ComposeResult RcxDocument::compose() const {
return rcx::compose(tree, *provider);
}
bool RcxDocument::save(const QString& path) {
QJsonObject json = tree.toJson();
QJsonDocument jdoc(json);
QFile file(path);
if (!file.open(QIODevice::WriteOnly))
return false;
file.write(jdoc.toJson(QJsonDocument::Indented));
filePath = path;
undoStack.setClean();
modified = false;
return true;
}
bool RcxDocument::load(const QString& path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return false;
undoStack.clear();
QJsonDocument jdoc = QJsonDocument::fromJson(file.readAll());
tree = NodeTree::fromJson(jdoc.object());
filePath = path;
modified = false;
emit documentChanged();
return true;
}
void RcxDocument::loadData(const QString& binaryPath) {
QFile file(binaryPath);
if (!file.open(QIODevice::ReadOnly))
return;
undoStack.clear();
provider = std::make_unique<FileProvider>(file.readAll());
tree.baseAddress = 0;
emit documentChanged();
}
void RcxDocument::loadData(const QByteArray& data) {
undoStack.clear();
provider = std::make_unique<FileProvider>(data);
tree.baseAddress = 0;
emit documentChanged();
}
// ── RcxCommand ──
RcxCommand::RcxCommand(RcxController* ctrl, Command cmd)
: m_ctrl(ctrl), m_cmd(cmd) {}
void RcxCommand::undo() { m_ctrl->applyCommand(m_cmd, true); }
void RcxCommand::redo() { m_ctrl->applyCommand(m_cmd, false); }
// ── RcxController ──
RcxController::RcxController(RcxDocument* doc, QWidget* parent)
: QObject(parent), m_doc(doc)
{
connect(m_doc, &RcxDocument::documentChanged, this, &RcxController::refresh);
}
RcxEditor* RcxController::primaryEditor() const {
return m_editors.isEmpty() ? nullptr : m_editors.first();
}
RcxEditor* RcxController::addSplitEditor(QSplitter* splitter) {
auto* editor = new RcxEditor(splitter);
splitter->addWidget(editor);
m_editors.append(editor);
connectEditor(editor);
if (!m_lastResult.text.isEmpty()) {
editor->applyDocument(m_lastResult);
}
return editor;
}
void RcxController::removeSplitEditor(RcxEditor* editor) {
m_editors.removeOne(editor);
editor->deleteLater();
}
void RcxController::connectEditor(RcxEditor* editor) {
connect(editor, &RcxEditor::marginClicked,
this, [this, editor](int margin, int line, Qt::KeyboardModifiers mods) {
handleMarginClick(editor, margin, line, mods);
});
connect(editor, &RcxEditor::contextMenuRequested,
this, [this, editor](int line, int nodeIdx, int subLine, QPoint globalPos) {
showContextMenu(editor, line, nodeIdx, subLine, globalPos);
});
connect(editor, &RcxEditor::nodeClicked,
this, [this, editor](int line, uint64_t nodeId, Qt::KeyboardModifiers mods) {
handleNodeClick(editor, line, nodeId, mods);
});
// Inline editing signals
connect(editor, &RcxEditor::inlineEditCommitted,
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) {
if (nodeIdx < 0) { refresh(); return; }
switch (target) {
case EditTarget::Name:
if (!text.isEmpty()) renameNode(nodeIdx, text);
break;
case EditTarget::Type: {
bool ok;
NodeKind k = kindFromTypeName(text, &ok);
if (ok) changeNodeKind(nodeIdx, k);
break;
}
case EditTarget::Value:
setNodeValue(nodeIdx, subLine, text);
break;
}
// Always refresh to restore canonical text (handles parse failures, no-ops, etc.)
refresh();
});
connect(editor, &RcxEditor::inlineEditCancelled,
this, [this]() { refresh(); });
}
void RcxController::refresh() {
m_lastResult = m_doc->compose();
// 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);
}
m_selIds = valid;
for (auto* editor : m_editors) {
ViewState vs = editor->saveViewState();
editor->applyDocument(m_lastResult);
editor->restoreViewState(vs);
}
applySelectionOverlays();
}
void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
auto& node = m_doc->tree.nodes[nodeIdx];
int oldSize = node.byteSize();
// Compute what byteSize() would be with the new kind
Node tmp = node;
tmp.kind = newKind;
int newSize = tmp.byteSize();
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}));
}
void RcxController::renameNode(int nodeIdx, const QString& newName) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
auto& node = m_doc->tree.nodes[nodeIdx];
m_doc->undoStack.push(new RcxCommand(this,
cmd::Rename{node.id, node.name, newName}));
}
void RcxController::insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name) {
Node n;
n.kind = kind;
n.name = name;
n.parentId = parentId;
if (offset < 0) {
// Auto-place after last sibling with alignment
int maxEnd = 0;
auto siblings = m_doc->tree.childrenOf(parentId);
for (int si : siblings) {
auto& sn = m_doc->tree.nodes[si];
int sz = (sn.kind == NodeKind::Struct || sn.kind == NodeKind::Array)
? m_doc->tree.structSpan(sn.id) : sn.byteSize();
int end = sn.offset + sz;
if (end > maxEnd) maxEnd = end;
}
int align = alignmentFor(kind);
n.offset = (maxEnd + align - 1) / align * align;
} else {
n.offset = offset;
}
// Assign ID before storing
n.id = m_doc->tree.m_nextId;
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
}
void RcxController::removeNode(int nodeIdx) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
uint64_t nodeId = m_doc->tree.nodes[nodeIdx].id;
QVector<int> indices = m_doc->tree.subtreeIndices(nodeId);
QVector<Node> subtree;
for (int i : indices)
subtree.append(m_doc->tree.nodes[i]);
m_doc->undoStack.push(new RcxCommand(this,
cmd::Remove{nodeId, subtree}));
}
void RcxController::toggleCollapse(int nodeIdx) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
auto& node = m_doc->tree.nodes[nodeIdx];
m_doc->undoStack.push(new RcxCommand(this,
cmd::Collapse{node.id, node.collapsed, !node.collapsed}));
}
void RcxController::applyCommand(const Command& command, bool isUndo) {
auto& tree = m_doc->tree;
std::visit([&](auto&& c) {
using T = std::decay_t<decltype(c)>;
if constexpr (std::is_same_v<T, cmd::ChangeKind>) {
int idx = tree.indexOfId(c.nodeId);
if (idx >= 0)
tree.nodes[idx].kind = isUndo ? c.oldKind : c.newKind;
for (const auto& adj : c.offAdjs) {
int ai = tree.indexOfId(adj.nodeId);
if (ai >= 0)
tree.nodes[ai].offset = isUndo ? adj.oldOffset : adj.newOffset;
}
} else if constexpr (std::is_same_v<T, cmd::Rename>) {
int idx = tree.indexOfId(c.nodeId);
if (idx >= 0)
tree.nodes[idx].name = isUndo ? c.oldName : c.newName;
} else if constexpr (std::is_same_v<T, cmd::Collapse>) {
int idx = tree.indexOfId(c.nodeId);
if (idx >= 0)
tree.nodes[idx].collapsed = isUndo ? c.oldState : c.newState;
} else if constexpr (std::is_same_v<T, cmd::Insert>) {
if (isUndo) {
int idx = tree.indexOfId(c.node.id);
if (idx >= 0) {
tree.nodes.remove(idx);
tree.invalidateIdCache();
}
} else {
tree.addNode(c.node);
}
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
if (isUndo) {
for (const Node& n : c.subtree)
tree.addNode(n);
} else {
QVector<int> indices = tree.subtreeIndices(c.nodeId);
std::sort(indices.begin(), indices.end(), std::greater<int>());
for (int idx : indices)
tree.nodes.remove(idx);
tree.invalidateIdCache();
}
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
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);
}
}, command);
refresh();
}
void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
if (!m_doc->provider->isWritable()) return;
const Node& node = m_doc->tree.nodes[nodeIdx];
uint64_t addr = m_doc->tree.computeOffset(nodeIdx);
// For vector sub-components, redirect to float parsing at sub-offset
NodeKind editKind = node.kind;
if ((node.kind == NodeKind::Vec2 || node.kind == NodeKind::Vec3 ||
node.kind == NodeKind::Vec4) && subLine >= 0) {
addr += subLine * 4;
editKind = NodeKind::Float;
}
bool ok;
QByteArray newBytes = fmt::parseValue(editKind, text, &ok);
if (!ok) return;
// For strings, pad/truncate to full buffer size
if (node.kind == NodeKind::UTF8 || node.kind == NodeKind::UTF16) {
int fullSize = node.byteSize();
newBytes = newBytes.left(fullSize);
if (newBytes.size() < fullSize)
newBytes.append(QByteArray(fullSize - newBytes.size(), '\0'));
}
if (newBytes.isEmpty()) return;
int writeSize = newBytes.size();
// Validate write range before pushing command
if (!m_doc->provider->isReadable(addr, writeSize)) return;
QByteArray oldBytes = m_doc->provider->readBytes(addr, writeSize);
m_doc->undoStack.push(new RcxCommand(this,
cmd::WriteBytes{addr, oldBytes, newBytes}));
}
void RcxController::duplicateNode(int nodeIdx) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
const Node& src = m_doc->tree.nodes[nodeIdx];
if (src.kind == NodeKind::Struct || src.kind == NodeKind::Array) return;
insertNode(src.parentId, src.offset + src.byteSize(), src.kind, src.name + "_copy");
}
void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
int subLine, const QPoint& globalPos) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
uint64_t clickedId = m_doc->tree.nodes[nodeIdx].id;
// Right-click selection policy: if not in selection, select only this node
if (!m_selIds.contains(clickedId)) {
m_selIds.clear();
m_selIds.insert(clickedId);
m_anchorLine = line;
applySelectionOverlays();
}
// Multi-select batch menu
if (m_selIds.size() > 1) {
QMenu menu;
int count = m_selIds.size();
QSet<uint64_t> ids = m_selIds;
menu.addAction(QString("Delete %1 nodes").arg(count), [this, ids]() {
QVector<int> indices;
for (uint64_t id : ids) {
int idx = m_doc->tree.indexOfId(id);
if (idx >= 0) indices.append(idx);
}
batchRemoveNodes(indices);
});
menu.addAction(QString("Change type of %1 nodes...").arg(count),
[this, ids]() {
QStringList types;
for (const auto& e : kKindMeta) types << e.name;
bool ok;
QString sel = QInputDialog::getItem(nullptr, "Change Type", "Type:",
types, 0, false, &ok);
if (ok) {
QVector<int> indices;
for (uint64_t id : ids) {
int idx = m_doc->tree.indexOfId(id);
if (idx >= 0) indices.append(idx);
}
batchChangeKind(indices, kindFromString(sel));
}
});
menu.exec(globalPos);
return;
}
const Node& node = m_doc->tree.nodes[nodeIdx];
uint64_t nodeId = node.id;
uint64_t parentId = node.parentId;
QMenu menu;
// Inline edit actions — position cursor on the right-clicked line
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
&& node.kind != NodeKind::Padding && node.kind != NodeKind::Mat4x4
&& m_doc->provider->isWritable();
if (isEditable) {
menu.addAction("Edit &Value", [editor, line]() {
editor->beginInlineEdit(EditTarget::Value, line);
});
}
menu.addAction("Re&name", [editor, line]() {
editor->beginInlineEdit(EditTarget::Name, line);
});
menu.addAction("Change &Type", [editor, line]() {
editor->beginInlineEdit(EditTarget::Type, line);
});
menu.addSeparator();
menu.addAction("&Add Field Below", [this, parentId]() {
insertNode(parentId, -1, NodeKind::Hex64, "newField");
});
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
menu.addAction("Add &Child", [this, nodeId]() {
insertNode(nodeId, 0, NodeKind::Hex64, "newField");
});
QString colText = node.collapsed ? "&Expand" : "&Collapse";
menu.addAction(colText, [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) toggleCollapse(ni);
});
}
menu.addAction("D&uplicate", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) duplicateNode(ni);
});
menu.addAction("&Delete", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) removeNode(ni);
});
menu.addSeparator();
menu.addAction("Copy &Address", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
QApplication::clipboard()->setText(
QStringLiteral("0x") + QString::number(addr, 16).toUpper());
});
menu.addAction("Copy &Offset", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
int off = m_doc->tree.nodes[ni].offset;
QApplication::clipboard()->setText(
QStringLiteral("+0x") + QString::number(off, 16).toUpper().rightJustified(4, '0'));
});
menu.exec(globalPos);
}
void RcxController::batchRemoveNodes(const QVector<int>& nodeIndices) {
QSet<uint64_t> idSet;
for (int idx : nodeIndices) {
if (idx >= 0 && idx < m_doc->tree.nodes.size())
idSet.insert(m_doc->tree.nodes[idx].id);
}
idSet = m_doc->tree.normalizePreferAncestors(idSet);
if (idSet.isEmpty()) return;
m_doc->undoStack.beginMacro(QString("Delete %1 nodes").arg(idSet.size()));
for (uint64_t id : idSet) {
int idx = m_doc->tree.indexOfId(id);
if (idx >= 0) removeNode(idx);
}
m_doc->undoStack.endMacro();
}
void RcxController::batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind) {
QSet<uint64_t> idSet;
for (int idx : nodeIndices) {
if (idx >= 0 && idx < m_doc->tree.nodes.size())
idSet.insert(m_doc->tree.nodes[idx].id);
}
idSet = m_doc->tree.normalizePreferDescendants(idSet);
if (idSet.isEmpty()) return;
m_doc->undoStack.beginMacro(QString("Change type of %1 nodes").arg(idSet.size()));
for (uint64_t id : idSet) {
int idx = m_doc->tree.indexOfId(id);
if (idx >= 0) changeNodeKind(idx, newKind);
}
m_doc->undoStack.endMacro();
}
void RcxController::handleNodeClick(RcxEditor* source, int line,
uint64_t nodeId,
Qt::KeyboardModifiers mods) {
bool ctrl = mods & Qt::ControlModifier;
bool shift = mods & Qt::ShiftModifier;
if (!ctrl && !shift) {
m_selIds.clear();
m_selIds.insert(nodeId);
m_anchorLine = line;
} else if (ctrl && !shift) {
if (m_selIds.contains(nodeId))
m_selIds.remove(nodeId);
else
m_selIds.insert(nodeId);
m_anchorLine = line;
} else if (shift && !ctrl) {
m_selIds.clear();
int from = qMin(m_anchorLine, line);
int to = qMax(m_anchorLine, line);
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
uint64_t nid = m_lastResult.meta[i].nodeId;
if (nid != 0) m_selIds.insert(nid);
}
} else { // Ctrl+Shift
int from = qMin(m_anchorLine, line);
int to = qMax(m_anchorLine, line);
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
uint64_t nid = m_lastResult.meta[i].nodeId;
if (nid != 0) m_selIds.insert(nid);
}
}
applySelectionOverlays();
if (m_selIds.size() == 1) {
uint64_t sid = *m_selIds.begin();
int idx = m_doc->tree.indexOfId(sid);
if (idx >= 0) emit nodeSelected(idx);
}
}
void RcxController::clearSelection() {
m_selIds.clear();
m_anchorLine = -1;
applySelectionOverlays();
}
void RcxController::applySelectionOverlays() {
for (auto* editor : m_editors)
editor->applySelectionOverlay(m_selIds);
}
void RcxController::handleMarginClick(RcxEditor* editor, int margin,
int line, Qt::KeyboardModifiers) {
const LineMeta* lm = editor->metaForLine(line);
if (!lm) return;
if (lm->foldHead && (margin == 0 || margin == 1)) {
toggleCollapse(lm->nodeIdx);
} else if (margin == 0 || margin == 1) {
emit nodeSelected(lm->nodeIdx);
}
}
} // namespace rcx

99
src/controller.h Normal file
View File

@@ -0,0 +1,99 @@
#pragma once
#include "core.h"
#include "editor.h"
#include <QObject>
#include <QUndoStack>
#include <QUndoCommand>
#include <memory>
class QSplitter;
namespace rcx {
class RcxController;
// ── Document ──
class RcxDocument : public QObject {
Q_OBJECT
public:
explicit RcxDocument(QObject* parent = nullptr);
NodeTree tree;
std::unique_ptr<Provider> provider;
QUndoStack undoStack;
QString filePath;
bool modified = false;
ComposeResult compose() const;
bool save(const QString& path);
bool load(const QString& path);
void loadData(const QString& binaryPath);
void loadData(const QByteArray& data);
signals:
void documentChanged();
};
// ── Undo command ──
class RcxCommand : public QUndoCommand {
public:
RcxCommand(RcxController* ctrl, Command cmd);
void undo() override;
void redo() override;
private:
RcxController* m_ctrl;
Command m_cmd;
};
// ── Controller ──
class RcxController : public QObject {
Q_OBJECT
public:
explicit RcxController(RcxDocument* doc, QWidget* parent = nullptr);
RcxEditor* primaryEditor() const;
RcxEditor* addSplitEditor(QSplitter* splitter);
void removeSplitEditor(RcxEditor* editor);
QList<RcxEditor*> editors() const { return m_editors; }
void changeNodeKind(int nodeIdx, NodeKind newKind);
void renameNode(int nodeIdx, const QString& newName);
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
void removeNode(int nodeIdx);
void toggleCollapse(int nodeIdx);
void setNodeValue(int nodeIdx, int subLine, const QString& text);
void duplicateNode(int nodeIdx);
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
void batchRemoveNodes(const QVector<int>& nodeIndices);
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
void applyCommand(const Command& cmd, bool isUndo);
void refresh();
// Selection
void handleNodeClick(RcxEditor* source, int line, uint64_t nodeId,
Qt::KeyboardModifiers mods);
void clearSelection();
void applySelectionOverlays();
QSet<uint64_t> selectedIds() const { return m_selIds; }
RcxDocument* document() const { return m_doc; }
signals:
void nodeSelected(int nodeIdx);
private:
RcxDocument* m_doc;
QList<RcxEditor*> m_editors;
ComposeResult m_lastResult;
QSet<uint64_t> m_selIds;
int m_anchorLine = -1;
void connectEditor(RcxEditor* editor);
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
};
} // namespace rcx

521
src/core.h Normal file
View File

@@ -0,0 +1,521 @@
#pragma once
#include <QString>
#include <QVector>
#include <QJsonObject>
#include <QJsonArray>
#include <QByteArray>
#include <QFile>
#include <QHash>
#include <QSet>
#include <cstdint>
#include <memory>
#include <variant>
namespace rcx {
// ── Node kind enum ──
enum class NodeKind : uint8_t {
Hex8, Hex16, Hex32, Hex64,
Int8, Int16, Int32, Int64,
UInt8, UInt16, UInt32, UInt64,
Float, Double, Bool,
Pointer32, Pointer64,
Vec2, Vec3, Vec4, Mat4x4,
UTF8, UTF16,
Padding,
Struct, Array
};
// ── Unified kind metadata table (single source of truth) ──
struct KindMeta {
NodeKind kind;
const char* name; // UI/JSON name: "Hex64", "UInt16"
const char* typeName; // display name: "Hex64", "uint16_t"
int size; // byte size (0 = dynamic: Struct/Array)
int lines; // display line count
int align; // natural alignment
};
inline constexpr KindMeta kKindMeta[] = {
// kind name typeName sz ln al
{NodeKind::Hex8, "Hex8", "Hex8", 1, 1, 1},
{NodeKind::Hex16, "Hex16", "Hex16", 2, 1, 2},
{NodeKind::Hex32, "Hex32", "Hex32", 4, 1, 4},
{NodeKind::Hex64, "Hex64", "Hex64", 8, 1, 8},
{NodeKind::Int8, "Int8", "int8_t", 1, 1, 1},
{NodeKind::Int16, "Int16", "int16_t", 2, 1, 2},
{NodeKind::Int32, "Int32", "int32_t", 4, 1, 4},
{NodeKind::Int64, "Int64", "int64_t", 8, 1, 8},
{NodeKind::UInt8, "UInt8", "uint8_t", 1, 1, 1},
{NodeKind::UInt16, "UInt16", "uint16_t", 2, 1, 2},
{NodeKind::UInt32, "UInt32", "uint32_t", 4, 1, 4},
{NodeKind::UInt64, "UInt64", "uint64_t", 8, 1, 8},
{NodeKind::Float, "Float", "float", 4, 1, 4},
{NodeKind::Double, "Double", "double", 8, 1, 8},
{NodeKind::Bool, "Bool", "bool", 1, 1, 1},
{NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4},
{NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8},
{NodeKind::Vec2, "Vec2", "Vec2", 8, 2, 4},
{NodeKind::Vec3, "Vec3", "Vec3", 12, 3, 4},
{NodeKind::Vec4, "Vec4", "Vec4", 16, 4, 4},
{NodeKind::Mat4x4, "Mat4x4", "Mat4x4", 64, 4, 4},
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1},
{NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2},
{NodeKind::Padding, "Padding", "pad", 1, 1, 1},
{NodeKind::Struct, "Struct", "struct", 0, 1, 1},
{NodeKind::Array, "Array", "array", 0, 1, 1},
};
inline constexpr const KindMeta* kindMeta(NodeKind k) {
for (const auto& m : kKindMeta)
if (m.kind == k) return &m;
return nullptr;
}
inline constexpr int sizeForKind(NodeKind k) { auto* m = kindMeta(k); return m ? m->size : 0; }
inline constexpr int linesForKind(NodeKind k) { auto* m = kindMeta(k); return m ? m->lines : 1; }
inline constexpr int alignmentFor(NodeKind k) { auto* m = kindMeta(k); return m ? m->align : 1; }
inline const char* kindToString(NodeKind k) {
auto* m = kindMeta(k);
return m ? m->name : "Unknown";
}
inline NodeKind kindFromString(const QString& s) {
for (const auto& m : kKindMeta)
if (s == m.name) return m.kind;
return NodeKind::Hex8;
}
inline NodeKind kindFromTypeName(const QString& s, bool* ok = nullptr) {
for (const auto& m : kKindMeta) {
if (s == m.typeName) {
if (ok) *ok = true;
return m.kind;
}
}
if (ok) *ok = false;
return NodeKind::Hex8;
}
// ── Marker vocabulary ──
enum Marker : int {
M_CONT = 0,
M_PAD = 1,
M_PTR0 = 2,
M_CYCLE = 3,
M_ERR = 4,
M_STRUCT_BG = 5,
};
// ── Provider interface ──
class Provider {
public:
virtual ~Provider() = default;
virtual uint8_t readU8 (uint64_t addr) const = 0;
virtual uint16_t readU16(uint64_t addr) const = 0;
virtual uint32_t readU32(uint64_t addr) const = 0;
virtual uint64_t readU64(uint64_t addr) const = 0;
virtual float readF32(uint64_t addr) const = 0;
virtual double readF64(uint64_t addr) const = 0;
virtual QByteArray readBytes(uint64_t addr, int len) const = 0;
virtual bool isValid() const = 0;
virtual bool isReadable(uint64_t addr, int len) const = 0;
virtual int size() const = 0;
virtual bool isWritable() const { return false; }
virtual bool writeBytes(uint64_t addr, const QByteArray& data) {
Q_UNUSED(addr); Q_UNUSED(data); return false;
}
};
class FileProvider : public Provider {
QByteArray m_data;
template<class T>
T readT(uint64_t a) const {
if (a + sizeof(T) > (uint64_t)m_data.size()) return T{};
T v; memcpy(&v, m_data.data() + a, sizeof(T)); return v;
}
public:
explicit FileProvider(const QByteArray& data) : m_data(data) {}
static FileProvider fromFile(const QString& path) {
QFile f(path);
if (f.open(QIODevice::ReadOnly)) return FileProvider(f.readAll());
return FileProvider({});
}
bool isValid() const override { return !m_data.isEmpty(); }
bool isReadable(uint64_t addr, int len) const override {
if (len <= 0) return len == 0;
if (addr > (uint64_t)m_data.size()) return false;
return (uint64_t)len <= (uint64_t)m_data.size() - addr;
}
int size() const override { return m_data.size(); }
uint8_t readU8 (uint64_t a) const override { return readT<uint8_t>(a); }
uint16_t readU16(uint64_t a) const override { return readT<uint16_t>(a); }
uint32_t readU32(uint64_t a) const override { return readT<uint32_t>(a); }
uint64_t readU64(uint64_t a) const override { return readT<uint64_t>(a); }
float readF32(uint64_t a) const override { return readT<float>(a); }
double readF64(uint64_t a) const override { return readT<double>(a); }
QByteArray readBytes(uint64_t a, int len) const override {
if (a >= (uint64_t)m_data.size()) return {};
int avail = qMin(len, (int)((uint64_t)m_data.size() - a));
return m_data.mid((int)a, avail);
}
bool isWritable() const override { return true; }
bool writeBytes(uint64_t addr, const QByteArray& data) override {
if (addr + data.size() > (uint64_t)m_data.size()) return false;
memcpy(m_data.data() + addr, data.data(), data.size());
return true;
}
};
class NullProvider : public Provider {
public:
uint8_t readU8 (uint64_t) const override { return 0; }
uint16_t readU16(uint64_t) const override { return 0; }
uint32_t readU32(uint64_t) const override { return 0; }
uint64_t readU64(uint64_t) const override { return 0; }
float readF32(uint64_t) const override { return 0.0f; }
double readF64(uint64_t) const override { return 0.0; }
QByteArray readBytes(uint64_t, int) const override { return {}; }
bool isValid() const override { return false; }
bool isReadable(uint64_t, int) const override { return false; }
int size() const override { return 0; }
};
// ── Node ──
struct Node {
uint64_t id = 0;
NodeKind kind = NodeKind::Hex8;
QString name;
uint64_t parentId = 0; // 0 = root (no parent)
int offset = 0;
int arrayLen = 0;
int strLen = 64;
bool collapsed = false;
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
int byteSize() const {
switch (kind) {
case NodeKind::UTF8: return strLen;
case NodeKind::UTF16: return strLen * 2;
case NodeKind::Padding: return qMax(1, arrayLen);
default: return sizeForKind(kind);
}
}
QJsonObject toJson() const {
QJsonObject o;
o["id"] = QString::number(id);
o["kind"] = kindToString(kind);
o["name"] = name;
o["parentId"] = QString::number(parentId);
o["offset"] = offset;
o["arrayLen"] = arrayLen;
o["strLen"] = strLen;
o["collapsed"] = collapsed;
o["refId"] = QString::number(refId);
return o;
}
static Node fromJson(const QJsonObject& o) {
Node n;
n.id = o["id"].toString("0").toULongLong();
n.kind = kindFromString(o["kind"].toString());
n.name = o["name"].toString();
n.parentId = o["parentId"].toString("0").toULongLong();
n.offset = o["offset"].toInt(0);
n.arrayLen = o["arrayLen"].toInt(0);
n.strLen = o["strLen"].toInt(64);
n.collapsed = o["collapsed"].toBool(false);
n.refId = o["refId"].toString("0").toULongLong();
return n;
}
};
// ── NodeTree ──
struct NodeTree {
QVector<Node> nodes;
uint64_t baseAddress = 0x00400000;
uint64_t m_nextId = 1;
mutable QHash<uint64_t, int> m_idCache;
int addNode(const Node& n) {
Node copy = n;
if (copy.id == 0) copy.id = m_nextId++;
else if (copy.id >= m_nextId) m_nextId = copy.id + 1;
nodes.append(copy);
m_idCache.clear();
return nodes.size() - 1;
}
void invalidateIdCache() const { m_idCache.clear(); }
int indexOfId(uint64_t id) const {
if (m_idCache.isEmpty() && !nodes.isEmpty()) {
for (int i = 0; i < nodes.size(); i++)
m_idCache[nodes[i].id] = i;
}
return m_idCache.value(id, -1);
}
QVector<int> childrenOf(uint64_t parentId) const {
QVector<int> result;
for (int i = 0; i < nodes.size(); i++) {
if (nodes[i].parentId == parentId) result.append(i);
}
return result;
}
// Collect node + all descendants (iterative, cycle-safe)
QVector<int> subtreeIndices(uint64_t nodeId) const {
int idx = indexOfId(nodeId);
if (idx < 0) return {};
// Build parent→children map
QHash<uint64_t, QVector<int>> childMap;
for (int i = 0; i < nodes.size(); i++)
childMap[nodes[i].parentId].append(i);
// BFS with visited guard
QVector<int> result;
QSet<uint64_t> visited;
QVector<uint64_t> stack;
stack.append(nodeId);
result.append(idx);
visited.insert(nodeId);
while (!stack.isEmpty()) {
uint64_t pid = stack.takeLast();
for (int ci : childMap.value(pid)) {
uint64_t cid = nodes[ci].id;
if (!visited.contains(cid)) {
visited.insert(cid);
result.append(ci);
stack.append(cid);
}
}
}
return result;
}
int depthOf(int idx) const {
int d = 0;
QSet<int> visited;
int cur = idx;
while (cur >= 0 && cur < nodes.size() && nodes[cur].parentId != 0) {
if (visited.contains(cur)) break;
visited.insert(cur);
cur = indexOfId(nodes[cur].parentId);
if (cur < 0) break;
d++;
}
return d;
}
int64_t computeOffset(int idx) const {
int64_t total = 0;
QSet<int> visited;
int cur = idx;
while (cur >= 0 && cur < nodes.size()) {
if (visited.contains(cur)) break;
visited.insert(cur);
total += nodes[cur].offset;
if (nodes[cur].parentId == 0) break;
cur = indexOfId(nodes[cur].parentId);
}
return total;
}
int structSpan(uint64_t structId,
const QHash<uint64_t, QVector<int>>* childMap = nullptr) const {
int maxEnd = 0;
QVector<int> kids = childMap ? childMap->value(structId) : childrenOf(structId);
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();
int end = c.offset + sz;
if (end > maxEnd) maxEnd = end;
}
return maxEnd;
}
// Batch selection normalizers
QSet<uint64_t> normalizePreferAncestors(const QSet<uint64_t>& ids) const;
QSet<uint64_t> normalizePreferDescendants(const QSet<uint64_t>& ids) const;
QJsonObject toJson() const {
QJsonObject o;
o["baseAddress"] = QString::number(baseAddress, 16);
o["nextId"] = QString::number(m_nextId);
QJsonArray arr;
for (const auto& n : nodes) arr.append(n.toJson());
o["nodes"] = arr;
return o;
}
static NodeTree fromJson(const QJsonObject& o) {
NodeTree t;
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
t.m_nextId = o["nextId"].toString("1").toULongLong();
QJsonArray arr = o["nodes"].toArray();
for (const auto& v : arr) {
Node n = Node::fromJson(v.toObject());
t.nodes.append(n);
if (n.id >= t.m_nextId) t.m_nextId = n.id + 1;
}
return t;
}
};
// ── LineMeta ──
enum class LineKind : uint8_t {
Header, Field, Continuation, Footer
};
struct LineMeta {
int nodeIdx = -1;
uint64_t nodeId = 0;
int subLine = 0;
int depth = 0;
int foldLevel = 0;
bool foldHead = false;
bool foldCollapsed = false;
bool isContinuation = false;
LineKind lineKind = LineKind::Field;
NodeKind nodeKind = NodeKind::Int32;
QString offsetText;
uint32_t markerMask = 0;
};
// ── ComposeResult ──
struct ComposeResult {
QString text;
QVector<LineMeta> meta;
};
// ── Command ──
namespace cmd {
struct OffsetAdj { uint64_t nodeId; int oldOffset, newOffset; };
struct ChangeKind { uint64_t nodeId; NodeKind oldKind, newKind;
QVector<OffsetAdj> offAdjs; };
struct Rename { uint64_t nodeId; QString oldName, newName; };
struct Collapse { uint64_t nodeId; bool oldState, newState; };
struct Insert { Node node; };
struct Remove { uint64_t nodeId; QVector<Node> subtree; };
struct ChangeBase { uint64_t oldBase, newBase; };
struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; };
}
using Command = std::variant<
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes
>;
// ── Column spans (for inline editing) ──
struct ColumnSpan {
int start = 0; // inclusive column index
int end = 0; // exclusive column index
bool valid = false;
};
enum class EditTarget { Name, Type, Value };
// Column layout constants (shared with format.cpp span computation)
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
inline constexpr int kColType = 10;
inline constexpr int kColName = 24;
inline constexpr int kSepWidth = 2;
inline ColumnSpan typeSpanFor(const LineMeta& lm) {
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
int ind = kFoldCol + lm.depth * 3;
return {ind, ind + kColType, true};
}
inline ColumnSpan nameSpanFor(const LineMeta& lm) {
if (lm.isContinuation || lm.lineKind != LineKind::Field) return {};
// Hex/Padding nodes show ASCII data preview instead of name
switch (lm.nodeKind) {
case NodeKind::Hex8: case NodeKind::Hex16:
case NodeKind::Hex32: case NodeKind::Hex64:
case NodeKind::Padding:
return {};
default: break;
}
int ind = kFoldCol + lm.depth * 3;
int start = ind + kColType + kSepWidth;
return {start, start + kColName, true};
}
inline ColumnSpan valueSpanFor(const LineMeta& lm, int lineLength) {
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
int ind = kFoldCol + lm.depth * 3;
if (lm.isContinuation) {
int prefixW = kColType + kColName + 4; // 2 seps × 2 chars
int start = ind + prefixW;
return {start, lineLength, start < lineLength};
}
if (lm.lineKind != LineKind::Field) return {};
int start = ind + kColType + kSepWidth + kColName + kSepWidth;
return {start, lineLength, start < lineLength};
}
// ── ViewState ──
struct ViewState {
int scrollLine = 0;
int cursorLine = 0;
int cursorCol = 0;
};
// ── Format function forward declarations ──
namespace fmt {
using TypeNameFn = QString (*)(NodeKind);
void setTypeNameProvider(TypeNameFn fn);
QString typeName(NodeKind kind);
QString fmtInt8(int8_t v);
QString fmtInt16(int16_t v);
QString fmtInt32(int32_t v);
QString fmtInt64(int64_t v);
QString fmtUInt8(uint8_t v);
QString fmtUInt16(uint16_t v);
QString fmtUInt32(uint32_t v);
QString fmtUInt64(uint64_t v);
QString fmtFloat(float v);
QString fmtDouble(double v);
QString fmtBool(uint8_t v);
QString fmtPointer32(uint32_t v);
QString fmtPointer64(uint64_t v);
QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine = 0);
QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation);
QString fmtStructHeader(const Node& node, int depth);
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
QString indent(int depth);
QString readValue(const Node& node, const Provider& prov,
uint64_t addr, int subLine);
QString editableValue(const Node& node, const Provider& prov,
uint64_t addr, int subLine);
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
} // namespace fmt
// ── Compose function forward declaration ──
ComposeResult compose(const NodeTree& tree, const Provider& prov);
} // namespace rcx

982
src/editor.cpp Normal file
View File

@@ -0,0 +1,982 @@
#include "editor.h"
#include <Qsci/qsciscintilla.h>
#include <Qsci/qsciscintillabase.h>
#include <Qsci/qscilexercpp.h>
#include <QVBoxLayout>
#include <QFont>
#include <QColor>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QFocusEvent>
#include <QToolTip>
#include <QTimer>
#include <QCursor>
#include <QApplication>
namespace rcx {
// ── Theme constants ──
static const QColor kBgText("#1e1e1e");
static const QColor kBgMargin("#252526");
static const QColor kFgMargin("#858585");
static const QColor kFgMarginDim("#505050");
static constexpr int IND_EDITABLE = 8;
static constexpr int IND_HEX_DIM = 9;
static constexpr int IND_SELECTED = 10;
static constexpr int IND_HOVER = 11;
static QFont editorFont() {
QFont f("Consolas", 12);
f.setFixedPitch(true);
return f;
}
RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
m_sci = new QsciScintilla(this);
layout->addWidget(m_sci);
setupScintilla();
setupLexer();
setupMargins();
setupFolding();
setupMarkers();
allocateMarginStyles();
m_sci->installEventFilter(this);
m_sci->viewport()->installEventFilter(this);
m_sci->viewport()->setMouseTracking(true);
// Hover cursor is applied synchronously in eventFilter (no timer).
connect(m_sci, &QsciScintilla::marginClicked,
this, [this](int margin, int line, Qt::KeyboardModifiers mods) {
emit marginClicked(margin, line, mods);
});
m_sci->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_sci, &QWidget::customContextMenuRequested,
this, [this](const QPoint& pos) {
int line = m_sci->lineAt(pos);
int nodeIdx = -1;
int subLine = 0;
if (line >= 0 && line < m_meta.size()) {
nodeIdx = m_meta[line].nodeIdx;
subLine = m_meta[line].subLine;
}
emit contextMenuRequested(line, nodeIdx, subLine, m_sci->mapToGlobal(pos));
});
connect(m_sci, &QsciScintilla::userListActivated,
this, [this](int id, const QString& text) {
if (id == 1 && m_editState.active && m_editState.target == EditTarget::Type) {
auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text);
}
});
connect(m_sci, &QsciScintilla::cursorPositionChanged,
this, [this](int line, int /*col*/) { updateEditableUnderline(line); });
}
void RcxEditor::setupScintilla() {
m_sci->setFont(editorFont());
m_sci->setReadOnly(true);
m_sci->setWrapMode(QsciScintilla::WrapNone);
m_sci->setCaretLineVisible(true);
m_sci->setCaretLineBackgroundColor(QColor("#2c3338"));
m_sci->setPaper(kBgText);
m_sci->setColor(QColor("#d4d4d4"));
m_sci->setTabWidth(2);
m_sci->setIndentationsUseTabs(false);
// Caret color for dark theme
m_sci->setCaretForegroundColor(QColor("#d4d4d4"));
// Line spacing for readability
m_sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2);
// Selection colors
m_sci->setSelectionBackgroundColor(QColor("#264f78"));
m_sci->setSelectionForegroundColor(QColor("#d4d4d4"));
// Editable-field link-style indicator (colored text + underline)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_EDITABLE, 17 /*INDIC_TEXTFORE*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_EDITABLE, QColor("#569cd6"));
// Hex/Padding node dim indicator — overrides text color to gray
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_HEX_DIM, QColor("#505050"));
// Selection overlay — translucent blue box
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_SELECTED, 8 /*INDIC_STRAIGHTBOX*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_SELECTED, QColor("#264f78"));
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETALPHA,
IND_SELECTED, (long)50);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETOUTLINEALPHA,
IND_SELECTED, (long)100);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
IND_SELECTED, (long)1);
// Hover row highlight — very subtle fill
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_HOVER, 16 /*INDIC_FULLBOX*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_HOVER, QColor("#264f78"));
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETALPHA,
IND_HOVER, (long)25);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETOUTLINEALPHA,
IND_HOVER, (long)0);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
IND_HOVER, (long)1);
}
void RcxEditor::setupLexer() {
m_lexer = new QsciLexerCPP(m_sci);
QFont font = editorFont();
m_lexer->setFont(font);
// Dark theme colors
m_lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword);
m_lexer->setColor(QColor("#4ec9b0"), QsciLexerCPP::KeywordSet2);
m_lexer->setColor(QColor("#b5cea8"), QsciLexerCPP::Number);
m_lexer->setColor(QColor("#ce9178"), QsciLexerCPP::DoubleQuotedString);
m_lexer->setColor(QColor("#ce9178"), QsciLexerCPP::SingleQuotedString);
m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::Comment);
m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine);
m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc);
m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Default);
m_lexer->setColor(QColor("#dcdcaa"), QsciLexerCPP::Identifier);
m_lexer->setColor(QColor("#c586c0"), QsciLexerCPP::PreProcessor);
m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator);
// Dark background for all styles
for (int i = 0; i <= 127; i++) {
m_lexer->setPaper(kBgText, i);
m_lexer->setFont(font, i);
}
m_sci->setLexer(m_lexer);
m_sci->setBraceMatching(QsciScintilla::SloppyBraceMatch);
}
void RcxEditor::setupMargins() {
m_sci->setMarginsFont(editorFont());
// Margin 0: Offset text
m_sci->setMarginType(0, QsciScintilla::TextMarginRightJustified);
m_sci->setMarginWidth(0, " +0x00000000 ");
m_sci->setMarginsBackgroundColor(kBgMargin);
m_sci->setMarginsForegroundColor(kFgMargin);
m_sci->setMarginSensitivity(0, true);
// Margin 1: hidden (fold chevrons moved to text column)
m_sci->setMarginWidth(1, 0);
}
void RcxEditor::setupFolding() {
// Hide fold margin (fold indicators are text-based now)
m_sci->setMarginWidth(2, 0);
m_sci->setFoldMarginColors(kBgMargin, kBgMargin);
// Fold indicators are now text in the line content (kFoldCol prefix),
// so no Scintilla markers needed for fold state.
// Keep Scintilla fold markers invisible (fold levels still used for click detection)
for (int i = 25; i <= 31; i++)
m_sci->markerDefine(QsciScintilla::Invisible, i);
// Disable automatic fold toggle — we handle collapse at model level
m_sci->SendScintilla(QsciScintillaBase::SCI_SETAUTOMATICFOLD,
(unsigned long)0);
// Disable lexer-driven folding — we set fold levels manually
m_sci->SendScintilla(QsciScintillaBase::SCI_SETPROPERTY,
(const char*)"fold", (const char*)"0");
}
void RcxEditor::setupMarkers() {
// M_CONT (0): vertical line
m_sci->markerDefine(QsciScintilla::VLine, M_CONT);
m_sci->setMarkerBackgroundColor(kFgMarginDim, M_CONT);
m_sci->setMarkerForegroundColor(kFgMarginDim, M_CONT);
// M_PAD (1): small rectangle (dim gray)
m_sci->markerDefine(QsciScintilla::SmallRectangle, M_PAD);
m_sci->setMarkerBackgroundColor(QColor("#606060"), M_PAD);
m_sci->setMarkerForegroundColor(QColor("#606060"), M_PAD);
// M_PTR0 (2): right triangle (red)
m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0);
m_sci->setMarkerBackgroundColor(QColor("#f44747"), M_PTR0);
m_sci->setMarkerForegroundColor(QColor("#f44747"), M_PTR0);
// M_CYCLE (3): arrows (orange)
m_sci->markerDefine(QsciScintilla::ThreeRightArrows, M_CYCLE);
m_sci->setMarkerBackgroundColor(QColor("#e5a00d"), M_CYCLE);
m_sci->setMarkerForegroundColor(QColor("#e5a00d"), M_CYCLE);
// M_ERR (4): background (dark red)
m_sci->markerDefine(QsciScintilla::Background, M_ERR);
m_sci->setMarkerBackgroundColor(QColor("#5c2020"), M_ERR);
m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR);
// M_STRUCT_BG (5): background tint for struct header/footer
m_sci->markerDefine(QsciScintilla::Background, M_STRUCT_BG);
m_sci->setMarkerBackgroundColor(QColor("#1a2332"), M_STRUCT_BG);
m_sci->setMarkerForegroundColor(QColor("#d4d4d4"), M_STRUCT_BG);
}
void RcxEditor::allocateMarginStyles() {
// Relative indices within margin style offset
static constexpr int MSTYLE_NORMAL = 0;
static constexpr int MSTYLE_CONT = 1;
long base = m_sci->SendScintilla(QsciScintillaBase::SCI_ALLOCATEEXTENDEDSTYLES, (long)2);
m_marginStyleBase = (int)base;
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLEOFFSET, base);
const long bgrMargin = 0x262525; // BGR for #252526
// Normal offset style: gray on dark
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE,
(unsigned long)(base + MSTYLE_NORMAL), (long)0x858585);
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK,
(unsigned long)(base + MSTYLE_NORMAL), bgrMargin);
// Continuation style: dimmer
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE,
(unsigned long)(base + MSTYLE_CONT), (long)0x505050);
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK,
(unsigned long)(base + MSTYLE_CONT), bgrMargin);
}
void RcxEditor::applyDocument(const ComposeResult& result) {
// Silently deactivate inline edit (no signal — refresh is already happening)
if (m_editState.active)
endInlineEdit();
m_meta = result.meta;
m_sci->setReadOnly(false);
m_sci->setText(result.text);
m_sci->setReadOnly(true);
// Force full re-lex to fix stale syntax coloring after edits
m_sci->SendScintilla(QsciScintillaBase::SCI_COLOURISE, (uintptr_t)0, (long)-1);
applyMarginText(result.meta);
applyMarkers(result.meta);
applyFoldLevels(result.meta);
applyHexDimming(result.meta);
// Re-apply editable underline for current cursor line
m_hintLine = -1;
int line, col;
m_sci->getCursorPosition(&line, &col);
updateEditableUnderline(line);
}
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
// Clear all margin text
m_sci->clearMarginText(-1);
for (int i = 0; i < meta.size(); i++) {
const auto& lm = meta[i];
if (!lm.offsetText.isEmpty()) {
int style = lm.isContinuation ? 1 : 0;
m_sci->setMarginText(i, lm.offsetText, style);
}
}
}
void RcxEditor::applyMarkers(const QVector<LineMeta>& meta) {
for (int m = M_CONT; m <= M_STRUCT_BG; m++) {
m_sci->markerDeleteAll(m);
}
for (int i = 0; i < meta.size(); i++) {
uint32_t mask = meta[i].markerMask;
for (int m = M_CONT; m <= M_STRUCT_BG; m++) {
if (mask & (1u << m)) {
m_sci->markerAdd(i, m);
}
}
}
}
void RcxEditor::applyFoldLevels(const QVector<LineMeta>& meta) {
for (int i = 0; i < meta.size(); i++) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETFOLDLEVEL,
(unsigned long)i, (long)meta[i].foldLevel);
}
}
void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM);
for (int i = 0; i < meta.size(); i++) {
switch (meta[i].nodeKind) {
case NodeKind::Hex8: case NodeKind::Hex16:
case NodeKind::Hex32: case NodeKind::Hex64:
case NodeKind::Padding: {
long pos = m_sci->SendScintilla(
QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)i);
long len = m_sci->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)i);
if (len > 0)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len);
break;
}
default: break;
}
}
}
void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
m_currentSelIds = selIds;
// Clear all selection indicators
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_SELECTED);
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (uintptr_t)0, docLen);
if (selIds.isEmpty()) return;
for (int i = 0; i < m_meta.size(); i++) {
if (selIds.contains(m_meta[i].nodeId)) {
long pos = m_sci->SendScintilla(
QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)i);
long len = m_sci->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)i);
if (len > 0)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len);
}
}
// Refresh hover since selection may suppress it
applyHoverHighlight();
}
void RcxEditor::applyHoverHighlight() {
// Clear previous hover indicator
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HOVER);
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (uintptr_t)0, docLen);
if (m_editState.active) return;
if (!m_hoverInside) return;
if (m_hoveredNodeId == 0) return;
if (m_currentSelIds.contains(m_hoveredNodeId)) return;
for (int i = 0; i < m_meta.size(); i++) {
if (m_meta[i].nodeId == m_hoveredNodeId) {
long pos = m_sci->SendScintilla(
QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)i);
long len = m_sci->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)i);
if (len > 0)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len);
}
}
}
ViewState RcxEditor::saveViewState() const {
ViewState vs;
vs.scrollLine = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETFIRSTVISIBLELINE);
int line, col;
m_sci->getCursorPosition(&line, &col);
vs.cursorLine = line;
vs.cursorCol = col;
return vs;
}
void RcxEditor::restoreViewState(const ViewState& vs) {
m_sci->setCursorPosition(vs.cursorLine, vs.cursorCol);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE,
(unsigned long)vs.scrollLine);
}
const LineMeta* RcxEditor::metaForLine(int line) const {
if (line >= 0 && line < m_meta.size())
return &m_meta[line];
return nullptr;
}
int RcxEditor::currentNodeIndex() const {
int line, col;
m_sci->getCursorPosition(&line, &col);
auto* lm = metaForLine(line);
return lm ? lm->nodeIdx : -1;
}
// ── Column span computation ──
ColumnSpan RcxEditor::typeSpan(const LineMeta& lm) { return typeSpanFor(lm); }
ColumnSpan RcxEditor::nameSpan(const LineMeta& lm) { return nameSpanFor(lm); }
ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength) { return valueSpanFor(lm, lineLength); }
// ── Multi-selection ──
QSet<int> RcxEditor::selectedNodeIndices() const {
int lineFrom, indexFrom, lineTo, indexTo;
m_sci->getSelection(&lineFrom, &indexFrom, &lineTo, &indexTo);
if (lineFrom < 0) {
int line, col;
m_sci->getCursorPosition(&line, &col);
auto* lm = metaForLine(line);
return lm && lm->nodeIdx >= 0 ? QSet<int>{lm->nodeIdx} : QSet<int>{};
}
QSet<int> result;
for (int line = lineFrom; line <= lineTo; line++) {
auto* lm = metaForLine(line);
if (lm && lm->nodeIdx >= 0) result.insert(lm->nodeIdx);
}
return result;
}
// ── Inline edit helpers ──
static QString getLineText(QsciScintilla* sci, int line) {
int len = (int)sci->SendScintilla(QsciScintillaBase::SCI_LINELENGTH, (unsigned long)line);
if (len <= 0) return {};
QByteArray buf(len + 1, '\0');
sci->SendScintilla(QsciScintillaBase::SCI_GETLINE, (unsigned long)line, (void*)buf.data());
QString text = QString::fromUtf8(buf.data(), len);
while (text.endsWith('\n') || text.endsWith('\r'))
text.chop(1);
return text;
}
// ── Shared inline-edit shutdown ──
RcxEditor::EndEditInfo RcxEditor::endInlineEdit() {
EndEditInfo info{m_editState.nodeIdx, m_editState.subLine, m_editState.target};
m_editState.active = false;
m_sci->setReadOnly(true);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)1);
m_sci->SendScintilla(QsciScintillaBase::SCI_EMPTYUNDOBUFFER);
return info;
}
// ── Span helpers ──
static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header) return {};
int bracePos = lineText.lastIndexOf(QStringLiteral(" {"));
if (bracePos <= 0) return {};
int ind = kFoldCol + lm.depth * 3;
int typeEnd = lineText.indexOf(' ', ind);
if (typeEnd <= ind || typeEnd >= bracePos) return {};
return {typeEnd + 1, bracePos, true};
}
RcxEditor::NormalizedSpan RcxEditor::normalizeSpan(
const ColumnSpan& raw, const QString& lineText,
EditTarget target, bool skipPrefixes) const
{
if (!raw.valid) return {};
int textLen = lineText.size();
if (raw.start >= textLen) return {};
int start = raw.start;
int end = qMin(raw.end, textLen);
if (end <= start) return {};
if (skipPrefixes && target == EditTarget::Value) {
QString spanText = lineText.mid(start, end - start);
int arrow = spanText.indexOf(QStringLiteral("->"));
if (arrow >= 0) {
int i = arrow + 2;
while (i < spanText.size() && spanText[i].isSpace()) i++;
start += i;
} else {
int eq = spanText.indexOf('=');
if (eq >= 0 && eq <= 3) {
int i = eq + 1;
while (i < spanText.size() && spanText[i].isSpace()) i++;
start += i;
}
}
if (start >= end) return {};
}
QString inner = lineText.mid(start, end - start);
int lead = 0;
while (lead < inner.size() && inner[lead].isSpace()) lead++;
int trail = inner.size();
while (trail > lead && inner[trail - 1].isSpace()) trail--;
if (trail <= lead) return {};
return {start + lead, start + trail, true};
}
// ── Double-click hit test ──
static bool hitTestTarget(QsciScintilla* sci,
const QVector<LineMeta>& meta,
const QPoint& viewportPos,
int& outLine, EditTarget& outTarget)
{
long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
(unsigned long)viewportPos.x(), (long)viewportPos.y());
if (pos < 0) return false;
int line = (int)sci->SendScintilla(QsciScintillaBase::SCI_LINEFROMPOSITION,
(unsigned long)pos);
int col = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETCOLUMN,
(unsigned long)pos);
if (line < 0 || line >= meta.size()) return false;
QString lineText = getLineText(sci, line);
int textLen = lineText.size();
const LineMeta& lm = meta[line];
ColumnSpan ts = RcxEditor::typeSpan(lm);
ColumnSpan ns = RcxEditor::nameSpan(lm);
ColumnSpan vs = RcxEditor::valueSpan(lm, textLen);
if (!ns.valid)
ns = headerNameSpan(lm, lineText);
auto inSpan = [&](const ColumnSpan& s) {
return s.valid && col >= s.start && col < s.end;
};
if (inSpan(ts)) outTarget = EditTarget::Type;
else if (inSpan(ns)) outTarget = EditTarget::Name;
else if (inSpan(vs)) outTarget = EditTarget::Value;
else return false;
outLine = line;
return true;
}
// ── Event filter ──
bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
if (obj == m_sci && event->type() == QEvent::KeyPress) {
auto* ke = static_cast<QKeyEvent*>(event);
return m_editState.active ? handleEditKey(ke) : handleNormalKey(ke);
}
if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonPress
&& m_editState.active) {
// Only commit if click is outside the active edit span
auto* me = static_cast<QMouseEvent*>(event);
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
(unsigned long)me->pos().x(), (long)me->pos().y());
bool insideEdit = false;
if (pos >= 0) {
int clickLine = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos);
int clickCol = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos);
if (clickLine == m_editState.line) {
QString lineText = getLineText(m_sci, m_editState.line);
int delta = lineText.size() - m_editState.linelenAfterReplace;
int editEnd = m_editState.spanStart + m_editState.original.size() + delta;
insideEdit = (clickCol >= m_editState.spanStart && clickCol < editEnd);
}
}
if (!insideEdit)
commitInlineEdit();
return false; // always let click through to Scintilla
}
// Single-click on fold column (" - " / " + ") toggles fold
// Other left-clicks emit nodeClicked for selection
if (obj == m_sci->viewport() && !m_editState.active
&& event->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(event);
if (me->button() == Qt::LeftButton) {
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
(unsigned long)me->pos().x(), (long)me->pos().y());
if (pos >= 0) {
int line = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos);
int col = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos);
if (col < kFoldCol && line >= 0 && line < m_meta.size()
&& m_meta[line].foldHead) {
emit marginClicked(0, line, me->modifiers());
return true;
}
// Selection click — emit for controller to manage
if (line >= 0 && line < m_meta.size()) {
uint64_t nid = m_meta[line].nodeId;
if (nid != 0)
emit nodeClicked(line, nid, me->modifiers());
}
}
}
}
if (obj == m_sci->viewport() && !m_editState.active
&& event->type() == QEvent::MouseButtonDblClick) {
auto* me = static_cast<QMouseEvent*>(event);
int line; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), line, t))
return beginInlineEdit(t, line);
}
if (obj == m_sci && event->type() == QEvent::FocusOut) {
auto* fe = static_cast<QFocusEvent*>(event);
// Commit active edit on focus loss (click-away = save)
// Deferred so autocomplete popup has time to register as active
if (m_editState.active && fe->reason() != Qt::PopupFocusReason) {
QTimer::singleShot(0, this, [this]() {
if (m_editState.active && !m_sci->hasFocus()
&& !m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCACTIVE))
commitInlineEdit();
});
}
// Clear underlines when editor loses focus
if (m_hintLine >= 0) {
long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)m_hintLine);
long len = m_sci->SendScintilla(QsciScintillaBase::SCI_LINELENGTH, (unsigned long)m_hintLine);
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) {
int line, col;
m_sci->getCursorPosition(&line, &col);
updateEditableUnderline(line);
}
if (obj == m_sci->viewport() && !m_editState.active) {
if (event->type() == QEvent::MouseMove) {
m_lastHoverPos = static_cast<QMouseEvent*>(event)->pos();
m_hoverInside = true;
} else if (event->type() == QEvent::Leave) {
m_hoverInside = false;
m_hoveredNodeId = 0;
applyHoverHighlight();
} else if (event->type() == QEvent::Wheel) {
m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos());
m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos);
}
// Resolve hovered nodeId on move/wheel
if (event->type() == QEvent::MouseMove
|| event->type() == QEvent::Wheel) {
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
(unsigned long)m_lastHoverPos.x(),
(long)m_lastHoverPos.y());
uint64_t newHoverId = 0;
if (pos >= 0 && m_hoverInside) {
int hLine = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos);
if (hLine >= 0 && hLine < m_meta.size())
newHoverId = m_meta[hLine].nodeId;
}
if (newHoverId != m_hoveredNodeId) {
m_hoveredNodeId = newHoverId;
applyHoverHighlight();
}
}
if (event->type() == QEvent::MouseMove
|| event->type() == QEvent::Leave
|| event->type() == QEvent::Wheel)
applyHoverCursor();
}
return QWidget::eventFilter(obj, event);
}
// ── Normal mode key handling ──
bool RcxEditor::handleNormalKey(QKeyEvent* ke) {
switch (ke->key()) {
case Qt::Key_F2:
return beginInlineEdit(EditTarget::Name);
case Qt::Key_T:
if (ke->modifiers() == Qt::NoModifier)
return beginInlineEdit(EditTarget::Type);
return false;
case Qt::Key_Return:
case Qt::Key_Enter:
return beginInlineEdit(EditTarget::Value);
default:
return false;
}
}
// ── Edit mode key handling ──
bool RcxEditor::handleEditKey(QKeyEvent* ke) {
bool autocActive = m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCACTIVE);
switch (ke->key()) {
case Qt::Key_Return:
case Qt::Key_Enter:
case Qt::Key_Tab:
if (autocActive) {
if (m_editState.target == EditTarget::Type) {
// Extract selected typeName directly from autocomplete
QByteArray buf(256, '\0');
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCGETCURRENTTEXT,
(unsigned long)256, (void*)buf.data());
QString selectedType = QString::fromUtf8(buf.constData());
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL);
auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, EditTarget::Type, selectedType);
return true;
}
// Other targets: let Scintilla complete, then auto-commit
QTimer::singleShot(0, this, &RcxEditor::commitInlineEdit);
return false;
}
commitInlineEdit();
return true;
case Qt::Key_Escape:
if (autocActive) {
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL);
return true; // close popup, stay in edit mode
}
cancelInlineEdit();
return true;
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown:
if (autocActive) return false; // let Scintilla navigate list
return true; // block line navigation
case Qt::Key_Delete:
return true; // block to prevent eating trailing content
case Qt::Key_Left:
case Qt::Key_Backspace: {
int line, col;
m_sci->getCursorPosition(&line, &col);
if (col <= m_editState.spanStart) return true;
return false;
}
case Qt::Key_Home:
m_sci->setCursorPosition(m_editState.line, m_editState.spanStart);
return true;
default:
return false;
}
}
// ── Begin inline edit ──
bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
if (m_editState.active) return false;
if (m_cursorOverridden) {
QApplication::restoreOverrideCursor();
m_cursorOverridden = false;
}
m_hoveredNodeId = 0;
applyHoverHighlight();
if (line >= 0) {
m_sci->setCursorPosition(line, 0);
}
int col;
m_sci->getCursorPosition(&line, &col);
auto* lm = metaForLine(line);
if (!lm || lm->nodeIdx < 0) return false;
QString lineText = getLineText(m_sci, line);
int textLen = lineText.size();
ColumnSpan span;
switch (target) {
case EditTarget::Type: span = typeSpan(*lm); break;
case EditTarget::Name: span = nameSpan(*lm); break;
case EditTarget::Value: span = valueSpan(*lm, textLen); break;
}
if (!span.valid && target == EditTarget::Name)
span = headerNameSpan(*lm, lineText);
auto norm = normalizeSpan(span, lineText, target, /*skipPrefixes=*/true);
if (!norm.valid) return false;
QString trimmed = lineText.mid(norm.start, norm.end - norm.start);
m_editState.active = true;
m_editState.line = line;
m_editState.nodeIdx = lm->nodeIdx;
m_editState.subLine = lm->subLine;
m_editState.target = target;
m_editState.spanStart = norm.start;
m_editState.original = trimmed;
m_editState.linelenAfterReplace = textLen;
// Disable Scintilla undo during inline edit
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0);
m_sci->setReadOnly(false);
// Select just the trimmed text (keeps columns aligned)
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
(unsigned long)line);
long posSelStart = lineStart + m_editState.spanStart;
long posSelEnd = posSelStart + trimmed.toUtf8().size();
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posSelStart, posSelEnd);
if (target == EditTarget::Type)
QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete);
return true;
}
// ── Commit inline edit ──
void RcxEditor::commitInlineEdit() {
if (!m_editState.active) return;
QString lineText = getLineText(m_sci, m_editState.line);
int currentLen = lineText.size();
int delta = currentLen - m_editState.linelenAfterReplace;
int editedLen = m_editState.original.size() + delta;
QString editedText;
if (editedLen > 0)
editedText = lineText.mid(m_editState.spanStart, editedLen).trimmed();
auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText);
}
// ── Cancel inline edit ──
void RcxEditor::cancelInlineEdit() {
if (!m_editState.active) return;
endInlineEdit();
emit inlineEditCancelled();
}
// ── Type autocomplete ──
void RcxEditor::showTypeAutocomplete() {
if (!m_editState.active || m_editState.target != EditTarget::Type)
return;
// Collapse selection to start — old type text stays visible
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
(unsigned long)m_editState.line);
long posStart = lineStart + m_editState.spanStart;
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, posStart);
// Build list from typeName (matches what the editor displays)
QStringList types;
for (const auto& m : kKindMeta)
types << m.typeName;
types.sort(Qt::CaseInsensitive);
QByteArray list = types.join(QChar(' ')).toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' ');
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETIGNORECASE, (long)1);
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETDROPRESTOFWORD, (long)1);
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSHOW,
(uintptr_t)0, list.constData());
// Highlight the current type in the list
QByteArray cur = m_editState.original.toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSELECT,
(uintptr_t)0, cur.constData());
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION, 0, pos);
int y = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION, 0, pos);
QToolTip::showText(
m_sci->viewport()->mapToGlobal(QPoint(x, y + 20)),
QStringLiteral("Type to filter \u2022 \u2191/\u2193 select \u2022 Enter apply \u2022 Esc cancel"),
m_sci);
}
// ── Editable-field underline indicator ──
void RcxEditor::updateEditableUnderline(int line) {
if (m_editState.active) return;
if (line == m_hintLine) return;
auto clearLine = [&](int l) {
if (l < 0) return;
long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)l);
long len = m_sci->SendScintilla(QsciScintillaBase::SCI_LINELENGTH, (unsigned long)l);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, start, len);
};
clearLine(m_hintLine);
m_hintLine = line;
const LineMeta* lm = metaForLine(line);
if (!lm || lm->nodeIdx < 0) return;
QString lineText = getLineText(m_sci, line);
int textLen = lineText.size();
ColumnSpan ts = typeSpan(*lm);
ColumnSpan ns = nameSpan(*lm);
ColumnSpan vs = valueSpan(*lm, textLen);
if (!ns.valid)
ns = headerNameSpan(*lm, lineText);
auto underlineSpan = [&](ColumnSpan s, EditTarget tgt) {
auto norm = normalizeSpan(s, lineText, tgt, /*skipPrefixes=*/true);
if (!norm.valid) return;
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE,
lineStart + norm.start, norm.end - norm.start);
};
underlineSpan(ts, EditTarget::Type);
underlineSpan(ns, EditTarget::Name);
underlineSpan(vs, EditTarget::Value);
}
// ── Hover cursor (coalesced) ──
void RcxEditor::applyHoverCursor() {
if (m_editState.active || !m_hoverInside
|| !m_sci->viewport()->underMouse()) {
if (m_cursorOverridden) {
QApplication::restoreOverrideCursor();
m_cursorOverridden = false;
}
return;
}
int line; EditTarget t;
bool interactive = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t);
// Also show pointer cursor for fold column on fold-head lines
if (!interactive) {
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
(unsigned long)m_lastHoverPos.x(),
(long)m_lastHoverPos.y());
if (pos >= 0) {
int hLine = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos);
int hCol = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos);
if (hCol < kFoldCol && hLine >= 0 && hLine < m_meta.size()
&& m_meta[hLine].foldHead)
interactive = true;
}
}
if (interactive && !m_cursorOverridden) {
QApplication::setOverrideCursor(Qt::PointingHandCursor);
m_cursorOverridden = true;
} else if (!interactive && m_cursorOverridden) {
QApplication::restoreOverrideCursor();
m_cursorOverridden = false;
}
}
} // namespace rcx

109
src/editor.h Normal file
View File

@@ -0,0 +1,109 @@
#pragma once
#include "core.h"
#include <QWidget>
#include <QSet>
#include <QPoint>
class QsciScintilla;
class QsciLexerCPP;
namespace rcx {
class RcxEditor : public QWidget {
Q_OBJECT
public:
explicit RcxEditor(QWidget* parent = nullptr);
void applyDocument(const ComposeResult& result);
ViewState saveViewState() const;
void restoreViewState(const ViewState& vs);
QsciScintilla* scintilla() const { return m_sci; }
const LineMeta* metaForLine(int line) const;
int currentNodeIndex() const;
// ── Column span computation ──
static ColumnSpan typeSpan(const LineMeta& lm);
static ColumnSpan nameSpan(const LineMeta& lm);
static ColumnSpan valueSpan(const LineMeta& lm, int lineLength);
// ── Multi-selection ──
QSet<int> selectedNodeIndices() const;
// ── Inline editing ──
bool isEditing() const { return m_editState.active; }
bool beginInlineEdit(EditTarget target, int line = -1);
void cancelInlineEdit();
void applySelectionOverlay(const QSet<uint64_t>& selIds);
signals:
void marginClicked(int margin, int line, Qt::KeyboardModifiers mods);
void contextMenuRequested(int line, int nodeIdx, int subLine, QPoint globalPos);
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
void inlineEditCommitted(int nodeIdx, int subLine,
EditTarget target, const QString& text);
void inlineEditCancelled();
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
private:
QsciScintilla* m_sci = nullptr;
QsciLexerCPP* m_lexer = nullptr;
QVector<LineMeta> m_meta;
int m_marginStyleBase = -1;
int m_hintLine = -1;
// ── Hover cursor + highlight ──
QPoint m_lastHoverPos;
bool m_hoverInside = false;
bool m_cursorOverridden = false;
uint64_t m_hoveredNodeId = 0;
QSet<uint64_t> m_currentSelIds;
// ── Inline edit state ──
struct InlineEditState {
bool active = false;
int line = -1;
int nodeIdx = -1;
int subLine = 0;
EditTarget target = EditTarget::Name;
int spanStart = 0;
int linelenAfterReplace = 0;
QString original;
};
InlineEditState m_editState;
void setupScintilla();
void setupLexer();
void setupMargins();
void setupFolding();
void setupMarkers();
void allocateMarginStyles();
void applyMarginText(const QVector<LineMeta>& meta);
void applyMarkers(const QVector<LineMeta>& meta);
void applyFoldLevels(const QVector<LineMeta>& meta);
void applyHexDimming(const QVector<LineMeta>& meta);
void commitInlineEdit();
bool handleNormalKey(QKeyEvent* ke);
bool handleEditKey(QKeyEvent* ke);
void showTypeAutocomplete();
void updateEditableUnderline(int line);
void applyHoverCursor();
void applyHoverHighlight();
// ── Refactored helpers ──
struct EndEditInfo { int nodeIdx; int subLine; EditTarget target; };
EndEditInfo endInlineEdit();
struct NormalizedSpan { int start = 0; int end = 0; bool valid = false; };
NormalizedSpan normalizeSpan(const ColumnSpan& raw, const QString& lineText,
EditTarget target, bool skipPrefixes) const;
};
} // namespace rcx

364
src/format.cpp Normal file
View File

@@ -0,0 +1,364 @@
#include "core.h"
#include <cstring>
#include <limits>
namespace rcx::fmt {
// ── Column layout ──
// COL_TYPE and COL_NAME use shared constants from core.h (kColType, kColName)
static constexpr int COL_TYPE = kColType;
static constexpr int COL_NAME = kColName;
static constexpr int COL_VALUE = 22;
static const QString SEP = QStringLiteral(" ");
static QString fit(QString s, int w) {
if (w <= 0) return {};
if (s.size() > w) {
if (w >= 2) s = s.left(w - 1) + QChar(0x2026); // ellipsis
else s = s.left(w);
}
return s.leftJustified(w, ' ');
}
// ── Type name ──
// Override seam: injectable type-name provider
static TypeNameFn g_typeNameFn = nullptr;
void setTypeNameProvider(TypeNameFn fn) { g_typeNameFn = fn; }
QString typeName(NodeKind kind) {
if (g_typeNameFn) return fit(g_typeNameFn(kind), COL_TYPE);
auto* m = kindMeta(kind);
return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), COL_TYPE);
}
// ── Value formatting ──
static QString hexStr(uint64_t v, int digits) {
return QStringLiteral("0x") + QString::number(v, 16).toUpper().rightJustified(digits, '0');
}
static QString rawHex(uint64_t v, int digits) {
return QString::number(v, 16).toUpper().rightJustified(digits, '0');
}
QString fmtInt8(int8_t v) { return QString::number(v); }
QString fmtInt16(int16_t v) { return QString::number(v); }
QString fmtInt32(int32_t v) { return QString::number(v); }
QString fmtInt64(int64_t v) { return QString::number(v); }
QString fmtUInt8(uint8_t v) { return QString::number(v); }
QString fmtUInt16(uint16_t v) { return QString::number(v); }
QString fmtUInt32(uint32_t v) { return QString::number(v); }
QString fmtUInt64(uint64_t v) { return QString::number(v); }
QString fmtFloat(float v) { return QString::number(v, 'f', 3); }
QString fmtDouble(double v) { return QString::number(v, 'f', 6); }
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
QString fmtPointer32(uint32_t v) {
if (v == 0) return QStringLiteral("-> NULL");
return QStringLiteral("-> ") + hexStr(v, 8);
}
QString fmtPointer64(uint64_t v) {
if (v == 0) return QStringLiteral("-> NULL");
return QStringLiteral("-> ") + hexStr(v, 16);
}
// ── Indentation ──
QString indent(int depth) {
return QString(depth * 3, ' ');
}
// ── Offset margin ──
QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation) {
if (isContinuation) return QStringLiteral(" \u00B7");
return QStringLiteral("+0x") + QString::number(relativeOffset, 16).toUpper();
}
// ── Struct header / footer ──
QString fmtStructHeader(const Node& node, int depth) {
return indent(depth) + typeName(node.kind).trimmed() +
QStringLiteral(" ") + node.name + QStringLiteral(" {");
}
QString fmtStructFooter(const Node& node, int depth, int totalSize) {
QString s = indent(depth) + QStringLiteral("}; // ") + node.name;
if (totalSize > 0)
s += QStringLiteral(" sizeof=0x") + QString::number(totalSize, 16).toUpper();
return s;
}
// ── Hex / ASCII preview ──
static inline bool isAsciiPrintable(uint8_t c) { return c >= 0x20 && c <= 0x7E; }
static QString bytesToAscii(const QByteArray& b, int slot) {
QString out;
out.reserve(slot);
for (int i = 0; i < slot; ++i) {
uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0;
out += isAsciiPrintable(c) ? QChar(c) : QChar('.');
}
return out;
}
static QString bytesToHex(const QByteArray& b, int slot) {
QString out;
out.reserve(slot * 3);
for (int i = 0; i < slot; ++i) {
uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0;
out += QString::asprintf("%02X", (unsigned)c);
if (i + 1 < slot) out += ' ';
}
return out;
}
static QString fmtAsciiAndBytes(const Provider& prov, uint64_t addr,
int sizeBytes, int slotBytes = 8) {
const int slot = qMax(slotBytes, sizeBytes);
QByteArray b = prov.isReadable(addr, slot)
? prov.readBytes(addr, slot)
: QByteArray(slot, '\0');
return bytesToAscii(b, slot) + QStringLiteral(" ") + bytesToHex(b, slot);
}
// ── Single value from provider (unified) ──
enum class ValueMode { Display, Editable };
static QString readValueImpl(const Node& node, const Provider& prov,
uint64_t addr, int subLine, ValueMode mode) {
const bool display = (mode == ValueMode::Display);
switch (node.kind) {
case NodeKind::Hex8: return display ? hexStr(prov.readU8(addr), 2) : rawHex(prov.readU8(addr), 2);
case NodeKind::Hex16: return display ? hexStr(prov.readU16(addr), 4) : rawHex(prov.readU16(addr), 4);
case NodeKind::Hex32: return display ? hexStr(prov.readU32(addr), 8) : rawHex(prov.readU32(addr), 8);
case NodeKind::Hex64: return display ? hexStr(prov.readU64(addr), 16): rawHex(prov.readU64(addr), 16);
case NodeKind::Int8: return fmtInt8((int8_t)prov.readU8(addr));
case NodeKind::Int16: return fmtInt16((int16_t)prov.readU16(addr));
case NodeKind::Int32: return fmtInt32((int32_t)prov.readU32(addr));
case NodeKind::Int64: return fmtInt64((int64_t)prov.readU64(addr));
case NodeKind::UInt8: return fmtUInt8(prov.readU8(addr));
case NodeKind::UInt16: return fmtUInt16(prov.readU16(addr));
case NodeKind::UInt32: return fmtUInt32(prov.readU32(addr));
case NodeKind::UInt64: return fmtUInt64(prov.readU64(addr));
case NodeKind::Float: { auto s = fmtFloat(prov.readF32(addr)); return display ? s : s.trimmed(); }
case NodeKind::Double: { auto s = fmtDouble(prov.readF64(addr)); return display ? s : s.trimmed(); }
case NodeKind::Bool: return fmtBool(prov.readU8(addr));
case NodeKind::Pointer32: return display ? fmtPointer32(prov.readU32(addr)) : rawHex(prov.readU32(addr), 8);
case NodeKind::Pointer64: return display ? fmtPointer64(prov.readU64(addr)) : rawHex(prov.readU64(addr), 16);
case NodeKind::Vec2:
case NodeKind::Vec3:
case NodeKind::Vec4: {
int maxSub = linesForKind(node.kind);
if (subLine < 0 || subLine >= maxSub) return QStringLiteral("?");
float component = prov.readF32(addr + subLine * 4);
if (!display) return fmtFloat(component).trimmed();
static const char* labels[] = {"x", "y", "z", "w"};
return QString(labels[subLine]) + QStringLiteral(" = ") + fmtFloat(component);
}
case NodeKind::Mat4x4: {
if (!display) return {}; // not editable as single value
if (subLine < 0 || subLine >= 4) return QStringLiteral("?");
QString line = QStringLiteral("[");
for (int c = 0; c < 4; c++) {
if (c > 0) line += QStringLiteral(", ");
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4)).trimmed();
}
line += QStringLiteral("]");
return line;
}
case NodeKind::Padding: return display ? hexStr(prov.readU8(addr), 2) : rawHex(prov.readU8(addr), 2);
case NodeKind::UTF8: {
QByteArray bytes = prov.readBytes(addr, node.strLen);
int end = bytes.indexOf('\0');
if (end >= 0) bytes.truncate(end);
QString s = QString::fromUtf8(bytes);
return display ? (QStringLiteral("\"") + s + QStringLiteral("\"")) : s;
}
case NodeKind::UTF16: {
QByteArray bytes = prov.readBytes(addr, node.strLen * 2);
QString s = QString::fromUtf16(reinterpret_cast<const char16_t*>(bytes.data()),
bytes.size() / 2);
int end = s.indexOf(QChar(0));
if (end >= 0) s.truncate(end);
return display ? (QStringLiteral("L\"") + s + QStringLiteral("\"")) : s;
}
default:
return {};
}
}
QString readValue(const Node& node, const Provider& prov,
uint64_t addr, int subLine) {
return readValueImpl(node, prov, addr, subLine, ValueMode::Display);
}
// ── Full node line ──
QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine) {
QString ind = indent(depth);
QString type = typeName(node.kind);
QString name = fit(node.name, COL_NAME);
// Blank prefix for continuation lines (same width as type+sep+name+sep)
const int prefixW = COL_TYPE + COL_NAME + 4; // 2 seps × 2 chars
// Mat4x4: subLine 0..3 = rows
if (node.kind == NodeKind::Mat4x4) {
QString val = readValue(node, prov, addr, subLine);
if (subLine == 0) return ind + type + SEP + name + SEP + val;
return ind + QString(prefixW, ' ') + val;
}
// For vector types, subLine selects component
if (subLine > 0 && (node.kind == NodeKind::Vec2 ||
node.kind == NodeKind::Vec3 ||
node.kind == NodeKind::Vec4)) {
QString val = readValue(node, prov, addr, subLine);
return ind + QString(prefixW, ' ') + val;
}
// Hex nodes and Padding: ASCII preview + hex bytes (compact)
if (node.kind == NodeKind::Hex8 || node.kind == NodeKind::Hex16 ||
node.kind == NodeKind::Hex32 || node.kind == NodeKind::Hex64 ||
node.kind == NodeKind::Padding)
{
if (node.kind == NodeKind::Padding) {
const int totalSz = qMax(1, node.arrayLen);
const int lineOff = subLine * 8;
const int lineBytes = qMin(8, totalSz - lineOff);
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
QString ascii = bytesToAscii(b, lineBytes);
QString hex = bytesToHex(b, lineBytes);
if (subLine == 0)
return ind + type + SEP + ascii + SEP + hex;
return ind + QString(COL_TYPE + (int)SEP.size(), ' ') + ascii + SEP + hex;
}
// Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns
const int sz = sizeForKind(node.kind);
QByteArray b = prov.isReadable(addr, sz)
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
QString ascii = bytesToAscii(b, sz).leftJustified(8, ' ');
QString hex = bytesToHex(b, sz);
return ind + type + SEP + ascii + SEP + hex;
}
QString val = readValue(node, prov, addr, subLine);
return ind + type + SEP + name + SEP + val;
}
// ── Editable value (parse-friendly form for edit dialog) ──
QString editableValue(const Node& node, const Provider& prov,
uint64_t addr, int subLine) {
return readValueImpl(node, prov, addr, subLine, ValueMode::Editable);
}
// ── Value parsing (text → bytes) ──
template<class T>
static QByteArray toBytes(T v) {
QByteArray b(sizeof(T), Qt::Uninitialized);
memcpy(b.data(), &v, sizeof(T));
return b;
}
static QString stripHex(const QString& s) {
if (s.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
return s.mid(2);
return s;
}
// Range-checked narrowing: sets *ok = false if parsed value doesn't fit in T
template<class T, class ParseT>
static QByteArray parseIntChecked(ParseT val, bool* ok) {
if (*ok) {
using L = std::numeric_limits<T>;
if constexpr (std::is_signed_v<T>) {
if (val < (ParseT)L::min() || val > (ParseT)L::max()) *ok = false;
} else {
if (val > (ParseT)L::max()) *ok = false;
}
}
return *ok ? toBytes<T>(static_cast<T>(val)) : QByteArray{};
}
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
*ok = false;
QString s = text.trimmed();
// Allow empty for string types (will produce zero-length content, caller pads)
if (s.isEmpty()) {
if (kind == NodeKind::UTF8 || kind == NodeKind::UTF16) {
*ok = true;
return {};
}
return {};
}
switch (kind) {
case NodeKind::Hex8: { uint val = stripHex(s).remove(' ').toUInt(ok, 16); return parseIntChecked<uint8_t>(val, ok); }
case NodeKind::Hex16: { uint val = stripHex(s).remove(' ').toUInt(ok, 16); return parseIntChecked<uint16_t>(val, ok); }
case NodeKind::Hex32: { uint val = stripHex(s).remove(' ').toUInt(ok, 16); return *ok ? toBytes<uint32_t>(val) : QByteArray{}; }
case NodeKind::Hex64: { qulonglong val = stripHex(s).remove(' ').toULongLong(ok, 16); return *ok ? toBytes<uint64_t>(val) : QByteArray{}; }
case NodeKind::Int8: { int val = s.toInt(ok); return parseIntChecked<int8_t>(val, ok); }
case NodeKind::Int16: { int val = s.toInt(ok); return parseIntChecked<int16_t>(val, ok); }
case NodeKind::Int32: { int val = s.toInt(ok); return *ok ? toBytes<int32_t>(val) : QByteArray{}; }
case NodeKind::Int64: { qlonglong val = s.toLongLong(ok); return *ok ? toBytes<int64_t>(val) : QByteArray{}; }
case NodeKind::UInt8: { uint val = s.toUInt(ok); return parseIntChecked<uint8_t>(val, ok); }
case NodeKind::UInt16: { uint val = s.toUInt(ok); return parseIntChecked<uint16_t>(val, ok); }
case NodeKind::UInt32: { uint val = s.toUInt(ok); return *ok ? toBytes<uint32_t>(val) : QByteArray{}; }
case NodeKind::UInt64: { qulonglong val = s.toULongLong(ok); return *ok ? toBytes<uint64_t>(val) : QByteArray{}; }
case NodeKind::Float: {
float val = s.toFloat(ok);
return *ok ? toBytes<float>(val) : QByteArray{};
}
case NodeKind::Double: {
double val = s.toDouble(ok);
return *ok ? toBytes<double>(val) : QByteArray{};
}
case NodeKind::Bool: {
if (s == QStringLiteral("true") || s == QStringLiteral("1")) {
*ok = true; return toBytes<uint8_t>(1);
}
if (s == QStringLiteral("false") || s == QStringLiteral("0")) {
*ok = true; return toBytes<uint8_t>(0);
}
return {}; // unknown token → ok stays false
}
case NodeKind::Pointer32: {
uint val = stripHex(s).toUInt(ok, 16);
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
}
case NodeKind::Pointer64: {
qulonglong val = stripHex(s).toULongLong(ok, 16);
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
}
case NodeKind::UTF8: {
*ok = true;
if (s.startsWith('"') && s.endsWith('"'))
s = s.mid(1, s.size() - 2);
return s.toUtf8();
}
case NodeKind::UTF16: {
*ok = true;
if (s.startsWith(QStringLiteral("L\""))) s = s.mid(2);
else if (s.startsWith('"')) s = s.mid(1);
if (s.endsWith('"')) s.chop(1);
QByteArray b(s.size() * 2, Qt::Uninitialized);
memcpy(b.data(), s.utf16(), s.size() * 2);
return b;
}
default:
return {};
}
}
} // namespace rcx::fmt

6
src/icons.qrc Normal file
View File

@@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/icons">
<file alias="chevron-right.png">icons/chevron-right.png</file>
<file alias="chevron-down.png">icons/chevron-down.png</file>
</qresource>
</RCC>

BIN
src/icons/chevron-down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

BIN
src/icons/chevron-right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

BIN
src/icons/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

636
src/main.cpp Normal file
View File

@@ -0,0 +1,636 @@
#include "controller.h"
#include <QApplication>
#include <QMainWindow>
#include <QMdiArea>
#include <QMdiSubWindow>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QLabel>
#include <QSplitter>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QAction>
#include <QMap>
#include <QTimer>
#include <QDir>
#include <QMetaObject>
#ifdef _WIN32
#include <windows.h>
#include <dbghelp.h>
#include <cstdio>
static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
fprintf(stderr, "\n=== UNHANDLED EXCEPTION ===\n");
fprintf(stderr, "Code : 0x%08lX\n", ep->ExceptionRecord->ExceptionCode);
fprintf(stderr, "Addr : %p\n", ep->ExceptionRecord->ExceptionAddress);
HANDLE process = GetCurrentProcess();
HANDLE thread = GetCurrentThread();
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
SymInitialize(process, NULL, TRUE);
CONTEXT* ctx = ep->ContextRecord;
STACKFRAME64 frame = {};
DWORD machineType;
#ifdef _M_X64
machineType = IMAGE_FILE_MACHINE_AMD64;
frame.AddrPC.Offset = ctx->Rip;
frame.AddrFrame.Offset = ctx->Rbp;
frame.AddrStack.Offset = ctx->Rsp;
#else
machineType = IMAGE_FILE_MACHINE_I386;
frame.AddrPC.Offset = ctx->Eip;
frame.AddrFrame.Offset = ctx->Ebp;
frame.AddrStack.Offset = ctx->Esp;
#endif
frame.AddrPC.Mode = AddrModeFlat;
frame.AddrFrame.Mode = AddrModeFlat;
frame.AddrStack.Mode = AddrModeFlat;
fprintf(stderr, "\nStack trace:\n");
for (int i = 0; i < 64; i++) {
if (!StackWalk64(machineType, process, thread, &frame, ctx,
NULL, SymFunctionTableAccess64,
SymGetModuleBase64, NULL))
break;
if (frame.AddrPC.Offset == 0) break;
char buf[sizeof(SYMBOL_INFO) + 256];
SYMBOL_INFO* sym = reinterpret_cast<SYMBOL_INFO*>(buf);
sym->SizeOfStruct = sizeof(SYMBOL_INFO);
sym->MaxNameLen = 255;
DWORD64 disp64 = 0;
DWORD disp32 = 0;
IMAGEHLP_LINE64 line = {};
line.SizeOfStruct = sizeof(line);
bool hasSym = SymFromAddr(process, frame.AddrPC.Offset, &disp64, sym);
bool hasLine = SymGetLineFromAddr64(process, frame.AddrPC.Offset,
&disp32, &line);
if (hasSym && hasLine) {
fprintf(stderr, " [%2d] %s+0x%llx (%s:%lu)\n",
i, sym->Name, (unsigned long long)disp64,
line.FileName, line.LineNumber);
} else if (hasSym) {
fprintf(stderr, " [%2d] %s+0x%llx\n",
i, sym->Name, (unsigned long long)disp64);
} else {
fprintf(stderr, " [%2d] 0x%llx\n",
i, (unsigned long long)frame.AddrPC.Offset);
}
}
SymCleanup(process);
fprintf(stderr, "=== END CRASH ===\n");
fflush(stderr);
return EXCEPTION_EXECUTE_HANDLER;
}
#endif
namespace rcx {
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = nullptr);
private slots:
void newFile();
void openFile();
void saveFile();
void saveFileAs();
void loadBinary();
void addNode();
void removeNode();
void changeNodeType();
void renameNodeAction();
void splitView();
void unsplitView();
void undo();
void redo();
void about();
private:
QMdiArea* m_mdiArea;
QLabel* m_statusLabel;
struct TabState {
RcxDocument* doc;
RcxController* ctrl;
QSplitter* splitter;
};
QMap<QMdiSubWindow*, TabState> m_tabs;
void createMenus();
void createStatusBar();
RcxController* activeController() const;
TabState* activeTab();
QMdiSubWindow* createTab(RcxDocument* doc);
void updateWindowTitle();
};
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setWindowTitle("ReclassX");
resize(1200, 800);
m_mdiArea = new QMdiArea(this);
m_mdiArea->setViewMode(QMdiArea::TabbedView);
m_mdiArea->setTabsClosable(true);
m_mdiArea->setTabsMovable(true);
setCentralWidget(m_mdiArea);
createMenus();
createStatusBar();
connect(m_mdiArea, &QMdiArea::subWindowActivated,
this, [this](QMdiSubWindow*) { updateWindowTitle(); });
}
void MainWindow::createMenus() {
// File
auto* file = menuBar()->addMenu("&File");
file->addAction("&New", QKeySequence::New, this, &MainWindow::newFile);
file->addAction("&Open...", QKeySequence::Open, this, &MainWindow::openFile);
file->addSeparator();
file->addAction("&Save", QKeySequence::Save, this, &MainWindow::saveFile);
file->addAction("Save &As...", QKeySequence::SaveAs, this, &MainWindow::saveFileAs);
file->addSeparator();
file->addAction("Load &Binary...", this, &MainWindow::loadBinary);
file->addSeparator();
file->addAction("E&xit", QKeySequence::Quit, this, &QMainWindow::close);
// Edit
auto* edit = menuBar()->addMenu("&Edit");
edit->addAction("&Undo", QKeySequence::Undo, this, &MainWindow::undo);
edit->addAction("&Redo", QKeySequence::Redo, this, &MainWindow::redo);
// View
auto* view = menuBar()->addMenu("&View");
view->addAction("Split &Horizontal", this, &MainWindow::splitView);
view->addAction("&Unsplit", this, &MainWindow::unsplitView);
// Node
auto* node = menuBar()->addMenu("&Node");
node->addAction("&Add Field", QKeySequence(Qt::Key_Insert), this, &MainWindow::addNode);
node->addAction("&Remove Field", QKeySequence::Delete, this, &MainWindow::removeNode);
auto* actType = node->addAction("Change &Type", this, &MainWindow::changeNodeType);
actType->setText("Change &Type\tT");
auto* actName = node->addAction("Re&name", this, &MainWindow::renameNodeAction);
actName->setText("Re&name\tF2");
// Help
auto* help = menuBar()->addMenu("&Help");
help->addAction("&About ReclassX", this, &MainWindow::about);
}
void MainWindow::createStatusBar() {
m_statusLabel = new QLabel("Ready");
statusBar()->addWidget(m_statusLabel, 1);
statusBar()->setStyleSheet("QStatusBar { background: #252526; color: #858585; }");
}
QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
auto* splitter = new QSplitter(Qt::Horizontal);
auto* ctrl = new RcxController(doc, splitter);
ctrl->addSplitEditor(splitter);
auto* sub = m_mdiArea->addSubWindow(splitter);
sub->setWindowTitle(doc->filePath.isEmpty()
? "Untitled" : QFileInfo(doc->filePath).fileName());
sub->setAttribute(Qt::WA_DeleteOnClose);
sub->showMaximized();
m_tabs[sub] = { doc, ctrl, splitter };
connect(sub, &QObject::destroyed, this, [this, sub]() {
auto it = m_tabs.find(sub);
if (it != m_tabs.end()) {
it->doc->deleteLater();
m_tabs.erase(it);
}
});
connect(ctrl, &RcxController::nodeSelected,
this, [this, ctrl](int nodeIdx) {
if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) {
auto& node = ctrl->document()->tree.nodes[nodeIdx];
m_statusLabel->setText(
QString("%1 %2 offset: +0x%3 size: %4 bytes")
.arg(kindToString(node.kind))
.arg(node.name)
.arg(node.offset, 4, 16, QChar('0'))
.arg(node.byteSize()));
} else {
m_statusLabel->setText("Ready");
}
});
ctrl->refresh();
return sub;
}
void MainWindow::newFile() {
auto* doc = new RcxDocument(this);
// Autoload self as binary data
doc->loadData(QCoreApplication::applicationFilePath());
doc->tree.baseAddress = 0;
// Read e_lfanew to find PE header offset
uint32_t lfanew = doc->provider->readU32(0x3C);
if (lfanew < 0x40 || lfanew >= (uint32_t)doc->provider->size())
lfanew = 0x40;
uint32_t pe = lfanew; // PE signature
uint32_t fh = pe + 4; // IMAGE_FILE_HEADER
uint32_t oh = fh + 20; // IMAGE_OPTIONAL_HEADER (PE32+)
Node root;
root.kind = NodeKind::Struct;
root.name = "PE_HEADER";
root.parentId = 0;
root.offset = 0;
int ri = doc->tree.addNode(root);
uint64_t rootId = doc->tree.nodes[ri].id;
auto add = [&](NodeKind k, const QString& name, int off) {
Node n;
n.kind = k;
n.name = name;
n.offset = off;
n.parentId = rootId;
doc->tree.addNode(n);
};
// ── IMAGE_DOS_HEADER (0x00 0x3F) ──
add(NodeKind::UInt16, "e_magic", 0x00);
add(NodeKind::UInt16, "e_cblp", 0x02);
add(NodeKind::UInt16, "e_cp", 0x04);
add(NodeKind::UInt16, "e_crlc", 0x06);
add(NodeKind::UInt16, "e_cparhdr", 0x08);
add(NodeKind::UInt16, "e_minalloc", 0x0A);
add(NodeKind::UInt16, "e_maxalloc", 0x0C);
add(NodeKind::UInt16, "e_ss", 0x0E);
add(NodeKind::UInt16, "e_sp", 0x10);
add(NodeKind::UInt16, "e_csum", 0x12);
add(NodeKind::UInt16, "e_ip", 0x14);
add(NodeKind::UInt16, "e_cs", 0x16);
add(NodeKind::UInt16, "e_lfarlc", 0x18);
add(NodeKind::UInt16, "e_ovno", 0x1A);
add(NodeKind::Hex64, "e_res", 0x1C);
add(NodeKind::UInt16, "e_oemid", 0x24);
add(NodeKind::UInt16, "e_oeminfo", 0x26);
add(NodeKind::Hex64, "e_res2_0", 0x28);
add(NodeKind::Hex64, "e_res2_1", 0x30);
add(NodeKind::Hex32, "e_res2_2", 0x38);
add(NodeKind::UInt32, "e_lfanew", 0x3C);
// ── DOS Stub (0x40 to PE signature) — fill with Hex nodes ──
{
int cursor = 0x40;
while (cursor + 8 <= (int)pe) {
add(NodeKind::Hex64,
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
cursor);
cursor += 8;
}
if (cursor + 4 <= (int)pe) {
add(NodeKind::Hex32,
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
cursor);
cursor += 4;
}
if (cursor + 2 <= (int)pe) {
add(NodeKind::Hex16,
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
cursor);
cursor += 2;
}
if (cursor + 1 <= (int)pe) {
add(NodeKind::Hex8,
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
cursor);
cursor += 1;
}
}
// ── PE Signature ──
add(NodeKind::UInt32, "Signature", pe);
// ── IMAGE_FILE_HEADER (nested struct) ──
{
Node fhStruct;
fhStruct.kind = NodeKind::Struct;
fhStruct.name = "IMAGE_FILE_HEADER";
fhStruct.parentId = rootId;
fhStruct.offset = fh;
int fi = doc->tree.addNode(fhStruct);
uint64_t fhId = doc->tree.nodes[fi].id;
auto addFH = [&](NodeKind k, const QString& name, int off) {
Node n;
n.kind = k;
n.name = name;
n.offset = off;
n.parentId = fhId;
doc->tree.addNode(n);
};
addFH(NodeKind::UInt16, "Machine", 0x00);
addFH(NodeKind::UInt16, "NumberOfSections", 0x02);
addFH(NodeKind::UInt32, "TimeDateStamp", 0x04);
addFH(NodeKind::UInt32, "PtrToSymbolTable", 0x08);
addFH(NodeKind::UInt32, "NumberOfSymbols", 0x0C);
addFH(NodeKind::UInt16, "SizeOfOptionalHeader", 0x10);
addFH(NodeKind::UInt16, "Characteristics", 0x12);
}
// ── IMAGE_OPTIONAL_HEADER64 (nested struct) ──
{
Node ohStruct;
ohStruct.kind = NodeKind::Struct;
ohStruct.name = "IMAGE_OPTIONAL_HEADER64";
ohStruct.parentId = rootId;
ohStruct.offset = oh;
int oi = doc->tree.addNode(ohStruct);
uint64_t ohId = doc->tree.nodes[oi].id;
auto addOH = [&](NodeKind k, const QString& name, int off) {
Node n;
n.kind = k;
n.name = name;
n.offset = off;
n.parentId = ohId;
doc->tree.addNode(n);
};
addOH(NodeKind::UInt16, "Magic", 0x00);
addOH(NodeKind::UInt8, "MajorLinkerVersion", 0x02);
addOH(NodeKind::UInt8, "MinorLinkerVersion", 0x03);
addOH(NodeKind::UInt32, "SizeOfCode", 0x04);
addOH(NodeKind::UInt32, "SizeOfInitData", 0x08);
addOH(NodeKind::UInt32, "SizeOfUninitData", 0x0C);
addOH(NodeKind::UInt32, "AddressOfEntryPoint", 0x10);
addOH(NodeKind::UInt32, "BaseOfCode", 0x14);
addOH(NodeKind::UInt64, "ImageBase", 0x18);
addOH(NodeKind::UInt32, "SectionAlignment", 0x20);
addOH(NodeKind::UInt32, "FileAlignment", 0x24);
addOH(NodeKind::UInt16, "MajorOSVersion", 0x28);
addOH(NodeKind::UInt16, "MinorOSVersion", 0x2A);
addOH(NodeKind::UInt16, "MajorImageVersion", 0x2C);
addOH(NodeKind::UInt16, "MinorImageVersion", 0x2E);
addOH(NodeKind::UInt16, "MajorSubsysVersion", 0x30);
addOH(NodeKind::UInt16, "MinorSubsysVersion", 0x32);
addOH(NodeKind::UInt32, "Win32VersionValue", 0x34);
addOH(NodeKind::UInt32, "SizeOfImage", 0x38);
addOH(NodeKind::UInt32, "SizeOfHeaders", 0x3C);
addOH(NodeKind::UInt32, "CheckSum", 0x40);
addOH(NodeKind::UInt16, "Subsystem", 0x44);
addOH(NodeKind::UInt16, "DllCharacteristics", 0x46);
addOH(NodeKind::UInt64, "SizeOfStackReserve", 0x48);
addOH(NodeKind::UInt64, "SizeOfStackCommit", 0x50);
addOH(NodeKind::UInt64, "SizeOfHeapReserve", 0x58);
addOH(NodeKind::UInt64, "SizeOfHeapCommit", 0x60);
addOH(NodeKind::UInt32, "LoaderFlags", 0x68);
addOH(NodeKind::UInt32, "NumberOfRvaAndSizes", 0x6C);
// Data directories (16 entries × 8 bytes)
static const char* dirNames[] = {
"Export", "Import", "Resource", "Exception",
"Security", "BaseReloc", "Debug", "Architecture",
"GlobalPtr", "TLS", "LoadConfig", "BoundImport",
"IAT", "DelayImport", "CLR", "Reserved"
};
for (int i = 0; i < 16; i++) {
int doff = 0x70 + i * 8;
addOH(NodeKind::UInt32, QString("%1_RVA").arg(dirNames[i]), doff);
addOH(NodeKind::UInt32, QString("%1_Size").arg(dirNames[i]), doff + 4);
}
}
// ── 0x100 bytes of Hex64 padding (32 nodes) ──
int padStart = oh + 0xF0; // end of optional header
for (int i = 0; i < 32; i++) {
int off = padStart + i * 8;
add(NodeKind::Hex64,
QString("pad_%1").arg(off, 4, 16, QChar('0')),
off);
}
createTab(doc);
}
void MainWindow::openFile() {
QString path = QFileDialog::getOpenFileName(this,
"Open Definition", {}, "ReclassX (*.rcx);;JSON (*.json);;All (*)");
if (path.isEmpty()) return;
auto* doc = new RcxDocument(this);
if (!doc->load(path)) {
QMessageBox::warning(this, "Error", "Failed to load: " + path);
delete doc;
return;
}
createTab(doc);
}
void MainWindow::saveFile() {
auto* tab = activeTab();
if (!tab) return;
if (tab->doc->filePath.isEmpty()) { saveFileAs(); return; }
tab->doc->save(tab->doc->filePath);
updateWindowTitle();
}
void MainWindow::saveFileAs() {
auto* tab = activeTab();
if (!tab) return;
QString path = QFileDialog::getSaveFileName(this,
"Save Definition", {}, "ReclassX (*.rcx);;JSON (*.json)");
if (path.isEmpty()) return;
tab->doc->save(path);
updateWindowTitle();
}
void MainWindow::loadBinary() {
auto* tab = activeTab();
if (!tab) return;
QString path = QFileDialog::getOpenFileName(this,
"Load Binary Data", {}, "All Files (*)");
if (path.isEmpty()) return;
tab->doc->loadData(path);
}
void MainWindow::addNode() {
auto* ctrl = activeController();
if (!ctrl) return;
uint64_t parentId = 0;
auto* primary = ctrl->primaryEditor();
if (primary && primary->isEditing()) return;
if (primary) {
int ni = primary->currentNodeIndex();
if (ni >= 0) {
auto& node = ctrl->document()->tree.nodes[ni];
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
parentId = node.id;
else
parentId = node.parentId;
}
}
ctrl->insertNode(parentId, -1, NodeKind::Hex64, "newField");
}
void MainWindow::removeNode() {
auto* ctrl = activeController();
if (!ctrl) return;
auto* primary = ctrl->primaryEditor();
if (!primary || primary->isEditing()) return;
QSet<int> indices = primary->selectedNodeIndices();
if (indices.size() > 1) {
ctrl->batchRemoveNodes(indices.values());
} else if (indices.size() == 1) {
ctrl->removeNode(*indices.begin());
}
}
void MainWindow::changeNodeType() {
auto* ctrl = activeController();
if (!ctrl) return;
auto* primary = ctrl->primaryEditor();
if (!primary) return;
primary->beginInlineEdit(EditTarget::Type);
}
void MainWindow::renameNodeAction() {
auto* ctrl = activeController();
if (!ctrl) return;
auto* primary = ctrl->primaryEditor();
if (!primary) return;
primary->beginInlineEdit(EditTarget::Name);
}
void MainWindow::splitView() {
auto* tab = activeTab();
if (!tab) return;
tab->ctrl->addSplitEditor(tab->splitter);
}
void MainWindow::unsplitView() {
auto* tab = activeTab();
if (!tab) return;
auto editors = tab->ctrl->editors();
if (editors.size() > 1)
tab->ctrl->removeSplitEditor(editors.last());
}
void MainWindow::undo() {
auto* tab = activeTab();
if (tab) tab->doc->undoStack.undo();
}
void MainWindow::redo() {
auto* tab = activeTab();
if (tab) tab->doc->undoStack.redo();
}
void MainWindow::about() {
QMessageBox::about(this, "About ReclassX",
"ReclassX - Structured Binary Editor\n"
"Built with Qt 6 + QScintilla\n\n"
"Margin-driven UI with offset display,\n"
"fold markers, and status flags.");
}
RcxController* MainWindow::activeController() const {
auto* sub = m_mdiArea->activeSubWindow();
if (sub && m_tabs.contains(sub))
return m_tabs[sub].ctrl;
return nullptr;
}
MainWindow::TabState* MainWindow::activeTab() {
auto* sub = m_mdiArea->activeSubWindow();
if (sub && m_tabs.contains(sub))
return &m_tabs[sub];
return nullptr;
}
void MainWindow::updateWindowTitle() {
auto* sub = m_mdiArea->activeSubWindow();
if (sub && m_tabs.contains(sub)) {
auto& tab = m_tabs[sub];
QString name = tab.doc->filePath.isEmpty() ? "Untitled"
: QFileInfo(tab.doc->filePath).fileName();
if (tab.doc->modified) name += " *";
setWindowTitle(name + " - ReclassX");
} else {
setWindowTitle("ReclassX");
}
}
} // namespace rcx
// ── Entry point ──
int main(int argc, char* argv[]) {
#ifdef _WIN32
SetUnhandledExceptionFilter(crashHandler);
#endif
QApplication app(argc, argv);
app.setApplicationName("ReclassX");
app.setOrganizationName("ReclassX");
app.setStyle("Fusion"); // Fusion style respects dark palette well
// Global dark palette
QPalette darkPalette;
darkPalette.setColor(QPalette::Window, QColor("#1e1e1e"));
darkPalette.setColor(QPalette::WindowText, QColor("#d4d4d4"));
darkPalette.setColor(QPalette::Base, QColor("#252526"));
darkPalette.setColor(QPalette::AlternateBase, QColor("#2a2d2e"));
darkPalette.setColor(QPalette::Text, QColor("#d4d4d4"));
darkPalette.setColor(QPalette::Button, QColor("#333333"));
darkPalette.setColor(QPalette::ButtonText, QColor("#d4d4d4"));
darkPalette.setColor(QPalette::Highlight, QColor("#264f78"));
darkPalette.setColor(QPalette::HighlightedText, QColor("#ffffff"));
darkPalette.setColor(QPalette::ToolTipBase, QColor("#252526"));
darkPalette.setColor(QPalette::ToolTipText, QColor("#d4d4d4"));
darkPalette.setColor(QPalette::Mid, QColor("#3c3c3c"));
darkPalette.setColor(QPalette::Dark, QColor("#1e1e1e"));
darkPalette.setColor(QPalette::Light, QColor("#505050"));
app.setPalette(darkPalette);
rcx::MainWindow window;
bool screenshotMode = app.arguments().contains("--screenshot");
if (screenshotMode)
window.setAttribute(Qt::WA_DontShowOnScreen);
window.show();
// Always auto-open PE header demo on startup
QMetaObject::invokeMethod(&window, "newFile");
if (screenshotMode) {
QString out = "screenshot.png";
int idx = app.arguments().indexOf("--screenshot");
if (idx + 1 < app.arguments().size())
out = app.arguments().at(idx + 1);
QTimer::singleShot(1000, [&window, &app, out]() {
QDir().mkpath(QFileInfo(out).absolutePath());
window.grab().save(out);
app.quit();
});
}
return app.exec();
}
#include "main.moc"