mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Initial commit: ReclassX structured binary editor
This commit is contained in:
323
src/compose.cpp
Normal file
323
src/compose.cpp
Normal 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
565
src/controller.cpp
Normal 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
99
src/controller.h
Normal 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
521
src/core.h
Normal 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
982
src/editor.cpp
Normal 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
109
src/editor.h
Normal 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
364
src/format.cpp
Normal 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
6
src/icons.qrc
Normal 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
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
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
BIN
src/icons/close.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 169 B |
636
src/main.cpp
Normal file
636
src/main.cpp
Normal 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"
|
||||
Reference in New Issue
Block a user