mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
- Tree line connectors (Unicode box-drawing ├─ └─ │) at arbitrary depth - Fix editor overwriting tree chars at depth 2+ (applyMarginText Pass 2) - Scanner: unknown value scan, comparison rescan modes (Changed/Unchanged/Increased/Decreased) - New Tailwind theme (tw.json), WCAG contrast fixes for warm/mid themes - Tooltip system (rcxtooltip.h) - Comprehensive README rewrite with full feature inventory - New tests for compose tree lines, scanner, tooltips Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3414 lines
137 KiB
C++
3414 lines
137 KiB
C++
#include "controller.h"
|
|
#include "addressparser.h"
|
|
#include "typeselectorpopup.h"
|
|
#include "providerregistry.h"
|
|
#include "themes/thememanager.h"
|
|
#include <Qsci/qsciscintilla.h>
|
|
#include <QSplitter>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QMenu>
|
|
#include <QInputDialog>
|
|
#include <QClipboard>
|
|
#include <QApplication>
|
|
#include <QFileDialog>
|
|
#include <QMessageBox>
|
|
#include <QSettings>
|
|
#include <QtConcurrent/QtConcurrentRun>
|
|
#include <limits>
|
|
|
|
namespace rcx {
|
|
|
|
static thread_local const RcxDocument* s_composeDoc = nullptr;
|
|
|
|
static QString docTypeNameProvider(NodeKind k) {
|
|
if (s_composeDoc) return s_composeDoc->resolveTypeName(k);
|
|
auto* m = kindMeta(k);
|
|
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
|
}
|
|
|
|
static QString elide(QString s, int max) {
|
|
if (max <= 0) return {};
|
|
if (s.size() <= max) return s;
|
|
if (max == 1) return QStringLiteral("\u2026");
|
|
return s.left(max - 1) + QChar(0x2026);
|
|
}
|
|
|
|
static QString elideLeft(const QString& s, int max) {
|
|
if (s.size() <= max) return s;
|
|
if (max <= 1) return QStringLiteral("\u2026").left(max);
|
|
return QStringLiteral("\u2026") + s.right(max - 1);
|
|
}
|
|
|
|
static QString crumbFor(const rcx::NodeTree& t, uint64_t nodeId) {
|
|
QStringList parts;
|
|
QSet<uint64_t> seen;
|
|
uint64_t cur = nodeId;
|
|
while (cur != 0 && !seen.contains(cur)) {
|
|
seen.insert(cur);
|
|
int idx = t.indexOfId(cur);
|
|
if (idx < 0) break;
|
|
const auto& n = t.nodes[idx];
|
|
parts << (n.name.isEmpty() ? QStringLiteral("<unnamed>") : n.name);
|
|
cur = n.parentId;
|
|
}
|
|
std::reverse(parts.begin(), parts.end());
|
|
if (parts.size() > 4)
|
|
parts = QStringList{parts.front(), QStringLiteral("\u2026"), parts[parts.size() - 2], parts.back()};
|
|
return parts.join(QStringLiteral(" \u00B7 "));
|
|
}
|
|
|
|
// ── RcxDocument ──
|
|
|
|
RcxDocument::RcxDocument(QObject* parent)
|
|
: QObject(parent)
|
|
, provider(std::make_shared<NullProvider>())
|
|
{
|
|
connect(&undoStack, &QUndoStack::cleanChanged, this, [this](bool clean) {
|
|
modified = !clean;
|
|
});
|
|
}
|
|
|
|
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
|
|
bool treeLines) const {
|
|
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines);
|
|
}
|
|
|
|
bool RcxDocument::save(const QString& path) {
|
|
QJsonObject json = tree.toJson();
|
|
|
|
// Save type aliases
|
|
if (!typeAliases.isEmpty()) {
|
|
QJsonObject aliasObj;
|
|
for (auto it = typeAliases.begin(); it != typeAliases.end(); ++it)
|
|
aliasObj[kindToString(it.key())] = it.value();
|
|
json["typeAliases"] = aliasObj;
|
|
}
|
|
|
|
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());
|
|
QJsonObject root = jdoc.object();
|
|
tree = NodeTree::fromJson(root);
|
|
|
|
// Load type aliases
|
|
typeAliases.clear();
|
|
QJsonObject aliasObj = root["typeAliases"].toObject();
|
|
for (auto it = aliasObj.begin(); it != aliasObj.end(); ++it) {
|
|
NodeKind k = kindFromString(it.key());
|
|
QString v = it.value().toString();
|
|
if (!v.isEmpty())
|
|
typeAliases[k] = v;
|
|
}
|
|
|
|
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_shared<BufferProvider>(
|
|
file.readAll(), QFileInfo(binaryPath).fileName());
|
|
dataPath = binaryPath;
|
|
tree.baseAddress = 0;
|
|
emit documentChanged();
|
|
}
|
|
|
|
void RcxDocument::loadData(const QByteArray& data) {
|
|
undoStack.clear();
|
|
provider = std::make_shared<BufferProvider>(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)
|
|
{
|
|
fmt::setTypeNameProvider(docTypeNameProvider);
|
|
connect(m_doc, &RcxDocument::documentChanged, this, &RcxController::refresh);
|
|
setupAutoRefresh();
|
|
}
|
|
|
|
RcxController::~RcxController() {
|
|
if (m_refreshWatcher) {
|
|
m_refreshWatcher->cancel();
|
|
m_refreshWatcher->waitForFinished();
|
|
}
|
|
|
|
m_snapshotProv.reset();
|
|
}
|
|
|
|
void RcxController::resetProvider() {
|
|
m_snapshotProv.reset();
|
|
}
|
|
|
|
RcxEditor* RcxController::primaryEditor() const {
|
|
return m_editors.isEmpty() ? nullptr : m_editors.first();
|
|
}
|
|
|
|
RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
|
|
auto* editor = new RcxEditor(parent);
|
|
m_editors.append(editor);
|
|
connectEditor(editor);
|
|
|
|
if (!m_lastResult.text.isEmpty()) {
|
|
editor->applyDocument(m_lastResult);
|
|
}
|
|
updateCommandRow();
|
|
|
|
// Eagerly pre-warm the type popup so first click isn't slow (~350ms cold start).
|
|
if (!m_cachedPopup) {
|
|
QTimer::singleShot(0, this, [this, editor]() {
|
|
if (!m_cachedPopup && !m_editors.isEmpty())
|
|
ensurePopup(editor);
|
|
});
|
|
}
|
|
return editor;
|
|
}
|
|
|
|
void RcxController::removeSplitEditor(RcxEditor* editor) {
|
|
m_editors.removeOne(editor);
|
|
// Caller (MainWindow) owns the parent QTabWidget and handles widget destruction.
|
|
}
|
|
|
|
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::keywordConvertRequested,
|
|
this, &RcxController::convertRootKeyword);
|
|
connect(editor, &RcxEditor::nodeClicked,
|
|
this, [this, editor](int line, uint64_t nodeId, Qt::KeyboardModifiers mods) {
|
|
handleNodeClick(editor, line, nodeId, mods);
|
|
});
|
|
|
|
// Type selector popup (command row chevron)
|
|
connect(editor, &RcxEditor::typeSelectorRequested,
|
|
this, [this, editor]() {
|
|
showTypePopup(editor, TypePopupMode::Root, -1, QPoint());
|
|
});
|
|
|
|
// Type picker popup (array element type / pointer target)
|
|
connect(editor, &RcxEditor::typePickerRequested,
|
|
this, [this, editor](EditTarget target, int nodeIdx, QPoint globalPos) {
|
|
TypePopupMode mode = TypePopupMode::FieldType;
|
|
if (target == EditTarget::ArrayElementType)
|
|
mode = TypePopupMode::ArrayElement;
|
|
else if (target == EditTarget::PointerTarget) {
|
|
// Primitive pointers (ptrDepth>0) should open FieldType with
|
|
// the base type selected and *//** preselected — not PointerTarget.
|
|
bool isPrimPtr = false;
|
|
if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) {
|
|
const auto& n = m_doc->tree.nodes[nodeIdx];
|
|
isPrimPtr = n.ptrDepth > 0 && n.refId == 0;
|
|
}
|
|
mode = isPrimPtr ? TypePopupMode::FieldType
|
|
: TypePopupMode::PointerTarget;
|
|
}
|
|
showTypePopup(editor, mode, nodeIdx, globalPos);
|
|
});
|
|
|
|
// Insert key shortcut
|
|
connect(editor, &RcxEditor::insertAboveRequested,
|
|
this, [this](int nodeIdx, NodeKind kind) {
|
|
if (nodeIdx >= 0)
|
|
insertNodeAbove(nodeIdx, kind, QStringLiteral("field"));
|
|
else {
|
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
|
insertNode(target, -1, kind, QStringLiteral("field"));
|
|
}
|
|
});
|
|
|
|
// Inline editing signals
|
|
connect(editor, &RcxEditor::inlineEditCommitted,
|
|
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text,
|
|
uint64_t resolvedAddr) {
|
|
// CommandRow BaseAddress/Source/RootClass edit has nodeIdx=-1
|
|
if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source
|
|
&& target != EditTarget::RootClassType && target != EditTarget::RootClassName) { refresh(); return; }
|
|
switch (target) {
|
|
case EditTarget::Name: {
|
|
if (text.isEmpty()) break;
|
|
if (nodeIdx >= m_doc->tree.nodes.size()) break;
|
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
|
// Enum member name edit
|
|
if (node.resolvedClassKeyword() == QStringLiteral("enum")
|
|
&& subLine >= 0 && subLine < node.enumMembers.size()) {
|
|
auto members = node.enumMembers;
|
|
members[subLine].first = text;
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeEnumMembers{node.id, node.enumMembers, members}));
|
|
break;
|
|
}
|
|
// ASCII edit on Hex nodes
|
|
if (isHexPreview(node.kind)) {
|
|
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true, resolvedAddr);
|
|
} else {
|
|
renameNode(nodeIdx, text);
|
|
}
|
|
break;
|
|
}
|
|
case EditTarget::Type: {
|
|
// Check for array type syntax: "type[count]" e.g. "int32_t[10]"
|
|
int bracketPos = text.indexOf('[');
|
|
if (bracketPos > 0 && text.endsWith(']')) {
|
|
QString elemTypeName = text.left(bracketPos).trimmed();
|
|
QString countStr = text.mid(bracketPos + 1, text.size() - bracketPos - 2);
|
|
bool countOk;
|
|
int newCount = countStr.toInt(&countOk);
|
|
if (countOk && newCount > 0) {
|
|
bool typeOk;
|
|
NodeKind elemKind = kindFromTypeName(elemTypeName, &typeOk);
|
|
if (typeOk && nodeIdx < m_doc->tree.nodes.size()) {
|
|
const uint64_t nodeId = m_doc->tree.nodes[nodeIdx].id;
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Change to array"));
|
|
if (m_doc->tree.nodes[nodeIdx].kind != NodeKind::Array)
|
|
changeNodeKind(nodeIdx, NodeKind::Array);
|
|
int idx = m_doc->tree.indexOfId(nodeId);
|
|
if (idx >= 0) {
|
|
auto& n = m_doc->tree.nodes[idx];
|
|
if (n.elementKind != elemKind || n.arrayLen != newCount)
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeArrayMeta{nodeId, n.elementKind, elemKind,
|
|
n.arrayLen, newCount}));
|
|
}
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
if (!m_suppressRefresh) refresh();
|
|
}
|
|
}
|
|
} else {
|
|
// Regular type change
|
|
bool ok;
|
|
NodeKind k = kindFromTypeName(text, &ok);
|
|
if (ok && k != NodeKind::Struct && k != NodeKind::Array) {
|
|
changeNodeKind(nodeIdx, k);
|
|
} else if (nodeIdx < m_doc->tree.nodes.size()) {
|
|
// Check if it's a defined struct type name
|
|
bool isStructType = false;
|
|
for (const auto& n : m_doc->tree.nodes) {
|
|
if (n.kind == NodeKind::Struct && n.structTypeName == text) {
|
|
isStructType = true;
|
|
break;
|
|
}
|
|
}
|
|
if (isStructType) {
|
|
auto& node = m_doc->tree.nodes[nodeIdx];
|
|
if (node.kind != NodeKind::Struct)
|
|
changeNodeKind(nodeIdx, NodeKind::Struct);
|
|
int idx = m_doc->tree.indexOfId(node.id);
|
|
if (idx >= 0) {
|
|
QString oldTypeName = m_doc->tree.nodes[idx].structTypeName;
|
|
if (oldTypeName != text) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeStructTypeName{node.id, oldTypeName, text}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case EditTarget::Value: {
|
|
// Enum member value edit
|
|
if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) {
|
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
|
if (node.resolvedClassKeyword() == QStringLiteral("enum")
|
|
&& subLine >= 0 && subLine < node.enumMembers.size()) {
|
|
bool ok;
|
|
int64_t val = text.toLongLong(&ok);
|
|
if (!ok) val = text.toLongLong(&ok, 16);
|
|
if (ok) {
|
|
auto members = node.enumMembers;
|
|
members[subLine].second = val;
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeEnumMembers{node.id, node.enumMembers, members}));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/false, resolvedAddr);
|
|
break;
|
|
}
|
|
case EditTarget::BaseAddress: {
|
|
QString s = text.trimmed();
|
|
s.remove('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000)
|
|
s.remove('\n');
|
|
s.remove('\r');
|
|
|
|
AddressParserCallbacks cbs;
|
|
if (m_doc->provider) {
|
|
auto* prov = m_doc->provider.get();
|
|
cbs.resolveModule = [prov](const QString& name, bool* ok) -> uint64_t {
|
|
uint64_t base = prov->symbolToAddress(name);
|
|
*ok = (base != 0);
|
|
return base;
|
|
};
|
|
int ptrSz = m_doc->tree.pointerSize;
|
|
cbs.readPointer = [prov, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
|
uint64_t val = 0;
|
|
*ok = prov->read(addr, &val, ptrSz);
|
|
return val;
|
|
};
|
|
}
|
|
auto result = AddressParser::evaluate(s, m_doc->tree.pointerSize, &cbs);
|
|
if (result.ok && result.value != m_doc->tree.baseAddress) {
|
|
uint64_t oldBase = m_doc->tree.baseAddress;
|
|
QString oldFormula = m_doc->tree.baseAddressFormula;
|
|
// Store formula if input uses module/deref syntax, otherwise clear
|
|
QString newFormula = (s.contains('<') || s.contains('[')) ? s : QString();
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeBase{oldBase, result.value, oldFormula, newFormula}));
|
|
}
|
|
break;
|
|
}
|
|
case EditTarget::Source:
|
|
selectSource(text);
|
|
break;
|
|
case EditTarget::ArrayElementType: {
|
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) break;
|
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
|
if (node.kind != NodeKind::Array) break;
|
|
bool ok;
|
|
NodeKind elemKind = kindFromTypeName(text, &ok);
|
|
if (ok && elemKind != node.elementKind) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeArrayMeta{node.id,
|
|
node.elementKind, elemKind,
|
|
node.arrayLen, node.arrayLen}));
|
|
}
|
|
break;
|
|
}
|
|
case EditTarget::ArrayElementCount: {
|
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) break;
|
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
|
if (node.kind != NodeKind::Array) break;
|
|
bool ok;
|
|
int newLen = text.toInt(&ok);
|
|
if (ok && newLen > 0 && newLen <= 100000 && newLen != node.arrayLen) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeArrayMeta{node.id,
|
|
node.elementKind, node.elementKind,
|
|
node.arrayLen, newLen}));
|
|
}
|
|
break;
|
|
}
|
|
case EditTarget::PointerTarget: {
|
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) break;
|
|
Node& node = m_doc->tree.nodes[nodeIdx];
|
|
if (node.kind != NodeKind::Pointer32 && node.kind != NodeKind::Pointer64) break;
|
|
// Find the struct with matching name or structTypeName
|
|
uint64_t newRefId = 0;
|
|
for (const auto& n : m_doc->tree.nodes) {
|
|
if (n.kind == NodeKind::Struct &&
|
|
(n.structTypeName == text || n.name == text)) {
|
|
newRefId = n.id;
|
|
break;
|
|
}
|
|
}
|
|
if (newRefId != node.refId) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangePointerRef{node.id, node.refId, newRefId}));
|
|
}
|
|
break;
|
|
}
|
|
case EditTarget::RootClassType: {
|
|
QString kw = text.toLower().trimmed();
|
|
if (kw != QStringLiteral("struct") && kw != QStringLiteral("class") && kw != QStringLiteral("enum")) break;
|
|
uint64_t targetId = m_viewRootId;
|
|
if (targetId == 0) {
|
|
for (const auto& n : m_doc->tree.nodes) {
|
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
|
targetId = n.id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (targetId != 0) {
|
|
int idx = m_doc->tree.indexOfId(targetId);
|
|
if (idx >= 0) {
|
|
QString oldKw = m_doc->tree.nodes[idx].resolvedClassKeyword();
|
|
if (oldKw != kw) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeClassKeyword{targetId, oldKw, kw}));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case EditTarget::RootClassName: {
|
|
// Rename the viewed root struct's structTypeName
|
|
if (!text.isEmpty()) {
|
|
uint64_t targetId = m_viewRootId;
|
|
if (targetId == 0) {
|
|
for (const auto& n : m_doc->tree.nodes) {
|
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
|
targetId = n.id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (targetId != 0) {
|
|
int idx = m_doc->tree.indexOfId(targetId);
|
|
if (idx >= 0) {
|
|
QString oldName = m_doc->tree.nodes[idx].structTypeName;
|
|
if (oldName != text) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeStructTypeName{targetId, oldName, text}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case EditTarget::StaticExpr: {
|
|
if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) {
|
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
|
if (node.isStatic && text != node.offsetExpr) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeOffsetExpr{node.id, node.offsetExpr, text}));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case EditTarget::ArrayIndex:
|
|
case EditTarget::ArrayCount:
|
|
// Array navigation removed - these cases are unreachable
|
|
break;
|
|
}
|
|
// Always refresh to restore canonical text (handles parse failures, no-ops, etc.)
|
|
refresh();
|
|
});
|
|
connect(editor, &RcxEditor::inlineEditCancelled,
|
|
this, [this]() { refresh(); });
|
|
}
|
|
|
|
void RcxController::setViewRootId(uint64_t id) {
|
|
if (m_viewRootId == id) return;
|
|
m_viewRootId = id;
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::scrollToNodeId(uint64_t nodeId) {
|
|
if (auto* editor = primaryEditor())
|
|
editor->scrollToNodeId(nodeId);
|
|
}
|
|
|
|
void RcxController::setTrackValues(bool on) {
|
|
m_trackValues = on;
|
|
if (!on) {
|
|
m_valueHistory.clear();
|
|
for (auto& lm : m_lastResult.meta)
|
|
lm.heatLevel = 0;
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
void RcxController::resetChangeTracking() {
|
|
m_changedOffsets.clear();
|
|
m_valueHistory.clear();
|
|
m_prevPages.clear();
|
|
m_valueTrackCooldown = 5; // suppress tracking for ~1s
|
|
for (auto& lm : m_lastResult.meta)
|
|
lm.heatLevel = 0;
|
|
}
|
|
|
|
void RcxController::refresh() {
|
|
// Bracket compose with thread-local doc pointer for type name resolution
|
|
s_composeDoc = m_doc;
|
|
|
|
// Compose against snapshot provider if active, otherwise real provider
|
|
if (m_snapshotProv)
|
|
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines);
|
|
else
|
|
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines);
|
|
|
|
s_composeDoc = nullptr;
|
|
|
|
// Mark lines whose node data changed since last refresh
|
|
if (!m_changedOffsets.isEmpty()) {
|
|
for (auto& lm : m_lastResult.meta) {
|
|
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
|
|
int64_t offset = m_doc->tree.computeOffset(lm.nodeIdx);
|
|
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
|
|
|
|
if (isHexPreview(node.kind)) {
|
|
// Per-byte tracking for hex preview nodes
|
|
int lineOff = 0;
|
|
int byteCount = lm.lineByteCount;
|
|
for (int b = 0; b < byteCount; b++) {
|
|
if (m_changedOffsets.contains(offset + lineOff + b)) {
|
|
lm.changedByteIndices.append(b);
|
|
lm.dataChanged = true;
|
|
}
|
|
}
|
|
} else {
|
|
// Use structSpan for containers (byteSize returns 0 for Array-of-Struct)
|
|
int sz = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
|
|
? m_doc->tree.structSpan(node.id) : node.byteSize();
|
|
for (int64_t b = offset; b < offset + sz; b++) {
|
|
if (m_changedOffsets.contains(b)) {
|
|
lm.dataChanged = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update value history and compute heat levels
|
|
// Only run when a live provider is attached (not for static file/buffer sources)
|
|
{
|
|
const Provider* prov = nullptr;
|
|
if (m_snapshotProv && m_snapshotProv->isLive())
|
|
prov = m_snapshotProv.get();
|
|
else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive())
|
|
prov = m_doc->provider.get();
|
|
|
|
if (m_valueTrackCooldown > 0) --m_valueTrackCooldown;
|
|
if (m_trackValues && prov && m_valueTrackCooldown <= 0) {
|
|
for (auto& lm : m_lastResult.meta) {
|
|
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
|
|
if (isSyntheticLine(lm) || lm.isContinuation) continue;
|
|
if (lm.lineKind != LineKind::Field) continue;
|
|
|
|
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
|
|
// Skip containers — they don't have scalar values
|
|
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue;
|
|
// Skip FuncPtr nodes — vtable entries don't change; tracking them
|
|
// causes false heatmap and popup fighting with the disasm popup.
|
|
if (isFuncPtr(node.kind)) continue;
|
|
|
|
// Use the absolute address from compose (correct for pointer-expanded nodes)
|
|
uint64_t addr = lm.offsetAddr;
|
|
int sz = node.byteSize();
|
|
if (sz <= 0 || !prov->isReadable(addr, sz)) continue;
|
|
|
|
QString val = fmt::readValue(node, *prov, addr, lm.subLine);
|
|
if (!val.isEmpty()) {
|
|
m_valueHistory[lm.nodeId].record(val);
|
|
lm.heatLevel = m_valueHistory[lm.nodeId].heatLevel();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prune stale selections (nodes removed by undo/redo/delete)
|
|
QSet<uint64_t> valid;
|
|
for (uint64_t id : m_selIds) {
|
|
uint64_t nodeId = id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
|
| kMemberBit | kMemberSubMask);
|
|
if (m_doc->tree.indexOfId(nodeId) >= 0)
|
|
valid.insert(id); // Keep original ID (with footer/array/member bits if present)
|
|
}
|
|
m_selIds = valid;
|
|
|
|
// Collect unique struct type names for the type picker
|
|
QStringList customTypes;
|
|
QSet<QString> seen;
|
|
for (const auto& node : m_doc->tree.nodes) {
|
|
if (node.kind == NodeKind::Struct && !node.structTypeName.isEmpty()) {
|
|
if (!seen.contains(node.structTypeName)) {
|
|
seen.insert(node.structTypeName);
|
|
customTypes << node.structTypeName;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resolve providers for disasm popup:
|
|
// - snapProv: snapshot or real — for reading pointer values within the tree
|
|
// - realProv: always the real process provider — for reading code at arbitrary addresses
|
|
const Provider* snapProv = m_snapshotProv
|
|
? static_cast<const Provider*>(m_snapshotProv.get())
|
|
: (m_doc->provider ? m_doc->provider.get() : nullptr);
|
|
const Provider* realProv = m_doc->provider ? m_doc->provider.get() : nullptr;
|
|
|
|
for (auto* editor : m_editors) {
|
|
editor->setCustomTypeNames(customTypes);
|
|
editor->setValueHistoryRef(&m_valueHistory);
|
|
editor->setProviderRef(snapProv, realProv, &m_doc->tree);
|
|
ViewState vs = editor->saveViewState();
|
|
editor->applyDocument(m_lastResult);
|
|
editor->restoreViewState(vs);
|
|
}
|
|
// Text-modifying passes first (command row replaces line 0 text),
|
|
// then overlays last so hover indicators survive the refresh.
|
|
pushSavedSourcesToEditors();
|
|
updateCommandRow();
|
|
applySelectionOverlays();
|
|
}
|
|
|
|
void RcxController::convertRootKeyword(const QString& newKeyword) {
|
|
uint64_t targetId = m_viewRootId;
|
|
if (targetId == 0) {
|
|
for (const auto& n : m_doc->tree.nodes) {
|
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
|
targetId = n.id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (targetId == 0) return;
|
|
int idx = m_doc->tree.indexOfId(targetId);
|
|
if (idx < 0) return;
|
|
QString oldKw = m_doc->tree.nodes[idx].resolvedClassKeyword();
|
|
if (oldKw == newKeyword) return;
|
|
// Only allow class↔struct conversion
|
|
if (oldKw == QStringLiteral("enum") || newKeyword == QStringLiteral("enum")) return;
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeClassKeyword{targetId, oldKw, newKeyword}));
|
|
}
|
|
|
|
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();
|
|
|
|
if (newSize > 0 && newSize < oldSize) {
|
|
// Shrinking: insert hex padding to fill gap (no offset shift)
|
|
int gap = oldSize - newSize;
|
|
uint64_t parentId = node.parentId;
|
|
int baseOffset = node.offset + newSize;
|
|
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Change type"));
|
|
|
|
// Push type change with no offset adjustments
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeKind{node.id, node.kind, newKind, {}}));
|
|
|
|
// Hex nodes don't display names (ASCII preview instead), so the stored
|
|
// name may be empty or stale. Give it a sensible default.
|
|
if (isHexNode(node.kind) && !isHexNode(newKind)) {
|
|
QString autoName = QStringLiteral("field_%1")
|
|
.arg(node.offset, 4, 16, QChar('0'));
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Rename{node.id, node.name, autoName}));
|
|
}
|
|
|
|
// Insert hex nodes to fill the gap (largest first for alignment)
|
|
int padOffset = baseOffset;
|
|
while (gap > 0) {
|
|
NodeKind padKind;
|
|
int padSize;
|
|
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
|
|
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
|
|
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
|
|
else { padKind = NodeKind::Hex8; padSize = 1; }
|
|
|
|
insertNode(parentId, padOffset, padKind,
|
|
QString("pad_%1").arg(padOffset, 2, 16, QChar('0')));
|
|
padOffset += padSize;
|
|
gap -= padSize;
|
|
}
|
|
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
if (!m_suppressRefresh) refresh();
|
|
} else {
|
|
// Same size or larger: adjust sibling offsets as before
|
|
int delta = newSize - oldSize;
|
|
QVector<cmd::OffsetAdj> adjs;
|
|
if (delta != 0 && oldSize > 0 && newSize > 0) {
|
|
int oldEnd = node.offset + oldSize;
|
|
auto siblings = m_doc->tree.childrenOf(node.parentId);
|
|
for (int si : siblings) {
|
|
if (si == nodeIdx) continue;
|
|
auto& sib = m_doc->tree.nodes[si];
|
|
if (sib.offset >= oldEnd)
|
|
adjs.append({sib.id, sib.offset, sib.offset + delta});
|
|
}
|
|
}
|
|
bool needsRename = isHexNode(node.kind) && !isHexNode(newKind);
|
|
if (needsRename) {
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Change type"));
|
|
}
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeKind{node.id, node.kind, newKind, adjs}));
|
|
if (needsRename) {
|
|
QString autoName = QStringLiteral("field_%1")
|
|
.arg(node.offset, 4, 16, QChar('0'));
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Rename{node.id, node.name, autoName}));
|
|
m_doc->undoStack.endMacro();
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Reserve unique ID atomically before pushing command
|
|
n.id = m_doc->tree.reserveId();
|
|
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
|
}
|
|
|
|
void RcxController::insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name) {
|
|
if (beforeIdx < 0 || beforeIdx >= m_doc->tree.nodes.size()) return;
|
|
const Node& before = m_doc->tree.nodes[beforeIdx];
|
|
|
|
Node n;
|
|
n.kind = kind;
|
|
n.name = name;
|
|
n.parentId = before.parentId;
|
|
n.offset = before.offset;
|
|
n.id = m_doc->tree.reserveId();
|
|
|
|
int insertSize = sizeForKind(kind);
|
|
|
|
// Shift siblings at or after the insertion offset down
|
|
QVector<cmd::OffsetAdj> adjs;
|
|
auto siblings = m_doc->tree.childrenOf(before.parentId);
|
|
for (int si : siblings) {
|
|
auto& sib = m_doc->tree.nodes[si];
|
|
if (sib.offset >= before.offset)
|
|
adjs.append({sib.id, sib.offset, sib.offset + insertSize});
|
|
}
|
|
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs}));
|
|
}
|
|
|
|
void RcxController::removeNode(int nodeIdx) {
|
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
|
uint64_t nodeId = node.id;
|
|
uint64_t parentId = node.parentId;
|
|
|
|
// Compute size of deleted node/subtree
|
|
int deletedSize = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
|
|
? m_doc->tree.structSpan(node.id) : node.byteSize();
|
|
int deletedEnd = node.offset + deletedSize;
|
|
|
|
// Find siblings after this node and compute offset adjustments
|
|
QVector<cmd::OffsetAdj> adjs;
|
|
if (parentId != 0) { // only adjust if not root-level
|
|
auto siblings = m_doc->tree.childrenOf(parentId);
|
|
for (int si : siblings) {
|
|
if (si == nodeIdx) continue;
|
|
auto& sib = m_doc->tree.nodes[si];
|
|
if (sib.offset >= deletedEnd) {
|
|
adjs.append({sib.id, sib.offset, sib.offset - deletedSize});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect subtree
|
|
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, adjs}));
|
|
}
|
|
|
|
void RcxController::deleteRootStruct(uint64_t structId) {
|
|
int ni = m_doc->tree.indexOfId(structId);
|
|
if (ni < 0) return;
|
|
const Node& node = m_doc->tree.nodes[ni];
|
|
if (node.parentId != 0 || node.kind != NodeKind::Struct) return;
|
|
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Delete root struct"));
|
|
|
|
// Clear all refId references pointing to this struct
|
|
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
|
auto& n = m_doc->tree.nodes[i];
|
|
if (n.refId == structId) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangePointerRef{n.id, n.refId, (uint64_t)0}));
|
|
}
|
|
}
|
|
|
|
// Remove the struct + subtree (re-lookup since commands may shift indices)
|
|
ni = m_doc->tree.indexOfId(structId);
|
|
if (ni >= 0)
|
|
removeNode(ni);
|
|
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
|
|
// Switch view if we just deleted the viewed root
|
|
if (m_viewRootId == structId) {
|
|
uint64_t nextRoot = 0;
|
|
for (const auto& n : m_doc->tree.nodes) {
|
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
|
nextRoot = n.id;
|
|
break;
|
|
}
|
|
}
|
|
setViewRootId(nextRoot);
|
|
}
|
|
|
|
if (!m_suppressRefresh) refresh();
|
|
}
|
|
|
|
void RcxController::groupIntoUnion(const QSet<uint64_t>& nodeIds) {
|
|
if (nodeIds.size() < 2) return;
|
|
|
|
// Collect nodes and verify they share the same parent
|
|
QVector<int> indices;
|
|
uint64_t parentId = 0;
|
|
bool first = true;
|
|
for (uint64_t id : nodeIds) {
|
|
int idx = m_doc->tree.indexOfId(id);
|
|
if (idx < 0) return;
|
|
if (first) { parentId = m_doc->tree.nodes[idx].parentId; first = false; }
|
|
else if (m_doc->tree.nodes[idx].parentId != parentId) return;
|
|
indices.append(idx);
|
|
}
|
|
|
|
// Sort by offset to find the union's insertion point
|
|
std::sort(indices.begin(), indices.end(), [&](int a, int b) {
|
|
return m_doc->tree.nodes[a].offset < m_doc->tree.nodes[b].offset;
|
|
});
|
|
int unionOffset = m_doc->tree.nodes[indices.first()].offset;
|
|
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Group into union"));
|
|
|
|
// Save copies of nodes before removal (subtrees included)
|
|
struct SavedNode { Node node; QVector<Node> subtree; };
|
|
QVector<SavedNode> saved;
|
|
for (int idx : indices) {
|
|
SavedNode sn;
|
|
sn.node = m_doc->tree.nodes[idx];
|
|
auto sub = m_doc->tree.subtreeIndices(sn.node.id);
|
|
for (int si : sub)
|
|
if (si != idx) sn.subtree.append(m_doc->tree.nodes[si]);
|
|
saved.append(sn);
|
|
}
|
|
|
|
// Remove selected nodes (in reverse order to keep indices valid)
|
|
for (int i = indices.size() - 1; i >= 0; i--) {
|
|
int idx = m_doc->tree.indexOfId(saved[i].node.id);
|
|
if (idx >= 0) {
|
|
QVector<Node> subtree;
|
|
for (int si : m_doc->tree.subtreeIndices(saved[i].node.id))
|
|
subtree.append(m_doc->tree.nodes[si]);
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Remove{saved[i].node.id, subtree, {}}));
|
|
}
|
|
}
|
|
|
|
// Insert union node
|
|
Node unionNode;
|
|
unionNode.kind = NodeKind::Struct;
|
|
unionNode.classKeyword = QStringLiteral("union");
|
|
unionNode.parentId = parentId;
|
|
unionNode.offset = unionOffset;
|
|
unionNode.id = m_doc->tree.reserveId();
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{unionNode}));
|
|
uint64_t unionId = unionNode.id;
|
|
|
|
// Re-insert nodes as children of the union, all at offset 0
|
|
for (const auto& sn : saved) {
|
|
Node copy = sn.node;
|
|
copy.parentId = unionId;
|
|
copy.offset = 0;
|
|
copy.id = m_doc->tree.reserveId();
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{copy}));
|
|
|
|
// Re-insert subtree with updated parentId for direct children
|
|
uint64_t oldId = sn.node.id;
|
|
uint64_t newId = copy.id;
|
|
for (const auto& child : sn.subtree) {
|
|
Node cc = child;
|
|
if (cc.parentId == oldId) cc.parentId = newId;
|
|
cc.id = m_doc->tree.reserveId();
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{cc}));
|
|
}
|
|
}
|
|
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
if (!m_suppressRefresh) refresh();
|
|
}
|
|
|
|
void RcxController::dissolveUnion(uint64_t unionId) {
|
|
int ui = m_doc->tree.indexOfId(unionId);
|
|
if (ui < 0) return;
|
|
const Node& unionNode = m_doc->tree.nodes[ui];
|
|
if (unionNode.kind != NodeKind::Struct
|
|
|| unionNode.resolvedClassKeyword() != QStringLiteral("union")) return;
|
|
|
|
uint64_t parentId = unionNode.parentId;
|
|
int unionOffset = unionNode.offset;
|
|
|
|
// Collect union children
|
|
auto children = m_doc->tree.childrenOf(unionId);
|
|
struct SavedNode { Node node; QVector<Node> subtree; };
|
|
QVector<SavedNode> saved;
|
|
for (int ci : children) {
|
|
SavedNode sn;
|
|
sn.node = m_doc->tree.nodes[ci];
|
|
auto sub = m_doc->tree.subtreeIndices(sn.node.id);
|
|
for (int si : sub)
|
|
if (si != ci) sn.subtree.append(m_doc->tree.nodes[si]);
|
|
saved.append(sn);
|
|
}
|
|
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Dissolve union"));
|
|
|
|
// Remove the union (and all its children)
|
|
{
|
|
QVector<Node> subtree;
|
|
for (int si : m_doc->tree.subtreeIndices(unionId))
|
|
subtree.append(m_doc->tree.nodes[si]);
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Remove{unionId, subtree, {}}));
|
|
}
|
|
|
|
// Re-insert children under the union's parent, at the union's offset
|
|
for (const auto& sn : saved) {
|
|
Node copy = sn.node;
|
|
copy.parentId = parentId;
|
|
copy.offset = unionOffset + sn.node.offset;
|
|
copy.id = m_doc->tree.reserveId();
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{copy}));
|
|
|
|
uint64_t oldId = sn.node.id;
|
|
uint64_t newId = copy.id;
|
|
for (const auto& child : sn.subtree) {
|
|
Node cc = child;
|
|
if (cc.parentId == oldId) cc.parentId = newId;
|
|
cc.id = m_doc->tree.reserveId();
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{cc}));
|
|
}
|
|
}
|
|
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
if (!m_suppressRefresh) refresh();
|
|
}
|
|
|
|
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::materializeRefChildren(int nodeIdx) {
|
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
|
auto& tree = m_doc->tree;
|
|
|
|
// Snapshot values before any mutation invalidates references
|
|
const uint64_t parentId = tree.nodes[nodeIdx].id;
|
|
const uint64_t refId = tree.nodes[nodeIdx].refId;
|
|
const NodeKind parentKind = tree.nodes[nodeIdx].kind;
|
|
const QString parentName = tree.nodes[nodeIdx].name;
|
|
|
|
if (refId == 0) return;
|
|
if (!tree.childrenOf(parentId).isEmpty()) return; // already materialized
|
|
|
|
// Collect children to clone (copy by value to avoid reference invalidation)
|
|
QVector<int> refChildren = tree.childrenOf(refId);
|
|
if (refChildren.isEmpty()) return;
|
|
|
|
QVector<Node> clones;
|
|
clones.reserve(refChildren.size());
|
|
for (int ci : refChildren) {
|
|
Node copy = tree.nodes[ci]; // copy by value before any mutation
|
|
copy.id = tree.reserveId();
|
|
copy.parentId = parentId;
|
|
copy.collapsed = true;
|
|
clones.append(copy);
|
|
}
|
|
|
|
// Wrap all mutations in an undo macro
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Materialize ref children"));
|
|
|
|
for (const Node& clone : clones) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Insert{clone, {}}));
|
|
}
|
|
|
|
// Auto-expand the self-referential child (the one that was the cycle)
|
|
// so the user gets expand in a single click
|
|
for (const Node& clone : clones) {
|
|
if (clone.kind == parentKind && clone.name == parentName && clone.refId == refId) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Collapse{clone.id, true, false}));
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
if (!m_suppressRefresh) refresh();
|
|
}
|
|
|
|
void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|
auto& tree = m_doc->tree;
|
|
|
|
// Clear value history for nodes whose effective offset changed.
|
|
// When offsets shift (insert/delete/resize), old recorded values came from
|
|
// a different memory address, so keeping them would show false heat.
|
|
// Also invalidates any in-flight async read so that stale snapshot data
|
|
// from before the offset change doesn't re-introduce false heat.
|
|
auto clearHistoryForAdjs = [&](const QVector<cmd::OffsetAdj>& adjs) {
|
|
if (adjs.isEmpty()) return;
|
|
m_refreshGen++; // discard in-flight async read (stale layout)
|
|
for (const auto& adj : adjs) {
|
|
// Clear the adjusted node itself
|
|
m_valueHistory.remove(adj.nodeId);
|
|
// Clear all descendants (their effective address also shifted)
|
|
for (int ci : tree.subtreeIndices(adj.nodeId))
|
|
m_valueHistory.remove(tree.nodes[ci].id);
|
|
}
|
|
};
|
|
|
|
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;
|
|
}
|
|
// The changed node's value format changed; clear its history.
|
|
// If offAdjs is empty (same-size change), still bump gen to
|
|
// discard in-flight reads that would record the old format.
|
|
if (c.offAdjs.isEmpty()) m_refreshGen++;
|
|
m_valueHistory.remove(c.nodeId);
|
|
clearHistoryForAdjs(c.offAdjs);
|
|
} 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) {
|
|
// Revert offset adjustments
|
|
for (const auto& adj : c.offAdjs) {
|
|
int ai = tree.indexOfId(adj.nodeId);
|
|
if (ai >= 0) tree.nodes[ai].offset = adj.oldOffset;
|
|
}
|
|
int idx = tree.indexOfId(c.node.id);
|
|
if (idx >= 0) {
|
|
tree.nodes.remove(idx);
|
|
tree.invalidateIdCache();
|
|
}
|
|
} else {
|
|
tree.addNode(c.node);
|
|
// Apply offset adjustments
|
|
for (const auto& adj : c.offAdjs) {
|
|
int ai = tree.indexOfId(adj.nodeId);
|
|
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
|
}
|
|
}
|
|
clearHistoryForAdjs(c.offAdjs);
|
|
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
|
|
if (isUndo) {
|
|
// Restore nodes first
|
|
for (const Node& n : c.subtree)
|
|
tree.addNode(n);
|
|
// Revert offset adjustments
|
|
for (const auto& adj : c.offAdjs) {
|
|
int ai = tree.indexOfId(adj.nodeId);
|
|
if (ai >= 0) tree.nodes[ai].offset = adj.oldOffset;
|
|
}
|
|
} else {
|
|
// Apply offset adjustments first (before removing changes indices)
|
|
for (const auto& adj : c.offAdjs) {
|
|
int ai = tree.indexOfId(adj.nodeId);
|
|
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
|
}
|
|
// Remove nodes and their value history
|
|
QVector<int> indices = tree.subtreeIndices(c.nodeId);
|
|
std::sort(indices.begin(), indices.end(), std::greater<int>());
|
|
for (int idx : indices) {
|
|
m_valueHistory.remove(tree.nodes[idx].id);
|
|
tree.nodes.remove(idx);
|
|
}
|
|
tree.invalidateIdCache();
|
|
}
|
|
// Siblings shifted — their old values are from wrong addresses
|
|
clearHistoryForAdjs(c.offAdjs);
|
|
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
|
|
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
|
|
tree.baseAddressFormula = isUndo ? c.oldFormula : c.newFormula;
|
|
resetSnapshot();
|
|
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
|
|
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
|
// Write through snapshot (patches pages only on success) or provider directly.
|
|
// If write fails, the snapshot is NOT patched, so the next compose shows the
|
|
// real unchanged value — no optimistic visual leak.
|
|
bool ok = m_snapshotProv
|
|
? m_snapshotProv->write(c.addr, bytes.constData(), bytes.size())
|
|
: m_doc->provider->writeBytes(c.addr, bytes);
|
|
if (!ok)
|
|
qWarning() << "WriteBytes failed at address" << QString::number(c.addr, 16);
|
|
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
|
|
int idx = tree.indexOfId(c.nodeId);
|
|
if (idx >= 0) {
|
|
tree.nodes[idx].elementKind = isUndo ? c.oldElementKind : c.newElementKind;
|
|
tree.nodes[idx].arrayLen = isUndo ? c.oldArrayLen : c.newArrayLen;
|
|
if (tree.nodes[idx].viewIndex >= tree.nodes[idx].arrayLen)
|
|
tree.nodes[idx].viewIndex = qMax(0, tree.nodes[idx].arrayLen - 1);
|
|
}
|
|
} else if constexpr (std::is_same_v<T, cmd::ChangePointerRef>) {
|
|
int idx = tree.indexOfId(c.nodeId);
|
|
if (idx >= 0) {
|
|
tree.nodes[idx].refId = isUndo ? c.oldRefId : c.newRefId;
|
|
if (tree.nodes[idx].refId != 0)
|
|
tree.nodes[idx].collapsed = true;
|
|
}
|
|
} else if constexpr (std::is_same_v<T, cmd::ChangeStructTypeName>) {
|
|
int idx = tree.indexOfId(c.nodeId);
|
|
if (idx >= 0)
|
|
tree.nodes[idx].structTypeName = isUndo ? c.oldName : c.newName;
|
|
} else if constexpr (std::is_same_v<T, cmd::ChangeClassKeyword>) {
|
|
int idx = tree.indexOfId(c.nodeId);
|
|
if (idx >= 0)
|
|
tree.nodes[idx].classKeyword = isUndo ? c.oldKeyword : c.newKeyword;
|
|
} else if constexpr (std::is_same_v<T, cmd::ChangeOffset>) {
|
|
int idx = tree.indexOfId(c.nodeId);
|
|
if (idx >= 0)
|
|
tree.nodes[idx].offset = isUndo ? c.oldOffset : c.newOffset;
|
|
// Node and its descendants read from a different address now
|
|
m_refreshGen++; // discard in-flight async read (stale layout)
|
|
m_valueHistory.remove(c.nodeId);
|
|
for (int ci : tree.subtreeIndices(c.nodeId))
|
|
m_valueHistory.remove(tree.nodes[ci].id);
|
|
} else if constexpr (std::is_same_v<T, cmd::ChangeEnumMembers>) {
|
|
int idx = tree.indexOfId(c.nodeId);
|
|
if (idx >= 0)
|
|
tree.nodes[idx].enumMembers = isUndo ? c.oldMembers : c.newMembers;
|
|
} else if constexpr (std::is_same_v<T, cmd::ChangeOffsetExpr>) {
|
|
int idx = tree.indexOfId(c.nodeId);
|
|
if (idx >= 0)
|
|
tree.nodes[idx].offsetExpr = isUndo ? c.oldExpr : c.newExpr;
|
|
} else if constexpr (std::is_same_v<T, cmd::ToggleStatic>) {
|
|
int idx = tree.indexOfId(c.nodeId);
|
|
if (idx >= 0)
|
|
tree.nodes[idx].isStatic = isUndo ? c.oldVal : c.newVal;
|
|
}
|
|
}, command);
|
|
|
|
if (!m_suppressRefresh)
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
|
|
bool isAscii, uint64_t resolvedAddr) {
|
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
|
if (!m_doc->provider->isWritable()) return;
|
|
|
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
|
|
|
// Use the compose-resolved address when available (correct for pointer children).
|
|
// Fall back to tree.baseAddress + computeOffset for callers that don't supply it.
|
|
uint64_t addr;
|
|
if (resolvedAddr != 0) {
|
|
addr = resolvedAddr;
|
|
} else {
|
|
int64_t signedAddr = m_doc->tree.computeOffset(nodeIdx);
|
|
if (signedAddr < 0) return; // malformed tree: negative offset
|
|
addr = m_doc->tree.baseAddress + static_cast<uint64_t>(signedAddr);
|
|
}
|
|
|
|
// For vector 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;
|
|
}
|
|
// For Mat4x4 components: subLine encodes flat index (row*4 + col), 0-15
|
|
if (node.kind == NodeKind::Mat4x4 && subLine >= 0 && subLine < 16) {
|
|
addr += subLine * 4;
|
|
editKind = NodeKind::Float;
|
|
}
|
|
|
|
bool ok;
|
|
QByteArray newBytes;
|
|
if (isAscii) {
|
|
int expectedSize = sizeForKind(editKind);
|
|
newBytes = fmt::parseAsciiValue(text, expectedSize, &ok);
|
|
} else {
|
|
newBytes = fmt::parseValue(editKind, text, &ok);
|
|
}
|
|
if (!ok) return;
|
|
|
|
// For strings, pad/truncate to full buffer size
|
|
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;
|
|
|
|
// Read old bytes before writing (for undo)
|
|
QByteArray oldBytes = m_doc->provider->readBytes(addr, writeSize);
|
|
|
|
// Test the write first — don't push a command that will silently fail.
|
|
// This prevents optimistic visual updates for read-only providers.
|
|
bool writeOk = m_snapshotProv
|
|
? m_snapshotProv->write(addr, newBytes.constData(), newBytes.size())
|
|
: m_doc->provider->writeBytes(addr, newBytes);
|
|
if (!writeOk) {
|
|
qWarning() << "Write failed at address" << QString::number(addr, 16);
|
|
refresh(); // refresh to show the real unchanged value
|
|
return;
|
|
}
|
|
|
|
// Write succeeded — push undo command (redo will write again, which is harmless)
|
|
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;
|
|
|
|
int copySize = src.byteSize();
|
|
int copyOffset = src.offset + copySize;
|
|
|
|
// Shift later siblings down to make room for the copy
|
|
QVector<cmd::OffsetAdj> adjs;
|
|
if (src.parentId != 0) {
|
|
auto siblings = m_doc->tree.childrenOf(src.parentId);
|
|
for (int si : siblings) {
|
|
if (si == nodeIdx) continue;
|
|
auto& sib = m_doc->tree.nodes[si];
|
|
if (sib.offset >= copyOffset)
|
|
adjs.append({sib.id, sib.offset, sib.offset + copySize});
|
|
}
|
|
}
|
|
|
|
Node n;
|
|
n.kind = src.kind;
|
|
n.name = src.name + "_copy";
|
|
n.parentId = src.parentId;
|
|
n.offset = copyOffset;
|
|
n.id = m_doc->tree.reserveId();
|
|
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs}));
|
|
}
|
|
|
|
void RcxController::convertToTypedPointer(uint64_t nodeId) {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni < 0) return;
|
|
const Node& node = m_doc->tree.nodes[ni];
|
|
|
|
// Determine pointer kind from document's target pointer size
|
|
NodeKind ptrKind = (m_doc->tree.pointerSize >= 8)
|
|
? NodeKind::Pointer64
|
|
: NodeKind::Pointer32;
|
|
|
|
// Generate unique struct name: "NewClass", "NewClass_2", "NewClass_3", ...
|
|
QString baseName = QStringLiteral("NewClass");
|
|
QString typeName = baseName;
|
|
int suffix = 2;
|
|
while (true) {
|
|
bool exists = false;
|
|
for (const auto& n : m_doc->tree.nodes) {
|
|
if (n.kind == NodeKind::Struct && n.structTypeName == typeName) {
|
|
exists = true; break;
|
|
}
|
|
}
|
|
if (!exists) break;
|
|
typeName = QStringLiteral("%1_%2").arg(baseName).arg(suffix++);
|
|
}
|
|
|
|
// Create the new root struct node
|
|
Node rootStruct;
|
|
rootStruct.kind = NodeKind::Struct;
|
|
rootStruct.name = QStringLiteral("instance");
|
|
rootStruct.structTypeName = typeName;
|
|
rootStruct.classKeyword = QStringLiteral("class");
|
|
rootStruct.parentId = 0;
|
|
rootStruct.offset = 0;
|
|
rootStruct.id = m_doc->tree.reserveId();
|
|
|
|
// Create child hex fields for the new struct, sized to target arch
|
|
constexpr int kDefaultFields = 16;
|
|
bool is32 = (m_doc->tree.pointerSize < 8);
|
|
NodeKind hexKind = is32 ? NodeKind::Hex32 : NodeKind::Hex64;
|
|
int stride = is32 ? 4 : 8;
|
|
QVector<Node> children;
|
|
for (int i = 0; i < kDefaultFields; i++) {
|
|
Node c;
|
|
c.kind = hexKind;
|
|
c.name = QStringLiteral("field_%1").arg(i * stride, 2, 16, QChar('0'));
|
|
c.parentId = rootStruct.id;
|
|
c.offset = i * stride;
|
|
c.id = m_doc->tree.reserveId();
|
|
children.append(c);
|
|
}
|
|
|
|
uint64_t oldRefId = node.refId;
|
|
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Change to ptr*"));
|
|
|
|
// 1. Change kind to Pointer64/32 (if not already)
|
|
if (node.kind != ptrKind)
|
|
changeNodeKind(ni, ptrKind);
|
|
|
|
// 2. Insert the new root struct
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{rootStruct, {}}));
|
|
|
|
// 3. Insert its children
|
|
for (const Node& c : children)
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{c, {}}));
|
|
|
|
// 4. Set refId to point to the new struct
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangePointerRef{nodeId, oldRefId, rootStruct.id}));
|
|
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = false;
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::splitHexNode(uint64_t nodeId) {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni < 0) return;
|
|
const Node& node = m_doc->tree.nodes[ni];
|
|
|
|
NodeKind halfKind;
|
|
int halfSize;
|
|
if (node.kind == NodeKind::Hex64) { halfKind = NodeKind::Hex32; halfSize = 4; }
|
|
else if (node.kind == NodeKind::Hex32) { halfKind = NodeKind::Hex16; halfSize = 2; }
|
|
else if (node.kind == NodeKind::Hex16) { halfKind = NodeKind::Hex8; halfSize = 1; }
|
|
else return;
|
|
|
|
uint64_t parentId = node.parentId;
|
|
int baseOffset = node.offset;
|
|
QString baseName = node.name;
|
|
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Split Hex node"));
|
|
|
|
// Remove the original node
|
|
QVector<Node> subtree;
|
|
subtree.append(node);
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Remove{nodeId, subtree, {}}));
|
|
|
|
// Insert two half-sized nodes
|
|
Node lo;
|
|
lo.kind = halfKind;
|
|
lo.name = baseName;
|
|
lo.parentId = parentId;
|
|
lo.offset = baseOffset;
|
|
lo.id = m_doc->tree.reserveId();
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{lo, {}}));
|
|
|
|
Node hi;
|
|
hi.kind = halfKind;
|
|
hi.name = baseName + QStringLiteral("_hi");
|
|
hi.parentId = parentId;
|
|
hi.offset = baseOffset + halfSize;
|
|
hi.id = m_doc->tree.reserveId();
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{hi, {}}));
|
|
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = false;
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::toggleBitfieldBit(uint64_t nodeId, int memberIdx) {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni < 0) return;
|
|
const Node& node = m_doc->tree.nodes[ni];
|
|
if (node.resolvedClassKeyword() != QStringLiteral("bitfield")) return;
|
|
if (memberIdx < 0 || memberIdx >= node.bitfieldMembers.size()) return;
|
|
if (!m_doc->provider || !m_doc->provider->isWritable()) return;
|
|
|
|
const auto& bm = node.bitfieldMembers[memberIdx];
|
|
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
|
|
int containerSize = sizeForKind(node.elementKind);
|
|
if (containerSize <= 0) containerSize = 4;
|
|
|
|
QByteArray oldBytes(containerSize, 0);
|
|
m_doc->provider->read(addr, oldBytes.data(), containerSize);
|
|
|
|
QByteArray newBytes = oldBytes;
|
|
// Toggle the bit
|
|
int byteIdx = bm.bitOffset / 8;
|
|
int bitInByte = bm.bitOffset % 8;
|
|
if (byteIdx < containerSize)
|
|
newBytes[byteIdx] = newBytes[byteIdx] ^ (1 << bitInByte);
|
|
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::WriteBytes{addr, oldBytes, newBytes}));
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::editBitfieldValue(uint64_t nodeId, int memberIdx) {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni < 0) return;
|
|
const Node& node = m_doc->tree.nodes[ni];
|
|
if (node.resolvedClassKeyword() != QStringLiteral("bitfield")) return;
|
|
if (memberIdx < 0 || memberIdx >= node.bitfieldMembers.size()) return;
|
|
if (!m_doc->provider || !m_doc->provider->isWritable()) return;
|
|
|
|
const auto& bm = node.bitfieldMembers[memberIdx];
|
|
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
|
|
int containerSize = sizeForKind(node.elementKind);
|
|
if (containerSize <= 0) containerSize = 4;
|
|
|
|
// Read current value
|
|
uint64_t curVal = fmt::extractBits(*m_doc->provider, addr, node.elementKind,
|
|
bm.bitOffset, bm.bitWidth);
|
|
uint64_t maxVal = (bm.bitWidth >= 64) ? UINT64_MAX : ((1ULL << bm.bitWidth) - 1);
|
|
|
|
bool ok = false;
|
|
QString input = QInputDialog::getText(nullptr,
|
|
QStringLiteral("Edit Bitfield Value"),
|
|
QStringLiteral("%1 (%2 bits, max %3):")
|
|
.arg(bm.name).arg(bm.bitWidth).arg(maxVal),
|
|
QLineEdit::Normal,
|
|
QString::number(curVal), &ok);
|
|
if (!ok || input.isEmpty()) return;
|
|
|
|
// Parse value (support hex with 0x prefix)
|
|
uint64_t newVal;
|
|
if (input.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
|
newVal = input.mid(2).toULongLong(&ok, 16);
|
|
else
|
|
newVal = input.toULongLong(&ok, 10);
|
|
if (!ok) return;
|
|
newVal &= maxVal;
|
|
|
|
QByteArray oldBytes(containerSize, 0);
|
|
m_doc->provider->read(addr, oldBytes.data(), containerSize);
|
|
|
|
// Read-modify-write: clear target bits and set new value
|
|
QByteArray newBytes = oldBytes;
|
|
uint64_t container = 0;
|
|
memcpy(&container, newBytes.constData(), qMin(containerSize, (int)sizeof(container)));
|
|
uint64_t mask = maxVal << bm.bitOffset;
|
|
container = (container & ~mask) | ((newVal & maxVal) << bm.bitOffset);
|
|
memcpy(newBytes.data(), &container, qMin(containerSize, (int)sizeof(container)));
|
|
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::WriteBytes{addr, oldBytes, newBytes}));
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|
int subLine, const QPoint& globalPos) {
|
|
auto icon = [](const char* name) { return QIcon(QStringLiteral(":/vsicons/%1").arg(name)); };
|
|
|
|
const bool hasNode = nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size();
|
|
|
|
// Selection policy
|
|
if (hasNode) {
|
|
uint64_t clickedId = m_doc->tree.nodes[nodeIdx].id;
|
|
if (!m_selIds.contains(clickedId)) {
|
|
m_selIds.clear();
|
|
m_selIds.insert(clickedId);
|
|
m_anchorLine = line;
|
|
applySelectionOverlays();
|
|
}
|
|
}
|
|
|
|
// Multi-select batch actions
|
|
if (hasNode && m_selIds.size() > 1) {
|
|
QMenu menu;
|
|
int count = m_selIds.size();
|
|
QSet<uint64_t> ids = m_selIds;
|
|
|
|
// Helper: collect indices from selected ids
|
|
auto collectIndices = [this, &ids]() {
|
|
QVector<int> indices;
|
|
for (uint64_t id : ids) {
|
|
int idx = m_doc->tree.indexOfId(id);
|
|
if (idx >= 0) indices.append(idx);
|
|
}
|
|
return indices;
|
|
};
|
|
|
|
// ── Insert shortcuts (always available) ──
|
|
menu.addAction(icon("diff-added.svg"), "Insert 4", [this]() {
|
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
|
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
|
|
});
|
|
menu.addAction(icon("diff-added.svg"), "Insert 8", [this]() {
|
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
|
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
|
|
});
|
|
menu.addSeparator();
|
|
|
|
// Quick-convert shortcuts when all selected nodes share the same kind
|
|
NodeKind commonKind = NodeKind::Hex64;
|
|
bool allSame = true;
|
|
{
|
|
bool first = true;
|
|
for (uint64_t id : ids) {
|
|
int idx = m_doc->tree.indexOfId(id);
|
|
if (idx < 0) continue;
|
|
if (first) { commonKind = m_doc->tree.nodes[idx].kind; first = false; }
|
|
else if (m_doc->tree.nodes[idx].kind != commonKind) { allSame = false; break; }
|
|
}
|
|
}
|
|
bool addedQuickConvert = false;
|
|
if (allSame) {
|
|
if (commonKind == NodeKind::Hex64) {
|
|
menu.addAction("Change to uint64_t", [this, collectIndices]() {
|
|
batchChangeKind(collectIndices(), NodeKind::UInt64); });
|
|
menu.addAction("Change to uint32_t", [this, collectIndices]() {
|
|
batchChangeKind(collectIndices(), NodeKind::UInt32); });
|
|
addedQuickConvert = true;
|
|
} else if (commonKind == NodeKind::Hex32) {
|
|
menu.addAction("Change to uint32_t", [this, collectIndices]() {
|
|
batchChangeKind(collectIndices(), NodeKind::UInt32); });
|
|
menu.addAction("Change to float", [this, collectIndices]() {
|
|
batchChangeKind(collectIndices(), NodeKind::Float); });
|
|
addedQuickConvert = true;
|
|
} else if (commonKind == NodeKind::Hex16) {
|
|
menu.addAction("Change to int16_t", [this, collectIndices]() {
|
|
batchChangeKind(collectIndices(), NodeKind::Int16); });
|
|
addedQuickConvert = true;
|
|
}
|
|
if (commonKind == NodeKind::Hex64 || commonKind == NodeKind::Pointer64) {
|
|
menu.addAction("Change to fnptr64", [this, collectIndices]() {
|
|
batchChangeKind(collectIndices(), NodeKind::FuncPtr64); });
|
|
addedQuickConvert = true;
|
|
}
|
|
if (commonKind == NodeKind::Hex32 || commonKind == NodeKind::Pointer32) {
|
|
menu.addAction("Change to fnptr32", [this, collectIndices]() {
|
|
batchChangeKind(collectIndices(), NodeKind::FuncPtr32); });
|
|
addedQuickConvert = true;
|
|
}
|
|
if (commonKind == NodeKind::FuncPtr64) {
|
|
menu.addAction("Change to ptr64", [this, collectIndices]() {
|
|
batchChangeKind(collectIndices(), NodeKind::Pointer64); });
|
|
addedQuickConvert = true;
|
|
}
|
|
if (commonKind == NodeKind::FuncPtr32) {
|
|
menu.addAction("Change to ptr32", [this, collectIndices]() {
|
|
batchChangeKind(collectIndices(), NodeKind::Pointer32); });
|
|
addedQuickConvert = true;
|
|
}
|
|
}
|
|
if (addedQuickConvert)
|
|
menu.addSeparator();
|
|
|
|
menu.addAction(icon("symbol-structure.svg"), QString("Change type of %1 nodes...").arg(count),
|
|
[this, ids, collectIndices]() {
|
|
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)
|
|
batchChangeKind(collectIndices(), kindFromString(sel));
|
|
});
|
|
|
|
menu.addSeparator();
|
|
{
|
|
auto* act = menu.addAction("Track Value Changes");
|
|
act->setCheckable(true);
|
|
act->setChecked(m_trackValues);
|
|
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
|
}
|
|
{
|
|
auto* act = menu.addAction("Clear Value History");
|
|
act->setToolTip(QStringLiteral("Reset change tracking for selected nodes"));
|
|
connect(act, &QAction::triggered, this, [this, ids]() {
|
|
for (uint64_t id : ids) {
|
|
m_valueHistory.remove(id);
|
|
for (int ci : m_doc->tree.subtreeIndices(id))
|
|
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
|
|
}
|
|
m_refreshGen++; // discard in-flight async reads
|
|
m_prevPages.clear(); // clean baseline for next read cycle
|
|
m_changedOffsets.clear(); // no phantom change indicators
|
|
m_valueTrackCooldown = 5; // suppress tracking for ~1s
|
|
refresh();
|
|
for (auto* editor : m_editors)
|
|
editor->dismissHistoryPopup();
|
|
});
|
|
}
|
|
menu.addSeparator();
|
|
|
|
// Check if all selected nodes share the same parent (required for grouping)
|
|
{
|
|
bool sameParent = true;
|
|
uint64_t firstParent = 0;
|
|
bool fp = true;
|
|
for (uint64_t id : ids) {
|
|
int idx = m_doc->tree.indexOfId(id);
|
|
if (idx < 0) { sameParent = false; break; }
|
|
if (fp) { firstParent = m_doc->tree.nodes[idx].parentId; fp = false; }
|
|
else if (m_doc->tree.nodes[idx].parentId != firstParent) { sameParent = false; break; }
|
|
}
|
|
if (sameParent)
|
|
menu.addAction("Group into Union", [this, ids]() { groupIntoUnion(ids); });
|
|
}
|
|
|
|
menu.addAction(icon("files.svg"), QString("Duplicate %1 nodes").arg(count), [this, ids]() {
|
|
for (uint64_t id : ids) {
|
|
int idx = m_doc->tree.indexOfId(id);
|
|
if (idx >= 0) duplicateNode(idx);
|
|
}
|
|
});
|
|
menu.addAction(icon("trash.svg"), QString("Delete %1 nodes").arg(count), [this, collectIndices]() {
|
|
batchRemoveNodes(collectIndices());
|
|
});
|
|
|
|
menu.addSeparator();
|
|
|
|
QMenu* copyMenu = menu.addMenu(icon("clippy.svg"), "Copy");
|
|
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, ids]() {
|
|
QStringList addrs;
|
|
for (uint64_t id : ids) {
|
|
int ni = m_doc->tree.indexOfId(id);
|
|
if (ni < 0) continue;
|
|
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
|
|
addrs << QStringLiteral("0x") + QString::number(addr, 16).toUpper();
|
|
}
|
|
QApplication::clipboard()->setText(addrs.join('\n'));
|
|
});
|
|
|
|
menu.exec(globalPos);
|
|
return;
|
|
}
|
|
|
|
QMenu menu;
|
|
|
|
// ── Insert shortcuts (at very top) ──
|
|
if (hasNode) {
|
|
menu.addAction(icon("diff-added.svg"), "Insert 4 Above\tShift+Ins",
|
|
[this, nodeIdx]() {
|
|
insertNodeAbove(nodeIdx, NodeKind::Hex32, QStringLiteral("field"));
|
|
});
|
|
menu.addAction(icon("diff-added.svg"), "Insert 8 Above\tIns",
|
|
[this, nodeIdx]() {
|
|
insertNodeAbove(nodeIdx, NodeKind::Hex64, QStringLiteral("field"));
|
|
});
|
|
} else {
|
|
menu.addAction(icon("diff-added.svg"), "Insert 4", [this]() {
|
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
|
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
|
|
});
|
|
menu.addAction(icon("diff-added.svg"), "Insert 8", [this]() {
|
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
|
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
|
|
});
|
|
}
|
|
menu.addSeparator();
|
|
|
|
// ── Node-specific actions (only when clicking on a node) ──
|
|
if (hasNode) {
|
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
|
uint64_t nodeId = node.id;
|
|
uint64_t parentId = node.parentId;
|
|
|
|
// ── Member line: enum or bitfield member ──
|
|
bool isEnumMember = node.resolvedClassKeyword() == QStringLiteral("enum")
|
|
&& !node.enumMembers.isEmpty()
|
|
&& subLine >= 0 && subLine < node.enumMembers.size();
|
|
bool isBitfieldMember = node.resolvedClassKeyword() == QStringLiteral("bitfield")
|
|
&& !node.bitfieldMembers.isEmpty()
|
|
&& subLine >= 0 && subLine < node.bitfieldMembers.size();
|
|
|
|
if (isEnumMember || isBitfieldMember) {
|
|
if (isBitfieldMember) {
|
|
const auto& bm = node.bitfieldMembers[subLine];
|
|
if (bm.bitWidth == 1) {
|
|
menu.addAction("Toggle Bit", [this, nodeId, subLine]() {
|
|
toggleBitfieldBit(nodeId, subLine);
|
|
});
|
|
} else {
|
|
menu.addAction("Edit Value...", [this, nodeId, subLine]() {
|
|
editBitfieldValue(nodeId, subLine);
|
|
});
|
|
}
|
|
menu.addSeparator();
|
|
}
|
|
// Fall through to always-available actions
|
|
} else {
|
|
|
|
// Quick-convert suggestions for Hex nodes
|
|
bool addedQuickConvert = false;
|
|
if (node.kind == NodeKind::Hex64) {
|
|
menu.addAction("Change to uint64_t", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt64);
|
|
});
|
|
menu.addAction("Change to uint32_t", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
|
|
});
|
|
addedQuickConvert = true;
|
|
} else if (node.kind == NodeKind::Hex32) {
|
|
menu.addAction("Change to uint32_t", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
|
|
});
|
|
menu.addAction("Change to float", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) changeNodeKind(ni, NodeKind::Float);
|
|
});
|
|
addedQuickConvert = true;
|
|
} else if (node.kind == NodeKind::Hex16) {
|
|
menu.addAction("Change to int16_t", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) changeNodeKind(ni, NodeKind::Int16);
|
|
});
|
|
addedQuickConvert = true;
|
|
}
|
|
if (node.kind == NodeKind::Hex64 || node.kind == NodeKind::Pointer64) {
|
|
menu.addAction("Change to fnptr64", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) changeNodeKind(ni, NodeKind::FuncPtr64);
|
|
});
|
|
addedQuickConvert = true;
|
|
}
|
|
if (node.kind == NodeKind::Hex32 || node.kind == NodeKind::Pointer32) {
|
|
menu.addAction("Change to fnptr32", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) changeNodeKind(ni, NodeKind::FuncPtr32);
|
|
});
|
|
addedQuickConvert = true;
|
|
}
|
|
if (node.kind == NodeKind::FuncPtr64) {
|
|
menu.addAction("Change to ptr64", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) changeNodeKind(ni, NodeKind::Pointer64);
|
|
});
|
|
addedQuickConvert = true;
|
|
}
|
|
if (node.kind == NodeKind::FuncPtr32) {
|
|
menu.addAction("Change to ptr32", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) changeNodeKind(ni, NodeKind::Pointer32);
|
|
});
|
|
addedQuickConvert = true;
|
|
}
|
|
// "Change to ptr*" — convert hex/void-ptr to typed pointer with auto-created class
|
|
if (node.kind == NodeKind::Hex64 || node.kind == NodeKind::Hex32
|
|
|| ((node.kind == NodeKind::Pointer64 || node.kind == NodeKind::Pointer32)
|
|
&& node.refId == 0)) {
|
|
menu.addAction("Change to ptr*", [this, nodeId]() {
|
|
convertToTypedPointer(nodeId);
|
|
});
|
|
addedQuickConvert = true;
|
|
}
|
|
// Split hex node into two half-sized hex nodes
|
|
if (node.kind == NodeKind::Hex64) {
|
|
menu.addAction("Change to hex32+hex32", [this, nodeId]() {
|
|
splitHexNode(nodeId);
|
|
});
|
|
addedQuickConvert = true;
|
|
} else if (node.kind == NodeKind::Hex32) {
|
|
menu.addAction("Change to hex16+hex16", [this, nodeId]() {
|
|
splitHexNode(nodeId);
|
|
});
|
|
addedQuickConvert = true;
|
|
} else if (node.kind == NodeKind::Hex16) {
|
|
menu.addAction("Change to hex8+hex8", [this, nodeId]() {
|
|
splitHexNode(nodeId);
|
|
});
|
|
addedQuickConvert = true;
|
|
}
|
|
if (addedQuickConvert)
|
|
menu.addSeparator();
|
|
|
|
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
|
|
&& !isHexNode(node.kind)
|
|
&& m_doc->provider->isWritable();
|
|
if (isEditable) {
|
|
menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() {
|
|
editor->beginInlineEdit(EditTarget::Value, line);
|
|
});
|
|
}
|
|
|
|
menu.addAction(icon("rename.svg"), "Re&name\tF2", [editor, line]() {
|
|
editor->beginInlineEdit(EditTarget::Name, line);
|
|
});
|
|
|
|
menu.addAction("Change &Type\tT", [editor, line]() {
|
|
editor->beginInlineEdit(EditTarget::Type, line);
|
|
});
|
|
|
|
menu.addSeparator();
|
|
{
|
|
auto* act = menu.addAction("Track Value Changes");
|
|
act->setCheckable(true);
|
|
act->setChecked(m_trackValues);
|
|
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
|
}
|
|
{
|
|
auto* act = menu.addAction("Clear Value History");
|
|
act->setToolTip(QStringLiteral("Reset change tracking for this node"));
|
|
connect(act, &QAction::triggered, this, [this, nodeId]() {
|
|
m_valueHistory.remove(nodeId);
|
|
for (int ci : m_doc->tree.subtreeIndices(nodeId))
|
|
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
|
|
m_refreshGen++; // discard in-flight async reads
|
|
m_prevPages.clear(); // clean baseline for next read cycle
|
|
m_changedOffsets.clear(); // no phantom change indicators
|
|
m_valueTrackCooldown = 5; // suppress tracking for ~1s
|
|
refresh();
|
|
for (auto* editor : m_editors)
|
|
editor->dismissHistoryPopup();
|
|
});
|
|
}
|
|
menu.addSeparator();
|
|
|
|
// Convert to Hex nodes (decompose non-hex types into Hex64/32/16/8)
|
|
if (!isHexNode(node.kind) && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
|
|
menu.addAction("Convert to &Hex", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni < 0) return;
|
|
const Node& n = m_doc->tree.nodes[ni];
|
|
int totalSize = n.byteSize();
|
|
if (totalSize <= 0) return;
|
|
|
|
uint64_t parentId = n.parentId;
|
|
int baseOffset = n.offset;
|
|
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Convert to Hex"));
|
|
|
|
// Remove the original node
|
|
QVector<Node> subtree;
|
|
subtree.append(n);
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Remove{nodeId, subtree, {}}));
|
|
|
|
// Insert hex nodes to fill the space (largest first)
|
|
int padOffset = baseOffset;
|
|
int gap = totalSize;
|
|
while (gap > 0) {
|
|
NodeKind padKind;
|
|
int padSize;
|
|
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
|
|
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
|
|
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
|
|
else { padKind = NodeKind::Hex8; padSize = 1; }
|
|
|
|
insertNode(parentId, padOffset, padKind,
|
|
QString("pad_%1").arg(padOffset, 2, 16, QChar('0')));
|
|
padOffset += padSize;
|
|
gap -= padSize;
|
|
}
|
|
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
if (!m_suppressRefresh) refresh();
|
|
});
|
|
}
|
|
|
|
menu.addSeparator();
|
|
|
|
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
|
|
menu.addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() {
|
|
insertNode(nodeId, 0, NodeKind::Hex64, "newField");
|
|
});
|
|
// Add Static Field — inserts a static field child
|
|
menu.addAction("Add Static Field", [this, nodeId]() {
|
|
Node sf;
|
|
sf.id = m_doc->tree.m_nextId++;
|
|
sf.kind = NodeKind::Hex64;
|
|
sf.name = QStringLiteral("static_field");
|
|
sf.parentId = nodeId;
|
|
sf.offset = 0;
|
|
sf.isStatic = true;
|
|
sf.offsetExpr = QStringLiteral("base");
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Insert{sf, {}}));
|
|
});
|
|
if (node.collapsed) {
|
|
menu.addAction(icon("expand-all.svg"), "&Expand", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) toggleCollapse(ni);
|
|
});
|
|
} else {
|
|
menu.addAction(icon("collapse-all.svg"), "&Collapse", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) toggleCollapse(ni);
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
// Add Static Field as sibling (for child nodes of a struct)
|
|
if (node.parentId != 0 && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
|
|
uint64_t parentId = node.parentId;
|
|
int pi = m_doc->tree.indexOfId(parentId);
|
|
if (pi >= 0 && (m_doc->tree.nodes[pi].kind == NodeKind::Struct
|
|
|| m_doc->tree.nodes[pi].kind == NodeKind::Array)) {
|
|
menu.addAction("Add Static Field", [this, parentId]() {
|
|
Node sf;
|
|
sf.id = m_doc->tree.m_nextId++;
|
|
sf.kind = NodeKind::Hex64;
|
|
sf.name = QStringLiteral("static_field");
|
|
sf.parentId = parentId;
|
|
sf.offset = 0;
|
|
sf.isStatic = true;
|
|
sf.offsetExpr = QStringLiteral("base");
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Insert{sf, {}}));
|
|
});
|
|
}
|
|
}
|
|
|
|
// Static field: Edit Expression inline
|
|
if (node.isStatic) {
|
|
menu.addAction("Edit E&xpression", [this, editor, line, nodeId]() {
|
|
// Build completions list: "base" + sibling field names
|
|
QStringList completions;
|
|
completions << QStringLiteral("base");
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) {
|
|
uint64_t parentId = m_doc->tree.nodes[ni].parentId;
|
|
for (const Node& sib : m_doc->tree.nodes) {
|
|
if (sib.parentId == parentId && !sib.isStatic && !sib.name.isEmpty())
|
|
completions << sib.name;
|
|
}
|
|
}
|
|
editor->setStaticCompletions(completions);
|
|
editor->beginInlineEdit(EditTarget::StaticExpr, line);
|
|
});
|
|
}
|
|
|
|
// Dissolve Union: available on union itself or any of its children
|
|
{
|
|
uint64_t targetUnionId = 0;
|
|
if (node.kind == NodeKind::Struct
|
|
&& node.resolvedClassKeyword() == QStringLiteral("union")) {
|
|
targetUnionId = nodeId;
|
|
} else if (node.parentId != 0) {
|
|
int pi = m_doc->tree.indexOfId(node.parentId);
|
|
if (pi >= 0 && m_doc->tree.nodes[pi].kind == NodeKind::Struct
|
|
&& m_doc->tree.nodes[pi].resolvedClassKeyword() == QStringLiteral("union")) {
|
|
targetUnionId = node.parentId;
|
|
}
|
|
}
|
|
if (targetUnionId != 0) {
|
|
menu.addAction("Dissolve Union", [this, targetUnionId]() {
|
|
dissolveUnion(targetUnionId);
|
|
});
|
|
}
|
|
}
|
|
|
|
menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) duplicateNode(ni);
|
|
});
|
|
menu.addAction(icon("trash.svg"), "&Delete\tDelete", [this, nodeId]() {
|
|
int ni = m_doc->tree.indexOfId(nodeId);
|
|
if (ni >= 0) removeNode(ni);
|
|
});
|
|
|
|
menu.addSeparator();
|
|
} // else (non-member node actions)
|
|
}
|
|
|
|
// ── Always-available actions ──
|
|
|
|
// Add Static Field to current view root (struct)
|
|
if (m_viewRootId != 0) {
|
|
int ri = m_doc->tree.indexOfId(m_viewRootId);
|
|
if (ri >= 0 && (m_doc->tree.nodes[ri].kind == NodeKind::Struct
|
|
|| m_doc->tree.nodes[ri].kind == NodeKind::Array)) {
|
|
uint64_t rootId = m_viewRootId;
|
|
menu.addAction("Add Static Field", [this, rootId]() {
|
|
Node sf;
|
|
sf.id = m_doc->tree.m_nextId++;
|
|
sf.kind = NodeKind::Hex64;
|
|
sf.name = QStringLiteral("static_field");
|
|
sf.parentId = rootId;
|
|
sf.offset = 0;
|
|
sf.isStatic = true;
|
|
sf.offsetExpr = QStringLiteral("base");
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::Insert{sf, {}}));
|
|
});
|
|
}
|
|
}
|
|
|
|
menu.addAction(icon("diff-added.svg"), "Append bytes...", [this, &menu]() {
|
|
bool ok;
|
|
QString input = QInputDialog::getText(menu.parentWidget(),
|
|
QStringLiteral("Append bytes"),
|
|
QStringLiteral("Byte count (decimal or 0x hex):"),
|
|
QLineEdit::Normal, QStringLiteral("128"), &ok);
|
|
if (!ok || input.trimmed().isEmpty()) return;
|
|
|
|
QString trimmed = input.trimmed();
|
|
int byteCount = 0;
|
|
if (trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
|
byteCount = trimmed.mid(2).toInt(&ok, 16);
|
|
else
|
|
byteCount = trimmed.toInt(&ok, 10);
|
|
if (!ok || byteCount <= 0) return;
|
|
|
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
|
int hex64Count = byteCount / 8;
|
|
int remainBytes = byteCount % 8;
|
|
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
|
|
int idx = 0;
|
|
for (int i = 0; i < hex64Count; i++, idx++)
|
|
insertNode(target, -1, NodeKind::Hex64,
|
|
QStringLiteral("field_%1").arg(idx));
|
|
for (int i = 0; i < remainBytes; i++, idx++)
|
|
insertNode(target, -1, NodeKind::Hex8,
|
|
QStringLiteral("field_%1").arg(idx));
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = false;
|
|
refresh();
|
|
});
|
|
|
|
menu.addSeparator();
|
|
// Only add Track Value Changes here if not already added in node-specific section
|
|
if (!hasNode) {
|
|
auto* act = menu.addAction("Track Value Changes");
|
|
act->setCheckable(true);
|
|
act->setChecked(m_trackValues);
|
|
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
|
menu.addSeparator();
|
|
}
|
|
|
|
menu.addAction(icon("arrow-left.svg"), "Undo", [this]() {
|
|
m_doc->undoStack.undo();
|
|
})->setEnabled(m_doc->undoStack.canUndo());
|
|
menu.addAction(icon("arrow-right.svg"), "Redo", [this]() {
|
|
m_doc->undoStack.redo();
|
|
})->setEnabled(m_doc->undoStack.canRedo());
|
|
|
|
menu.addSeparator();
|
|
|
|
QMenu* copyMenu = menu.addMenu(icon("clippy.svg"), "Copy");
|
|
if (hasNode) {
|
|
uint64_t copyNodeId = m_doc->tree.nodes[nodeIdx].id;
|
|
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, copyNodeId]() {
|
|
int ni = m_doc->tree.indexOfId(copyNodeId);
|
|
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());
|
|
});
|
|
copyMenu->addAction(icon("whole-word.svg"), "Copy &Offset", [this, copyNodeId]() {
|
|
int ni = m_doc->tree.indexOfId(copyNodeId);
|
|
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'));
|
|
});
|
|
copyMenu->addSeparator();
|
|
}
|
|
copyMenu->addAction("Copy Line", [editor, line]() {
|
|
auto* sci = editor->scintilla();
|
|
int len = (int)sci->SendScintilla(QsciScintillaBase::SCI_LINELENGTH, (unsigned long)line);
|
|
if (len > 0) {
|
|
QByteArray buf(len + 1, '\0');
|
|
sci->SendScintilla(QsciScintillaBase::SCI_GETLINE, (unsigned long)line, (void*)buf.data());
|
|
QString text = QString::fromUtf8(buf.data(), len).trimmed();
|
|
if (!text.isEmpty())
|
|
QApplication::clipboard()->setText(text);
|
|
}
|
|
});
|
|
copyMenu->addAction("Copy All as Text", [editor]() {
|
|
QApplication::clipboard()->setText(editor->textWithMargins());
|
|
});
|
|
|
|
menu.addSeparator();
|
|
|
|
menu.addAction(icon("search.svg"), "Search...\tCtrl+F", [editor]() {
|
|
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
|
|
});
|
|
|
|
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;
|
|
|
|
// Clear selection before delete (prevents stale highlight on shifted lines)
|
|
m_selIds.clear();
|
|
m_anchorLine = -1;
|
|
|
|
m_suppressRefresh = true;
|
|
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();
|
|
m_suppressRefresh = false;
|
|
refresh();
|
|
}
|
|
|
|
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;
|
|
|
|
// Clear selection before batch change
|
|
m_selIds.clear();
|
|
m_anchorLine = -1;
|
|
|
|
m_suppressRefresh = true;
|
|
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();
|
|
m_suppressRefresh = false;
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::handleNodeClick(RcxEditor* source, int line,
|
|
uint64_t nodeId,
|
|
Qt::KeyboardModifiers mods) {
|
|
bool ctrl = mods & Qt::ControlModifier;
|
|
bool shift = mods & Qt::ShiftModifier;
|
|
|
|
// Compute effective selection ID:
|
|
// footers → nodeId | kFooterIdBit
|
|
// array elements → nodeId | kArrayElemBit | (elemIdx << 48)
|
|
// everything else → nodeId
|
|
auto effectiveId = [this](int ln, uint64_t nid) -> uint64_t {
|
|
if (ln < 0 || ln >= m_lastResult.meta.size()) return nid;
|
|
const auto& lm = m_lastResult.meta[ln];
|
|
if (lm.lineKind == LineKind::Footer)
|
|
return nid | kFooterIdBit;
|
|
if (lm.isArrayElement && lm.arrayElementIdx >= 0)
|
|
return makeArrayElemSelId(nid, lm.arrayElementIdx);
|
|
if (lm.isMemberLine && lm.subLine >= 0)
|
|
return makeMemberSelId(nid, lm.subLine);
|
|
return nid;
|
|
};
|
|
|
|
uint64_t selId = effectiveId(line, nodeId);
|
|
|
|
if (!ctrl && !shift) {
|
|
m_selIds.clear();
|
|
m_selIds.insert(selId);
|
|
m_anchorLine = line;
|
|
} else if (ctrl && !shift) {
|
|
if (m_selIds.contains(selId))
|
|
m_selIds.remove(selId);
|
|
else
|
|
m_selIds.insert(selId);
|
|
m_anchorLine = line;
|
|
} else if (shift && !ctrl) {
|
|
if (m_anchorLine < 0) {
|
|
m_selIds.clear();
|
|
m_selIds.insert(selId);
|
|
m_anchorLine = line;
|
|
} else {
|
|
m_selIds.clear();
|
|
int from = qMin(m_anchorLine, line);
|
|
int to = qMax(m_anchorLine, line);
|
|
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
|
|
uint64_t nid = m_lastResult.meta[i].nodeId;
|
|
if (nid != 0 && nid != kCommandRowId) m_selIds.insert(effectiveId(i, nid));
|
|
}
|
|
}
|
|
} else { // Ctrl+Shift
|
|
if (m_anchorLine < 0) {
|
|
m_selIds.insert(selId);
|
|
m_anchorLine = line;
|
|
} else {
|
|
int from = qMin(m_anchorLine, line);
|
|
int to = qMax(m_anchorLine, line);
|
|
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
|
|
uint64_t nid = m_lastResult.meta[i].nodeId;
|
|
if (nid != 0 && nid != kCommandRowId) m_selIds.insert(effectiveId(i, nid));
|
|
}
|
|
}
|
|
}
|
|
|
|
updateCommandRow();
|
|
applySelectionOverlays();
|
|
|
|
if (m_selIds.size() == 1) {
|
|
uint64_t sid = *m_selIds.begin();
|
|
// Strip footer/array/member bits for node lookup
|
|
int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
|
| kMemberBit | kMemberSubMask));
|
|
if (idx >= 0) emit nodeSelected(idx);
|
|
}
|
|
}
|
|
|
|
void RcxController::clearSelection() {
|
|
m_selIds.clear();
|
|
m_anchorLine = -1;
|
|
updateCommandRow();
|
|
applySelectionOverlays();
|
|
}
|
|
|
|
void RcxController::applySelectionOverlays() {
|
|
for (auto* editor : m_editors)
|
|
editor->applySelectionOverlay(m_selIds);
|
|
}
|
|
|
|
|
|
void RcxController::updateCommandRow() {
|
|
// -- Source label: driven by provider metadata --
|
|
QString src;
|
|
QString provName = m_doc->provider->name();
|
|
if (provName.isEmpty()) {
|
|
src = QStringLiteral("source\u25BE");
|
|
} else {
|
|
src = QStringLiteral("'%1'\u25BE")
|
|
.arg(provName);
|
|
}
|
|
|
|
QString addr;
|
|
if (!m_doc->tree.baseAddressFormula.isEmpty())
|
|
addr = m_doc->tree.baseAddressFormula;
|
|
else
|
|
addr = QStringLiteral("0x") +
|
|
QString::number(m_doc->tree.baseAddress, 16).toUpper();
|
|
|
|
QString row = QStringLiteral("%1 %2")
|
|
.arg(elide(src, 40), elide(addr, 24));
|
|
|
|
// Build row 2: root class type + name (uses current view root)
|
|
QString row2;
|
|
if (m_viewRootId != 0) {
|
|
int vi = m_doc->tree.indexOfId(m_viewRootId);
|
|
if (vi >= 0) {
|
|
const auto& n = m_doc->tree.nodes[vi];
|
|
QString keyword = n.resolvedClassKeyword();
|
|
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
|
row2 = QStringLiteral("%1 %2 {")
|
|
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
|
|
}
|
|
}
|
|
if (row2.isEmpty()) {
|
|
// Fallback: find first root struct
|
|
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
|
const auto& n = m_doc->tree.nodes[i];
|
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
|
QString keyword = n.resolvedClassKeyword();
|
|
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
|
row2 = QStringLiteral("%1 %2 {")
|
|
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (row2.isEmpty())
|
|
row2 = QStringLiteral("struct NoName {");
|
|
|
|
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" ") + row2;
|
|
|
|
for (auto* ed : m_editors) {
|
|
ed->setCommandRowText(combined);
|
|
}
|
|
emit selectionChanged(m_selIds.size());
|
|
}
|
|
|
|
TypeSelectorPopup* RcxController::ensurePopup(RcxEditor* editor) {
|
|
if (!m_cachedPopup) {
|
|
m_cachedPopup = new TypeSelectorPopup(editor);
|
|
// Keep popup colors in sync when theme changes
|
|
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
|
|
m_cachedPopup, &TypeSelectorPopup::applyTheme);
|
|
// Pre-warm: force native window creation so first visible show is fast
|
|
m_cachedPopup->warmUp();
|
|
}
|
|
// Disconnect previous signals so we can reconnect fresh
|
|
m_cachedPopup->disconnect(this);
|
|
return m_cachedPopup;
|
|
}
|
|
|
|
void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|
int nodeIdx, QPoint globalPos) {
|
|
const Node* node = nullptr;
|
|
if (nodeIdx >= 0 && nodeIdx < (int)m_doc->tree.nodes.size())
|
|
node = &m_doc->tree.nodes[nodeIdx];
|
|
|
|
// ── Determine modifier preset (cheap — only reads node properties) ──
|
|
int preModId = 0;
|
|
int preArrayCount = 0;
|
|
if (mode == TypePopupMode::FieldType && node) {
|
|
bool isPtr = (node->kind == NodeKind::Pointer32 || node->kind == NodeKind::Pointer64);
|
|
bool isPrimPtr = isPtr && node->ptrDepth > 0 && node->refId == 0;
|
|
bool isTypedPtr = isPtr && node->refId != 0;
|
|
bool isArray = node->kind == NodeKind::Array;
|
|
if (isPrimPtr) preModId = (node->ptrDepth >= 2) ? 2 : 1;
|
|
else if (isTypedPtr) preModId = 1;
|
|
else if (isArray) { preModId = 3; preArrayCount = node->arrayLen; }
|
|
}
|
|
|
|
// ── Node size for same-size sorting (cheap) ──
|
|
int nodeSize = 0;
|
|
if (node) {
|
|
if (mode == TypePopupMode::ArrayElement)
|
|
nodeSize = sizeForKind(node->elementKind);
|
|
else
|
|
nodeSize = sizeForKind(node->kind);
|
|
}
|
|
|
|
// ── Font with zoom ──
|
|
QSettings settings("Reclass", "Reclass");
|
|
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
|
QFont font(fontName, 12);
|
|
font.setFixedPitch(true);
|
|
auto* sci = editor->scintilla();
|
|
int zoom = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
|
|
font.setPointSize(font.pointSize() + zoom);
|
|
|
|
// ── Position ──
|
|
QPoint pos = globalPos;
|
|
if (mode == TypePopupMode::Root) {
|
|
long lineStart = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0);
|
|
int lineH = (int)sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
|
|
int x = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
|
0, lineStart);
|
|
int y = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
|
0, lineStart);
|
|
pos = sci->viewport()->mapToGlobal(QPoint(x, y + lineH));
|
|
}
|
|
|
|
// ── Configure popup + show skeleton instantly ──
|
|
auto* popup = ensurePopup(editor);
|
|
popup->setFont(font);
|
|
popup->setMode(mode);
|
|
if (preModId > 0)
|
|
popup->setModifier(preModId, preArrayCount);
|
|
popup->setCurrentNodeSize(nodeSize);
|
|
popup->setPointerSize(m_doc->tree.pointerSize);
|
|
|
|
connect(popup, &TypeSelectorPopup::typeSelected,
|
|
this, [this, mode, nodeIdx](const TypeEntry& entry, const QString& fullText) {
|
|
applyTypePopupResult(mode, nodeIdx, entry, fullText);
|
|
});
|
|
connect(popup, &TypeSelectorPopup::createNewTypeRequested,
|
|
this, [this, mode, nodeIdx]() {
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Create new type"));
|
|
|
|
QString baseName = QStringLiteral("NewClass");
|
|
QString typeName = baseName;
|
|
int counter = 1;
|
|
QSet<QString> existing;
|
|
for (const auto& nd : m_doc->tree.nodes) {
|
|
if (nd.kind == NodeKind::Struct && !nd.structTypeName.isEmpty())
|
|
existing.insert(nd.structTypeName);
|
|
}
|
|
while (existing.contains(typeName))
|
|
typeName = baseName + QString::number(counter++);
|
|
|
|
Node n;
|
|
n.kind = NodeKind::Struct;
|
|
n.structTypeName = typeName;
|
|
n.name = QStringLiteral("instance");
|
|
n.parentId = 0;
|
|
n.offset = 0;
|
|
n.id = m_doc->tree.reserveId();
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
insertNode(n.id, i * 8, NodeKind::Hex64,
|
|
QString("field_%1").arg(i * 8, 2, 16, QChar('0')));
|
|
}
|
|
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
|
|
TypeEntry newEntry;
|
|
newEntry.entryKind = TypeEntry::Composite;
|
|
newEntry.structId = n.id;
|
|
applyTypePopupResult(mode, nodeIdx, newEntry, QString());
|
|
});
|
|
|
|
popup->popupLoading(pos);
|
|
|
|
// ── Deferred: build entry list + fill content (runs next event-loop tick) ──
|
|
int gen = ++m_typePopupGen;
|
|
QTimer::singleShot(0, this, [this, popup, mode, nodeIdx, gen]() {
|
|
if (gen != m_typePopupGen) return; // popup was reopened, discard stale load
|
|
|
|
const Node* node = nullptr;
|
|
if (nodeIdx >= 0 && nodeIdx < (int)m_doc->tree.nodes.size())
|
|
node = &m_doc->tree.nodes[nodeIdx];
|
|
|
|
QVector<TypeEntry> entries;
|
|
TypeEntry currentEntry;
|
|
bool hasCurrent = false;
|
|
|
|
auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) {
|
|
for (const auto& m : kKindMeta) {
|
|
if (excludeStructArrayPad &&
|
|
(m.kind == NodeKind::Struct || m.kind == NodeKind::Array))
|
|
continue;
|
|
TypeEntry e;
|
|
e.entryKind = TypeEntry::Primitive;
|
|
e.primitiveKind = m.kind;
|
|
e.displayName = QString::fromLatin1(m.typeName);
|
|
e.enabled = enabled;
|
|
e.sizeBytes = m.size;
|
|
e.alignment = m.align;
|
|
entries.append(e);
|
|
}
|
|
};
|
|
|
|
auto addComposites = [&](const std::function<bool(const Node&, const TypeEntry&)>& isCurrent) {
|
|
for (const auto& n : m_doc->tree.nodes) {
|
|
if (n.parentId != 0 || n.kind != NodeKind::Struct) continue;
|
|
TypeEntry e;
|
|
e.entryKind = TypeEntry::Composite;
|
|
e.structId = n.id;
|
|
e.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
|
e.classKeyword = n.resolvedClassKeyword();
|
|
e.category = (e.classKeyword == QStringLiteral("enum"))
|
|
? TypeEntry::CatEnum : TypeEntry::CatType;
|
|
e.sizeBytes = m_doc->tree.structSpan(n.id);
|
|
|
|
QVector<int> kids = m_doc->tree.childrenOf(n.id);
|
|
int nonStaticCount = 0;
|
|
int maxAlign = 1;
|
|
for (int i = 0; i < kids.size(); i++) {
|
|
const Node& child = m_doc->tree.nodes[kids[i]];
|
|
if (child.isStatic) continue;
|
|
nonStaticCount++;
|
|
int childAlign = alignmentFor(child.kind);
|
|
if (childAlign > maxAlign) maxAlign = childAlign;
|
|
if (e.fieldSummary.size() < 6) {
|
|
auto* cm = kindMeta(child.kind);
|
|
QString typeName = cm ? QString::fromLatin1(cm->typeName)
|
|
: QStringLiteral("???");
|
|
if (child.kind == NodeKind::Struct && child.refId != 0) {
|
|
int refIdx = m_doc->tree.indexOfId(child.refId);
|
|
if (refIdx >= 0) {
|
|
const Node& ref = m_doc->tree.nodes[refIdx];
|
|
typeName = ref.structTypeName.isEmpty()
|
|
? ref.name : ref.structTypeName;
|
|
}
|
|
}
|
|
e.fieldSummary << QStringLiteral("0x%1: %2 %3")
|
|
.arg(child.offset, 2, 16, QChar('0'))
|
|
.arg(typeName, child.name);
|
|
}
|
|
}
|
|
e.fieldCount = nonStaticCount;
|
|
e.alignment = maxAlign;
|
|
|
|
entries.append(e);
|
|
if (!hasCurrent && node && isCurrent(*node, e)) {
|
|
currentEntry = e;
|
|
hasCurrent = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
switch (mode) {
|
|
case TypePopupMode::Root:
|
|
addComposites([this](const Node&, const TypeEntry& e) {
|
|
return e.structId == m_viewRootId;
|
|
});
|
|
break;
|
|
|
|
case TypePopupMode::FieldType: {
|
|
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true);
|
|
bool isPtr = node
|
|
&& (node->kind == NodeKind::Pointer32 || node->kind == NodeKind::Pointer64);
|
|
bool isTypedPtr = isPtr && node->refId != 0;
|
|
bool isPrimPtr = isPtr && node->ptrDepth > 0 && node->refId == 0;
|
|
bool isArray = node && node->kind == NodeKind::Array;
|
|
|
|
if (isPrimPtr) {
|
|
for (auto& e : entries) {
|
|
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
|
|
currentEntry = e;
|
|
hasCurrent = true;
|
|
break;
|
|
}
|
|
}
|
|
} else if (isTypedPtr) {
|
|
// current set by addComposites below
|
|
} else if (isArray) {
|
|
if (node->elementKind != NodeKind::Struct) {
|
|
for (auto& e : entries) {
|
|
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
|
|
currentEntry = e;
|
|
hasCurrent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else if (node) {
|
|
if (!(node->kind == NodeKind::Struct && node->refId != 0)) {
|
|
for (auto& e : entries) {
|
|
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) {
|
|
currentEntry = e;
|
|
hasCurrent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
addComposites([&](const Node& n, const TypeEntry& e) {
|
|
if (isTypedPtr && n.refId == e.structId) return true;
|
|
if (isArray && n.elementKind == NodeKind::Struct && n.refId == e.structId) return true;
|
|
if (!isPtr && !isArray && n.kind == NodeKind::Struct && n.refId == e.structId) return true;
|
|
return false;
|
|
});
|
|
break;
|
|
}
|
|
|
|
case TypePopupMode::ArrayElement:
|
|
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true);
|
|
if (node) {
|
|
for (auto& e : entries) {
|
|
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
|
|
currentEntry = e;
|
|
hasCurrent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
addComposites([](const Node& n, const TypeEntry& e) {
|
|
return n.elementKind == NodeKind::Struct && n.refId == e.structId;
|
|
});
|
|
break;
|
|
|
|
case TypePopupMode::PointerTarget: {
|
|
TypeEntry voidEntry;
|
|
voidEntry.entryKind = TypeEntry::Primitive;
|
|
voidEntry.primitiveKind = NodeKind::Hex8;
|
|
voidEntry.displayName = QStringLiteral("void");
|
|
voidEntry.enabled = true;
|
|
entries.append(voidEntry);
|
|
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true);
|
|
if (node && node->refId == 0 && node->ptrDepth <= 1) {
|
|
currentEntry = voidEntry;
|
|
hasCurrent = true;
|
|
} else if (node && node->refId == 0 && node->ptrDepth > 0) {
|
|
for (auto& e : entries) {
|
|
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
|
|
currentEntry = e;
|
|
hasCurrent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
addComposites([](const Node& n, const TypeEntry& e) {
|
|
return n.refId == e.structId;
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add types from other open documents
|
|
if (mode != TypePopupMode::Root && m_projectDocs) {
|
|
QSet<QString> localNames;
|
|
for (const auto& e : entries)
|
|
if (e.entryKind == TypeEntry::Composite)
|
|
localNames.insert(e.displayName);
|
|
for (auto* doc : *m_projectDocs) {
|
|
if (doc == m_doc) continue;
|
|
for (const auto& n : doc->tree.nodes) {
|
|
if (n.parentId != 0 || n.kind != NodeKind::Struct) continue;
|
|
QString name = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
|
if (name.isEmpty() || localNames.contains(name)) continue;
|
|
localNames.insert(name);
|
|
TypeEntry e;
|
|
e.entryKind = TypeEntry::Composite;
|
|
e.structId = 0;
|
|
e.displayName = name;
|
|
e.classKeyword = n.resolvedClassKeyword();
|
|
e.category = (e.classKeyword == QStringLiteral("enum"))
|
|
? TypeEntry::CatEnum : TypeEntry::CatType;
|
|
e.sizeBytes = doc->tree.structSpan(n.id);
|
|
entries.append(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
popup->setTypes(entries, hasCurrent ? ¤tEntry : nullptr);
|
|
});
|
|
}
|
|
|
|
void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
|
const TypeEntry& entry, const QString& fullText) {
|
|
// Resolve external types: structId==0 means from another document, import first
|
|
TypeEntry resolved = entry;
|
|
if (resolved.entryKind == TypeEntry::Composite && resolved.structId == 0
|
|
&& !resolved.displayName.isEmpty()) {
|
|
resolved.structId = findOrCreateStructByName(resolved.displayName);
|
|
}
|
|
|
|
if (mode == TypePopupMode::Root) {
|
|
if (resolved.entryKind == TypeEntry::Composite)
|
|
setViewRootId(resolved.structId);
|
|
return;
|
|
}
|
|
|
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
|
|
|
// BUG-1 fix: Copy needed fields to locals before any mutation.
|
|
// changeNodeKind() can trigger insertNode() → addNode() → nodes.append(),
|
|
// which may reallocate the QVector, invalidating any reference into it.
|
|
const uint64_t nodeId = m_doc->tree.nodes[nodeIdx].id;
|
|
const NodeKind nodeKind = m_doc->tree.nodes[nodeIdx].kind;
|
|
const NodeKind elemKind = m_doc->tree.nodes[nodeIdx].elementKind;
|
|
const uint64_t nodeRefId = m_doc->tree.nodes[nodeIdx].refId;
|
|
const int arrLen = m_doc->tree.nodes[nodeIdx].arrayLen;
|
|
|
|
// Parse the full text for modifiers (e.g. "int32_t[10]", "Ball*")
|
|
TypeSpec spec = parseTypeSpec(fullText);
|
|
|
|
if (mode == TypePopupMode::FieldType) {
|
|
if (resolved.entryKind == TypeEntry::Primitive) {
|
|
if (spec.arrayCount > 0) {
|
|
// Primitive array: e.g. "int32_t[10]"
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Change to primitive array"));
|
|
if (nodeKind != NodeKind::Array)
|
|
changeNodeKind(nodeIdx, NodeKind::Array);
|
|
int idx = m_doc->tree.indexOfId(nodeId);
|
|
if (idx >= 0) {
|
|
auto& n = m_doc->tree.nodes[idx];
|
|
if (n.elementKind != resolved.primitiveKind || n.arrayLen != spec.arrayCount)
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeArrayMeta{nodeId, n.elementKind, resolved.primitiveKind,
|
|
n.arrayLen, spec.arrayCount}));
|
|
}
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
if (!m_suppressRefresh) refresh();
|
|
} else if (spec.isPointer) {
|
|
if (!isValidPrimitivePtrTarget(resolved.primitiveKind)) {
|
|
// Hex, pointer, fnptr types with * → plain void pointer
|
|
if (nodeKind != NodeKind::Pointer64)
|
|
changeNodeKind(nodeIdx, NodeKind::Pointer64);
|
|
int idx = m_doc->tree.indexOfId(nodeId);
|
|
if (idx >= 0) {
|
|
auto& n = m_doc->tree.nodes[idx];
|
|
n.ptrDepth = 0;
|
|
if (n.refId != 0)
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangePointerRef{nodeId, n.refId, 0}));
|
|
}
|
|
} else {
|
|
// Primitive pointer: e.g. "int32*" or "f64**" → Pointer64 + elementKind + ptrDepth
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Change to primitive pointer"));
|
|
if (nodeKind != NodeKind::Pointer64)
|
|
changeNodeKind(nodeIdx, NodeKind::Pointer64);
|
|
int idx = m_doc->tree.indexOfId(nodeId);
|
|
if (idx >= 0) {
|
|
auto& n = m_doc->tree.nodes[idx];
|
|
if (n.elementKind != resolved.primitiveKind || n.ptrDepth != spec.ptrDepth) {
|
|
NodeKind oldEK = n.elementKind;
|
|
int oldDepth = n.ptrDepth;
|
|
n.elementKind = resolved.primitiveKind;
|
|
n.ptrDepth = spec.ptrDepth;
|
|
if (n.refId != 0)
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangePointerRef{nodeId, n.refId, 0}));
|
|
Q_UNUSED(oldEK); Q_UNUSED(oldDepth);
|
|
}
|
|
}
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
if (!m_suppressRefresh) refresh();
|
|
}
|
|
} else {
|
|
if (resolved.primitiveKind != nodeKind)
|
|
changeNodeKind(nodeIdx, resolved.primitiveKind);
|
|
}
|
|
} else if (resolved.entryKind == TypeEntry::Composite) {
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Change to composite type"));
|
|
|
|
if (spec.isPointer) {
|
|
// Pointer modifier: e.g. "Material*" → Pointer64 + refId
|
|
if (nodeKind != NodeKind::Pointer64)
|
|
changeNodeKind(nodeIdx, NodeKind::Pointer64);
|
|
int idx = m_doc->tree.indexOfId(nodeId);
|
|
if (idx >= 0 && m_doc->tree.nodes[idx].refId != resolved.structId)
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, resolved.structId}));
|
|
|
|
} else if (spec.arrayCount > 0) {
|
|
// Array modifier: e.g. "Material[10]" → Array + Struct element
|
|
if (nodeKind != NodeKind::Array)
|
|
changeNodeKind(nodeIdx, NodeKind::Array);
|
|
int idx = m_doc->tree.indexOfId(nodeId);
|
|
if (idx >= 0) {
|
|
auto& n = m_doc->tree.nodes[idx];
|
|
if (n.elementKind != NodeKind::Struct || n.arrayLen != spec.arrayCount)
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeArrayMeta{nodeId, n.elementKind, NodeKind::Struct,
|
|
n.arrayLen, spec.arrayCount}));
|
|
if (n.refId != resolved.structId)
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangePointerRef{nodeId, n.refId, resolved.structId}));
|
|
}
|
|
|
|
} else {
|
|
// Plain struct: e.g. "Material" → Struct + structTypeName + refId + collapsed
|
|
if (nodeKind != NodeKind::Struct)
|
|
changeNodeKind(nodeIdx, NodeKind::Struct);
|
|
int idx = m_doc->tree.indexOfId(nodeId);
|
|
if (idx >= 0) {
|
|
int refIdx = m_doc->tree.indexOfId(resolved.structId);
|
|
QString targetName;
|
|
if (refIdx >= 0) {
|
|
const Node& ref = m_doc->tree.nodes[refIdx];
|
|
targetName = ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName;
|
|
}
|
|
QString oldTypeName = m_doc->tree.nodes[idx].structTypeName;
|
|
if (oldTypeName != targetName)
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeStructTypeName{nodeId, oldTypeName, targetName}));
|
|
// Set refId so compose can expand the referenced struct's children
|
|
if (m_doc->tree.nodes[idx].refId != resolved.structId)
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, resolved.structId}));
|
|
// ChangePointerRef auto-sets collapsed=true when refId != 0
|
|
}
|
|
}
|
|
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
if (!m_suppressRefresh) refresh();
|
|
}
|
|
} else if (mode == TypePopupMode::ArrayElement) {
|
|
if (resolved.entryKind == TypeEntry::Primitive) {
|
|
if (resolved.primitiveKind != elemKind) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeArrayMeta{nodeId,
|
|
elemKind, resolved.primitiveKind,
|
|
arrLen, arrLen}));
|
|
}
|
|
} else if (resolved.entryKind == TypeEntry::Composite) {
|
|
if (elemKind != NodeKind::Struct || nodeRefId != resolved.structId) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangeArrayMeta{nodeId,
|
|
elemKind, NodeKind::Struct,
|
|
arrLen, arrLen}));
|
|
if (nodeRefId != resolved.structId) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangePointerRef{nodeId, nodeRefId, resolved.structId}));
|
|
}
|
|
}
|
|
}
|
|
} else if (mode == TypePopupMode::PointerTarget) {
|
|
// "void" entry → refId 0; composite entry → real structId
|
|
uint64_t realRefId = (resolved.entryKind == TypeEntry::Composite) ? resolved.structId : 0;
|
|
if (realRefId != nodeRefId) {
|
|
m_doc->undoStack.push(new RcxCommand(this,
|
|
cmd::ChangePointerRef{nodeId, nodeRefId, realRefId}));
|
|
}
|
|
}
|
|
}
|
|
|
|
uint64_t RcxController::findOrCreateStructByName(const QString& typeName) {
|
|
// Check if it already exists locally
|
|
for (const auto& n : m_doc->tree.nodes) {
|
|
if (n.parentId == 0 && n.kind == NodeKind::Struct
|
|
&& (n.structTypeName == typeName || (n.structTypeName.isEmpty() && n.name == typeName)))
|
|
return n.id;
|
|
}
|
|
// Import: create a new root struct with that name + default hex fields
|
|
bool wasSuppressed = m_suppressRefresh;
|
|
m_suppressRefresh = true;
|
|
m_doc->undoStack.beginMacro(QStringLiteral("Import type"));
|
|
Node n;
|
|
n.kind = NodeKind::Struct;
|
|
n.structTypeName = typeName;
|
|
n.name = QStringLiteral("instance");
|
|
n.parentId = 0;
|
|
n.offset = 0;
|
|
n.id = m_doc->tree.reserveId();
|
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
|
for (int i = 0; i < 8; i++)
|
|
insertNode(n.id, i * 8, NodeKind::Hex64,
|
|
QString("field_%1").arg(i * 8, 2, 16, QChar('0')));
|
|
m_doc->undoStack.endMacro();
|
|
m_suppressRefresh = wasSuppressed;
|
|
return n.id;
|
|
}
|
|
|
|
void RcxController::attachViaPlugin(const QString& providerIdentifier, const QString& target) {
|
|
const auto* info = ProviderRegistry::instance().findProvider(providerIdentifier);
|
|
if (!info || !info->plugin) {
|
|
QMessageBox::warning(qobject_cast<QWidget*>(parent()),
|
|
"Provider Error",
|
|
QString("Provider '%1' not found. Is the plugin loaded?").arg(providerIdentifier));
|
|
return;
|
|
}
|
|
|
|
QString errorMsg;
|
|
auto provider = info->plugin->createProvider(target, &errorMsg);
|
|
if (!provider) {
|
|
if (!errorMsg.isEmpty())
|
|
QMessageBox::warning(qobject_cast<QWidget*>(parent()), "Provider Error", errorMsg);
|
|
return;
|
|
}
|
|
|
|
m_doc->undoStack.clear();
|
|
m_doc->provider = std::move(provider);
|
|
m_doc->dataPath.clear();
|
|
// Don't overwrite baseAddress — caller (e.g. selfTest) already set it.
|
|
// User-initiated source switches go through selectSource() which does update it.
|
|
|
|
// Adopt the provider's pointer size for this document
|
|
m_doc->tree.pointerSize = m_doc->provider->pointerSize();
|
|
|
|
// Re-evaluate stored formula against the new provider
|
|
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
|
AddressParserCallbacks cbs;
|
|
auto* prov = m_doc->provider.get();
|
|
cbs.resolveModule = [prov](const QString& name, bool* ok) -> uint64_t {
|
|
uint64_t base = prov->symbolToAddress(name);
|
|
*ok = (base != 0);
|
|
return base;
|
|
};
|
|
int ptrSz = m_doc->tree.pointerSize;
|
|
cbs.readPointer = [prov, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
|
uint64_t val = 0;
|
|
*ok = prov->read(addr, &val, ptrSz);
|
|
return val;
|
|
};
|
|
auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, ptrSz, &cbs);
|
|
if (result.ok)
|
|
m_doc->tree.baseAddress = result.value;
|
|
}
|
|
|
|
resetSnapshot();
|
|
emit m_doc->documentChanged();
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::switchToSavedSource(int idx) {
|
|
if (idx < 0 || idx >= m_savedSources.size()) return;
|
|
if (idx == m_activeSourceIdx) return;
|
|
|
|
// Save current source's base address before switching
|
|
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size()) {
|
|
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
|
|
m_savedSources[m_activeSourceIdx].baseAddressFormula = m_doc->tree.baseAddressFormula;
|
|
}
|
|
|
|
m_activeSourceIdx = idx;
|
|
const auto& entry = m_savedSources[idx];
|
|
|
|
if (entry.kind == QStringLiteral("File")) {
|
|
m_doc->loadData(entry.filePath);
|
|
m_doc->tree.baseAddress = entry.baseAddress;
|
|
m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
|
|
refresh();
|
|
} else if (!entry.providerTarget.isEmpty()) {
|
|
// Plugin-based provider (e.g. "processmemory" with target "pid:name")
|
|
// Restore formula before attach so it can be re-evaluated against the new provider
|
|
m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
|
|
attachViaPlugin(entry.kind, entry.providerTarget);
|
|
// Restore saved base address (user may have navigated away from provider default)
|
|
if (entry.baseAddress != 0 && entry.baseAddressFormula.isEmpty())
|
|
m_doc->tree.baseAddress = entry.baseAddress;
|
|
}
|
|
}
|
|
|
|
void RcxController::selectSource(const QString& text) {
|
|
if (text == QStringLiteral("#clear")) {
|
|
clearSources();
|
|
} else if (text.startsWith(QStringLiteral("#saved:"))) {
|
|
int idx = text.mid(7).toInt();
|
|
switchToSavedSource(idx);
|
|
} else if (text == QStringLiteral("File")) {
|
|
auto* w = qobject_cast<QWidget*>(parent());
|
|
QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)");
|
|
if (!path.isEmpty()) {
|
|
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
|
|
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
|
|
|
|
m_doc->loadData(path);
|
|
|
|
int existingIdx = -1;
|
|
for (int i = 0; i < m_savedSources.size(); i++) {
|
|
if (m_savedSources[i].kind == QStringLiteral("File")
|
|
&& m_savedSources[i].filePath == path) {
|
|
existingIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
if (existingIdx >= 0) {
|
|
m_activeSourceIdx = existingIdx;
|
|
m_doc->tree.baseAddress = m_savedSources[existingIdx].baseAddress;
|
|
} else {
|
|
SavedSourceEntry entry;
|
|
entry.kind = QStringLiteral("File");
|
|
entry.displayName = QFileInfo(path).fileName();
|
|
entry.filePath = path;
|
|
entry.baseAddress = m_doc->tree.baseAddress;
|
|
m_savedSources.append(entry);
|
|
m_activeSourceIdx = m_savedSources.size() - 1;
|
|
}
|
|
refresh();
|
|
}
|
|
} else {
|
|
const auto* providerInfo = ProviderRegistry::instance().findProvider(text.toLower().replace(" ", ""));
|
|
if (providerInfo) {
|
|
QString target;
|
|
bool selected = false;
|
|
|
|
if (providerInfo->isBuiltin) {
|
|
if (providerInfo->factory)
|
|
selected = providerInfo->factory(qobject_cast<QWidget*>(parent()), &target);
|
|
} else {
|
|
if (providerInfo->plugin)
|
|
selected = providerInfo->plugin->selectTarget(qobject_cast<QWidget*>(parent()), &target);
|
|
}
|
|
|
|
if (selected && !target.isEmpty()) {
|
|
std::unique_ptr<Provider> provider;
|
|
QString errorMsg;
|
|
if (providerInfo->plugin)
|
|
provider = providerInfo->plugin->createProvider(target, &errorMsg);
|
|
|
|
if (provider) {
|
|
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
|
|
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
|
|
|
|
uint64_t newBase = provider->base();
|
|
QString displayName = provider->name();
|
|
m_doc->undoStack.clear();
|
|
m_doc->provider = std::move(provider);
|
|
m_doc->dataPath.clear();
|
|
m_doc->tree.pointerSize = m_doc->provider->pointerSize();
|
|
|
|
// Re-evaluate formula if present (mirrors attachViaPlugin)
|
|
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
|
AddressParserCallbacks cbs;
|
|
auto* prov = m_doc->provider.get();
|
|
cbs.resolveModule = [prov](const QString& name, bool* ok) -> uint64_t {
|
|
uint64_t base = prov->symbolToAddress(name);
|
|
*ok = (base != 0);
|
|
return base;
|
|
};
|
|
int ptrSz = m_doc->tree.pointerSize;
|
|
cbs.readPointer = [prov, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
|
uint64_t val = 0;
|
|
*ok = prov->read(addr, &val, ptrSz);
|
|
return val;
|
|
};
|
|
auto result = AddressParser::evaluate(
|
|
m_doc->tree.baseAddressFormula, ptrSz, &cbs);
|
|
if (result.ok)
|
|
m_doc->tree.baseAddress = result.value;
|
|
} else if (newBase != 0 && m_doc->tree.baseAddress == 0x00400000) {
|
|
// Only apply provider base for fresh/default projects.
|
|
// If user loaded an .rcx with a custom base, preserve it.
|
|
m_doc->tree.baseAddress = newBase;
|
|
}
|
|
resetSnapshot();
|
|
emit m_doc->documentChanged();
|
|
|
|
QString identifier = providerInfo->identifier;
|
|
int existingIdx = -1;
|
|
for (int i = 0; i < m_savedSources.size(); i++) {
|
|
if (m_savedSources[i].kind == identifier
|
|
&& m_savedSources[i].providerTarget == target) {
|
|
existingIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
if (existingIdx >= 0) {
|
|
m_activeSourceIdx = existingIdx;
|
|
m_savedSources[existingIdx].baseAddress = m_doc->tree.baseAddress;
|
|
} else {
|
|
SavedSourceEntry entry;
|
|
entry.kind = identifier;
|
|
entry.displayName = displayName;
|
|
entry.providerTarget = target;
|
|
entry.baseAddress = m_doc->tree.baseAddress;
|
|
m_savedSources.append(entry);
|
|
m_activeSourceIdx = m_savedSources.size() - 1;
|
|
}
|
|
refresh();
|
|
} else if (!errorMsg.isEmpty()) {
|
|
QMessageBox::warning(qobject_cast<QWidget*>(parent()), "Provider Error", errorMsg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RcxController::clearSources() {
|
|
m_savedSources.clear();
|
|
m_activeSourceIdx = -1;
|
|
m_doc->provider = std::make_shared<NullProvider>();
|
|
m_doc->dataPath.clear();
|
|
resetSnapshot();
|
|
pushSavedSourcesToEditors();
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx) {
|
|
m_savedSources = sources;
|
|
m_activeSourceIdx = activeIdx;
|
|
pushSavedSourcesToEditors();
|
|
}
|
|
|
|
void RcxController::pushSavedSourcesToEditors() {
|
|
QVector<SavedSourceDisplay> display;
|
|
display.reserve(m_savedSources.size());
|
|
for (int i = 0; i < m_savedSources.size(); i++) {
|
|
SavedSourceDisplay d;
|
|
d.text = QStringLiteral("%1 '%2'")
|
|
.arg(m_savedSources[i].kind, m_savedSources[i].displayName);
|
|
d.active = (i == m_activeSourceIdx);
|
|
display.append(d);
|
|
}
|
|
for (auto* editor : m_editors)
|
|
editor->setSavedSources(display);
|
|
}
|
|
|
|
// ── Auto-refresh ──
|
|
|
|
void RcxController::setRefreshInterval(int ms) {
|
|
if (m_refreshTimer)
|
|
m_refreshTimer->setInterval(qMax(1, ms));
|
|
}
|
|
|
|
void RcxController::setCompactColumns(bool v) {
|
|
m_compactColumns = v;
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::setTreeLines(bool v) {
|
|
m_treeLines = v;
|
|
refresh();
|
|
}
|
|
|
|
void RcxController::setupAutoRefresh() {
|
|
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
|
m_refreshTimer = new QTimer(this);
|
|
m_refreshTimer->setInterval(qMax(1, ms));
|
|
connect(m_refreshTimer, &QTimer::timeout, this, &RcxController::onRefreshTick);
|
|
m_refreshTimer->start();
|
|
|
|
m_refreshWatcher = new QFutureWatcher<PageMap>(this);
|
|
connect(m_refreshWatcher, &QFutureWatcher<PageMap>::finished,
|
|
this, &RcxController::onReadComplete);
|
|
}
|
|
|
|
// Recursively collect memory ranges for a struct and its pointer targets.
|
|
// memBase is the absolute address where this struct's data lives.
|
|
void RcxController::collectPointerRanges(
|
|
uint64_t structId, uint64_t memBase,
|
|
int depth, int maxDepth,
|
|
QSet<QPair<uint64_t,uint64_t>>& visited,
|
|
QVector<QPair<uint64_t,int>>& ranges) const
|
|
{
|
|
if (depth >= maxDepth) return;
|
|
QPair<uint64_t,uint64_t> key{structId, memBase};
|
|
if (visited.contains(key)) return;
|
|
visited.insert(key);
|
|
|
|
int span = m_doc->tree.structSpan(structId);
|
|
if (span <= 0) return;
|
|
ranges.append({memBase, span});
|
|
|
|
if (!m_snapshotProv) return;
|
|
|
|
// Walk children looking for non-collapsed pointers
|
|
QVector<int> children = m_doc->tree.childrenOf(structId);
|
|
for (int ci : children) {
|
|
const Node& child = m_doc->tree.nodes[ci];
|
|
if (child.kind != NodeKind::Pointer32 && child.kind != NodeKind::Pointer64)
|
|
continue;
|
|
if (child.collapsed || child.refId == 0) continue;
|
|
|
|
uint64_t ptrAddr = memBase + child.offset;
|
|
int ptrSize = child.byteSize();
|
|
if (!m_snapshotProv->isReadable(ptrAddr, ptrSize)) continue;
|
|
|
|
uint64_t ptrVal = (child.kind == NodeKind::Pointer32)
|
|
? (uint64_t)m_snapshotProv->readU32(ptrAddr)
|
|
: m_snapshotProv->readU64(ptrAddr);
|
|
if (ptrVal == 0 || ptrVal == UINT64_MAX) continue;
|
|
|
|
uint64_t pBase = ptrVal;
|
|
collectPointerRanges(child.refId, pBase, depth + 1, maxDepth,
|
|
visited, ranges);
|
|
}
|
|
|
|
// Embedded struct references (struct node with refId but no own children)
|
|
int idx = m_doc->tree.indexOfId(structId);
|
|
if (idx >= 0) {
|
|
const Node& sn = m_doc->tree.nodes[idx];
|
|
if (sn.kind == NodeKind::Struct && sn.refId != 0 && children.isEmpty())
|
|
collectPointerRanges(sn.refId, memBase, depth, maxDepth,
|
|
visited, ranges);
|
|
}
|
|
}
|
|
|
|
void RcxController::onRefreshTick() {
|
|
if (m_readInFlight) return;
|
|
if (!m_doc->provider || !m_doc->provider->isLive()) return;
|
|
if (m_suppressRefresh) return;
|
|
for (auto* editor : m_editors)
|
|
if (editor->isEditing()) return;
|
|
|
|
int extent = computeDataExtent();
|
|
if (extent <= 0) return;
|
|
|
|
// Collect all needed ranges: main struct + pointer targets (absolute addresses)
|
|
QVector<QPair<uint64_t,int>> ranges;
|
|
ranges.append({m_doc->tree.baseAddress, extent});
|
|
|
|
if (m_snapshotProv) {
|
|
QSet<QPair<uint64_t,uint64_t>> visited;
|
|
uint64_t rootId = m_viewRootId;
|
|
if (rootId == 0 && !m_doc->tree.nodes.isEmpty())
|
|
rootId = m_doc->tree.nodes[0].id;
|
|
collectPointerRanges(rootId, m_doc->tree.baseAddress, 0, 99, visited, ranges);
|
|
}
|
|
|
|
m_readInFlight = true;
|
|
m_readGen = m_refreshGen;
|
|
|
|
auto prov = m_doc->provider;
|
|
m_refreshWatcher->setFuture(QtConcurrent::run([prov, ranges]() -> PageMap {
|
|
constexpr uint64_t kPageSize = 4096;
|
|
constexpr uint64_t kPageMask = ~(kPageSize - 1);
|
|
PageMap pages;
|
|
for (const auto& r : ranges) {
|
|
uint64_t pageStart = r.first & kPageMask;
|
|
uint64_t end = r.first + r.second;
|
|
uint64_t pageEnd = (end + kPageSize - 1) & kPageMask;
|
|
for (uint64_t p = pageStart; p < pageEnd; p += kPageSize) {
|
|
if (!pages.contains(p))
|
|
pages[p] = prov->readBytes(p, static_cast<int>(kPageSize));
|
|
}
|
|
}
|
|
return pages;
|
|
}));
|
|
}
|
|
|
|
void RcxController::onReadComplete() {
|
|
m_readInFlight = false;
|
|
|
|
if (m_readGen != m_refreshGen) return;
|
|
|
|
PageMap newPages;
|
|
try {
|
|
newPages = m_refreshWatcher->result();
|
|
} catch (const std::exception& e) {
|
|
qWarning() << "[Refresh] async read threw:" << e.what();
|
|
return;
|
|
} catch (...) {
|
|
qWarning() << "[Refresh] async read threw unknown exception";
|
|
return;
|
|
}
|
|
|
|
// All-zero guard: if page 0 is all zeros and we already have data, discard
|
|
if (!m_prevPages.isEmpty() && newPages.contains(0)) {
|
|
const QByteArray& p0 = newPages.value(0);
|
|
bool allZero = true;
|
|
for (int i = 0; i < p0.size(); ++i) {
|
|
if (p0[i] != 0) { allZero = false; break; }
|
|
}
|
|
if (allZero) {
|
|
qDebug() << "[Refresh] discarding all-zero page-0, keeping stale snapshot";
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fast path: no changes at all
|
|
if (newPages == m_prevPages)
|
|
return;
|
|
|
|
// Compute which byte offsets changed (for change highlighting).
|
|
// Skip on first snapshot — nothing to compare against.
|
|
m_changedOffsets.clear();
|
|
if (!m_prevPages.isEmpty()) {
|
|
for (auto it = newPages.constBegin(); it != newPages.constEnd(); ++it) {
|
|
uint64_t pageAddr = it.key();
|
|
const QByteArray& newPage = it.value();
|
|
auto oldIt = m_prevPages.constFind(pageAddr);
|
|
if (oldIt == m_prevPages.constEnd())
|
|
continue; // new page, no previous data to diff against
|
|
const QByteArray& oldPage = oldIt.value();
|
|
int cmpLen = qMin(oldPage.size(), newPage.size());
|
|
for (int i = 0; i < cmpLen; ++i) {
|
|
if (oldPage[i] != newPage[i])
|
|
m_changedOffsets.insert(static_cast<int64_t>(pageAddr) + i);
|
|
}
|
|
}
|
|
}
|
|
|
|
int mainExtent = computeDataExtent();
|
|
m_prevPages = newPages;
|
|
|
|
if (m_snapshotProv)
|
|
m_snapshotProv->updatePages(std::move(newPages), mainExtent);
|
|
else
|
|
m_snapshotProv = std::make_unique<SnapshotProvider>(
|
|
m_doc->provider, std::move(newPages), mainExtent);
|
|
|
|
refresh();
|
|
m_changedOffsets.clear();
|
|
}
|
|
|
|
int RcxController::computeDataExtent() const {
|
|
static constexpr int64_t kMaxMainExtent = 16 * 1024 * 1024; // 16 MB cap
|
|
|
|
int64_t treeExtent = 0;
|
|
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
|
const Node& node = m_doc->tree.nodes[i];
|
|
int64_t off = m_doc->tree.computeOffset(i);
|
|
int sz = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
|
|
? m_doc->tree.structSpan(node.id) : node.byteSize();
|
|
int64_t end = off + sz;
|
|
if (end > treeExtent) treeExtent = end;
|
|
}
|
|
if (treeExtent > 0) return static_cast<int>(qMin(treeExtent, kMaxMainExtent));
|
|
|
|
int provSize = m_doc->provider->size();
|
|
if (provSize > 0) return provSize;
|
|
return 0;
|
|
}
|
|
|
|
void RcxController::resetSnapshot() {
|
|
m_refreshGen++;
|
|
m_readInFlight = false;
|
|
m_snapshotProv.reset();
|
|
m_prevPages.clear();
|
|
m_changedOffsets.clear();
|
|
m_valueHistory.clear();
|
|
}
|
|
|
|
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)) {
|
|
if (lm->markerMask & (1u << M_CYCLE))
|
|
materializeRefChildren(lm->nodeIdx);
|
|
else
|
|
toggleCollapse(lm->nodeIdx);
|
|
} else if (margin == 0 || margin == 1) {
|
|
emit nodeSelected(lm->nodeIdx);
|
|
}
|
|
}
|
|
|
|
void RcxController::setEditorFont(const QString& fontName) {
|
|
for (auto* editor : m_editors)
|
|
editor->setEditorFont(fontName);
|
|
}
|
|
|
|
} // namespace rcx
|