#include "controller.h" #include "addressparser.h" #include "typeselectorpopup.h" #include "providerregistry.h" #include "themes/thememanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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("") : 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()) { connect(&undoStack, &QUndoStack::cleanChanged, this, [this](bool clean) { modified = !clean; }); } ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns) const { return rcx::compose(tree, *provider, viewRootId, compactColumns); } 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( file.readAll(), QFileInfo(binaryPath).fileName()); dataPath = binaryPath; tree.baseAddress = 0; emit documentChanged(); } void RcxDocument::loadData(const QByteArray& data) { undoStack.clear(); provider = std::make_shared(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(); } } 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); }); // 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) { 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; }; cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t { uint64_t val = 0; *ok = prov->read(addr, &val, 8); return val; }; } auto result = AddressParser::evaluate(s, 8, &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::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::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); else m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns); 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_trackValues && prov) { 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 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 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(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, {}})); // 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 adjs; if (delta != 0 && oldSize > 0 && newSize > 0) { int oldEnd = node.offset + oldSize; auto siblings = m_doc->tree.childrenOf(node.parentId); for (int si : siblings) { if (si == nodeIdx) continue; auto& sib = m_doc->tree.nodes[si]; if (sib.offset >= oldEnd) adjs.append({sib.id, sib.offset, sib.offset + delta}); } } m_doc->undoStack.push(new RcxCommand(this, cmd::ChangeKind{node.id, node.kind, newKind, adjs})); } } void RcxController::renameNode(int nodeIdx, const QString& newName) { if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; auto& node = m_doc->tree.nodes[nodeIdx]; m_doc->undoStack.push(new RcxCommand(this, cmd::Rename{node.id, node.name, newName})); } void RcxController::insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name) { Node n; n.kind = kind; n.name = name; n.parentId = parentId; if (offset < 0) { // Auto-place after last sibling with alignment int maxEnd = 0; auto siblings = m_doc->tree.childrenOf(parentId); for (int si : siblings) { auto& sn = m_doc->tree.nodes[si]; int sz = (sn.kind == NodeKind::Struct || sn.kind == NodeKind::Array) ? m_doc->tree.structSpan(sn.id) : sn.byteSize(); int end = sn.offset + sz; if (end > maxEnd) maxEnd = end; } int align = alignmentFor(kind); n.offset = (maxEnd + align - 1) / align * align; } else { n.offset = offset; } // 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::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 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 indices = m_doc->tree.subtreeIndices(nodeId); QVector 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& nodeIds) { if (nodeIds.size() < 2) return; // Collect nodes and verify they share the same parent QVector 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 subtree; }; QVector 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 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 subtree; }; QVector 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 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 refChildren = tree.childrenOf(refId); if (refChildren.isEmpty()) return; QVector 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& 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; if constexpr (std::is_same_v) { 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) { 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) { 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) { 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) { 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 indices = tree.subtreeIndices(c.nodeId); std::sort(indices.begin(), indices.end(), std::greater()); 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) { tree.baseAddress = isUndo ? c.oldBase : c.newBase; tree.baseAddressFormula = isUndo ? c.oldFormula : c.newFormula; resetSnapshot(); } else if constexpr (std::is_same_v) { 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) { 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) { 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) { 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) { 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) { 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) { int idx = tree.indexOfId(c.nodeId); if (idx >= 0) tree.nodes[idx].enumMembers = isUndo ? c.oldMembers : c.newMembers; } }, 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(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 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 current node size NodeKind ptrKind; if (node.byteSize() >= 8 || node.kind == NodeKind::Pointer64) ptrKind = NodeKind::Pointer64; else ptrKind = 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 Hex64 fields for the new struct constexpr int kDefaultFields = 16; QVector children; for (int i = 0; i < kDefaultFields; i++) { Node c; c.kind = NodeKind::Hex64; c.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0')); c.parentId = rootStruct.id; c.offset = i * 8; 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 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 ids = m_selIds; // Helper: collect indices from selected ids auto collectIndices = [this, &ids]() { QVector indices; for (uint64_t id : ids) { int idx = m_doc->tree.indexOfId(id); if (idx >= 0) indices.append(idx); } return indices; }; // 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); }); 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); } 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(); menu.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; // ── 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); }); 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); } 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 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"); }); 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); }); } } // 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(); menu.addAction(icon("link.svg"), "Copy &Address", [this, nodeId]() { int ni = m_doc->tree.indexOfId(nodeId); if (ni < 0) return; uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni); QApplication::clipboard()->setText( QStringLiteral("0x") + QString::number(addr, 16).toUpper()); }); menu.addAction(icon("whole-word.svg"), "Copy &Offset", [this, nodeId]() { int ni = m_doc->tree.indexOfId(nodeId); if (ni < 0) return; int off = m_doc->tree.nodes[ni].offset; QApplication::clipboard()->setText( QStringLiteral("+0x") + QString::number(off, 16).toUpper().rightJustified(4, '0')); }); menu.addSeparator(); } // else (non-member node actions) } // ── Always-available actions ── 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(); menu.addAction(icon("clippy.svg"), "Copy All as Text", [editor]() { QApplication::clipboard()->setText(editor->textWithMargins()); }); menu.exec(globalPos); } void RcxController::batchRemoveNodes(const QVector& nodeIndices) { QSet 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& nodeIndices, NodeKind newKind) { QSet 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 < m_doc->tree.nodes.size()) node = &m_doc->tree.nodes[nodeIdx]; // ── Build entry list based on mode ── QVector entries; TypeEntry currentEntry; bool hasCurrent = false; int preModId = 0; // modifier to preselect: 0=plain, 1=*, 2=**, 3=[n] int preArrayCount = 0; // array count when preModId==3 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; entries.append(e); } }; auto addComposites = [&](const std::function& 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(); entries.append(e); if (!hasCurrent && node && isCurrent(*node, e)) { currentEntry = e; hasCurrent = true; } } }; switch (mode) { case TypePopupMode::Root: // No primitives in Root mode – only project types are valid roots addComposites([&](const Node&, const TypeEntry& e) { return e.structId == m_viewRootId; }); break; case TypePopupMode::FieldType: { addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false); 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) { // Primitive pointer (e.g. int32* or f64**) — current = element kind, modifier = *//** preModId = (node->ptrDepth >= 2) ? 2 : 1; for (auto& e : entries) { if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) { currentEntry = e; hasCurrent = true; break; } } } else if (isTypedPtr) { // Typed pointer (e.g. Ball*) — current = composite target, modifier = * preModId = 1; } else if (isArray) { // Array — modifier = [n] preModId = 3; preArrayCount = node->arrayLen; if (node->elementKind != NodeKind::Struct) { // Primitive array — mark element kind as current for (auto& e : entries) { if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) { currentEntry = e; hasCurrent = true; break; } } } } else if (node) { // Plain primitive — mark current for (auto& e : entries) { if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) { currentEntry = e; hasCurrent = true; break; } } } // For isTypedPtr or struct-array: current is a Composite, set by addComposites below 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; 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: { // "void" entry as a primitive with a special display TypeEntry voidEntry; voidEntry.entryKind = TypeEntry::Primitive; voidEntry.primitiveKind = NodeKind::Hex8; // unused, but needs a value voidEntry.displayName = QStringLiteral("void"); voidEntry.enabled = true; entries.append(voidEntry); if (node && node->refId == 0) { currentEntry = voidEntry; hasCurrent = true; } addComposites([](const Node& n, const TypeEntry& e) { return n.refId == e.structId; }); break; } } // ── Add types from other open documents (not for Root mode) ── if (mode != TypePopupMode::Root && m_projectDocs) { QSet 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; // sentinel: not in local tree yet e.displayName = name; e.classKeyword = n.resolvedClassKeyword(); entries.append(e); } } } // ── 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) { // Bottom-left of the [▸] span on line 0 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 and show popup ── auto* popup = ensurePopup(editor); popup->setFont(font); popup->setMode(mode); // Preselect modifier button to reflect current node state (after setMode resets to plain) if (preModId > 0) popup->setModifier(preModId, preArrayCount); // Pass current node size for same-size sorting int nodeSize = 0; if (node) { if (mode == TypePopupMode::ArrayElement) nodeSize = sizeForKind(node->elementKind); else nodeSize = sizeForKind(node->kind); } popup->setCurrentNodeSize(nodeSize); static const char* titles[] = { "Change root", "Change type", "Element type", "Pointer target" }; popup->setTitle(QString::fromLatin1(titles[(int)mode])); popup->setTypes(entries, hasCurrent ? ¤tEntry : nullptr); 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")); // Generate unique default type name QString baseName = QStringLiteral("NewClass"); QString typeName = baseName; int counter = 1; QSet 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})); // Populate with default hex nodes (8 x Hex64 = 64 bytes) 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->popup(pos); } 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(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(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. // 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; }; cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t { uint64_t val = 0; *ok = prov->read(addr, &val, 8); return val; }; auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, 8, &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(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(parent()), &target); } else { if (providerInfo->plugin) selected = providerInfo->plugin->selectTarget(qobject_cast(parent()), &target); } if (selected && !target.isEmpty()) { std::unique_ptr 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.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress; 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(parent()), "Provider Error", errorMsg); } } } } } void RcxController::clearSources() { m_savedSources.clear(); m_activeSourceIdx = -1; m_doc->provider = std::make_shared(); m_doc->dataPath.clear(); resetSnapshot(); pushSavedSourcesToEditors(); refresh(); } void RcxController::copySavedSources(const QVector& sources, int activeIdx) { m_savedSources = sources; m_activeSourceIdx = activeIdx; pushSavedSourcesToEditors(); } void RcxController::pushSavedSourcesToEditors() { QVector 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::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(this); connect(m_refreshWatcher, &QFutureWatcher::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>& visited, QVector>& ranges) const { if (depth >= maxDepth) return; QPair 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 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> ranges; ranges.append({m_doc->tree.baseAddress, extent}); if (m_snapshotProv) { QSet> 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(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(pageAddr) + i); } } } int mainExtent = computeDataExtent(); m_prevPages = newPages; if (m_snapshotProv) m_snapshotProv->updatePages(std::move(newPages), mainExtent); else m_snapshotProv = std::make_unique( 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(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