mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: disasm popup, symbol separation, context menu improvements, RVA fixes
- Add Fadec x86 disassembler with hover popup for FuncPtr/void Pointer nodes - Separate pointer symbol from address: // prefix, green comment coloring, independent hover/click zones (address triggers popup, symbol is passive) - Fix RVA margin and inline local offset for pointer-expanded vtable children using ptrBase field threaded through composition - Expand multi-select context menu with quick-convert, duplicate, copy address - Remove Edit Value from hex node context menu - Fix heatmap flickering on hex nodes (remove per-byte alternation) - Fix popup repositioning when moving mouse between lines - Truncate disasm popup to 6 lines with ... indicator - Add BUILD_UI_TESTS option to skip widget tests on headless CI - Add test_disasm with 35 test cases for disassembly and hex dump - Add KUSER_SHARED_DATA example .rcx file
This commit is contained in:
@@ -22,6 +22,7 @@ struct ComposeState {
|
||||
int nameW = kColName; // global name column width (fallback)
|
||||
int offsetHexDigits = 8; // hex digit tier for offset margin
|
||||
bool baseEmitted = false; // only first root struct shows base address
|
||||
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
||||
|
||||
// Precomputed for O(1) lookups
|
||||
QHash<uint64_t, QVector<int>> childMap;
|
||||
@@ -141,6 +142,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeKind = node.kind;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.effectiveTypeW = typeW;
|
||||
@@ -187,6 +189,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
@@ -205,6 +208,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.lineKind = LineKind::ArrayElementSeparator;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
@@ -234,6 +238,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.lineKind = LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.isRootHeader = false;
|
||||
lm.foldHead = true;
|
||||
@@ -297,6 +302,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.isArrayElement = true;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + elemAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
|
||||
lm.foldLevel = computeFoldLevel(childDepth, false);
|
||||
lm.effectiveTypeW = eTW;
|
||||
@@ -350,6 +356,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
tree.baseAddress + absAddr + child.offset, false,
|
||||
state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr + child.offset;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = child.kind;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = true;
|
||||
@@ -394,6 +401,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
int sz = tree.structSpan(node.id, &state.childMap);
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr + sz;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
|
||||
}
|
||||
|
||||
@@ -439,6 +447,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = effectiveCollapsed;
|
||||
@@ -481,6 +490,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
if (!ptrReadable)
|
||||
pBase = (uint64_t)0 - tree.baseAddress;
|
||||
|
||||
uint64_t savedPtrBase = state.currentPtrBase;
|
||||
state.currentPtrBase = tree.baseAddress + pBase;
|
||||
|
||||
if (hasMaterialized) {
|
||||
// Render materialized children at the pointer target address.
|
||||
// These are real tree nodes with independent state — use rootId
|
||||
@@ -519,6 +531,8 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
}
|
||||
}
|
||||
|
||||
state.currentPtrBase = savedPtrBase;
|
||||
|
||||
// Footer for pointer fold
|
||||
{
|
||||
LineMeta lm;
|
||||
@@ -668,6 +682,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
lm.foldHead = false;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.markerMask = 0;
|
||||
lm.effectiveTypeW = state.typeW;
|
||||
lm.effectiveNameW = state.nameW;
|
||||
|
||||
@@ -654,9 +654,15 @@ void RcxController::refresh() {
|
||||
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;
|
||||
|
||||
int64_t nodeOff = m_doc->tree.computeOffset(lm.nodeIdx);
|
||||
uint64_t addr = static_cast<uint64_t>(nodeOff); // provider-relative
|
||||
// Use the absolute address from compose (correct for pointer-expanded nodes)
|
||||
// and convert to provider-relative by subtracting the base address.
|
||||
uint64_t addr = lm.offsetAddr >= m_doc->tree.baseAddress
|
||||
? lm.offsetAddr - m_doc->tree.baseAddress
|
||||
: static_cast<uint64_t>(m_doc->tree.computeOffset(lm.nodeIdx));
|
||||
int sz = node.byteSize();
|
||||
if (sz <= 0 || !prov->isReadable(addr, sz)) continue;
|
||||
|
||||
@@ -690,9 +696,18 @@ void RcxController::refresh() {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve providers for disasm popup:
|
||||
// - snapProv: snapshot or real — for reading pointer values within the tree
|
||||
// - realProv: always the real process provider — for reading code at arbitrary addresses
|
||||
const Provider* snapProv = m_snapshotProv
|
||||
? static_cast<const Provider*>(m_snapshotProv.get())
|
||||
: (m_doc->provider ? m_doc->provider.get() : nullptr);
|
||||
const Provider* realProv = m_doc->provider ? m_doc->provider.get() : nullptr;
|
||||
|
||||
for (auto* editor : m_editors) {
|
||||
editor->setCustomTypeNames(customTypes);
|
||||
editor->setValueHistoryRef(&m_valueHistory);
|
||||
editor->setProviderRef(snapProv, realProv, &m_doc->tree);
|
||||
ViewState vs = editor->saveViewState();
|
||||
editor->applyDocument(m_lastResult);
|
||||
editor->restoreViewState(vs);
|
||||
@@ -1160,35 +1175,111 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-select batch actions at top
|
||||
// Multi-select batch actions
|
||||
if (hasNode && m_selIds.size() > 1) {
|
||||
QMenu menu;
|
||||
int count = m_selIds.size();
|
||||
QSet<uint64_t> ids = m_selIds;
|
||||
menu.addAction(icon("trash.svg"), QString("Delete %1 nodes").arg(count), [this, ids]() {
|
||||
|
||||
// Helper: collect indices from selected ids
|
||||
auto collectIndices = [this, &ids]() {
|
||||
QVector<int> indices;
|
||||
for (uint64_t id : ids) {
|
||||
int idx = m_doc->tree.indexOfId(id);
|
||||
if (idx >= 0) indices.append(idx);
|
||||
}
|
||||
batchRemoveNodes(indices);
|
||||
});
|
||||
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]() {
|
||||
[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) {
|
||||
QVector<int> indices;
|
||||
for (uint64_t id : ids) {
|
||||
int idx = m_doc->tree.indexOfId(id);
|
||||
if (idx >= 0) indices.append(idx);
|
||||
}
|
||||
batchChangeKind(indices, kindFromString(sel));
|
||||
if (ok)
|
||||
batchChangeKind(collectIndices(), kindFromString(sel));
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1258,6 +1349,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
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]() {
|
||||
|
||||
@@ -487,6 +487,7 @@ struct LineMeta {
|
||||
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
|
||||
QString offsetText;
|
||||
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
|
||||
uint64_t ptrBase = 0; // Pointer expansion base (non-zero = use for RVA)
|
||||
uint32_t markerMask = 0;
|
||||
bool dataChanged = false; // true if any byte in this node changed since last refresh
|
||||
int heatLevel = 0; // 0=static, 1=cold, 2=warm, 3=hot (from ValueHistory)
|
||||
|
||||
76
src/disasm.cpp
Normal file
76
src/disasm.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "disasm.h"
|
||||
|
||||
extern "C" {
|
||||
#include <fadec.h>
|
||||
}
|
||||
|
||||
namespace rcx {
|
||||
|
||||
QString disassemble(const QByteArray& bytes, uint64_t baseAddr, int bitness, int maxBytes) {
|
||||
if (bytes.isEmpty() || (bitness != 32 && bitness != 64))
|
||||
return {};
|
||||
|
||||
int len = qMin((int)bytes.size(), maxBytes);
|
||||
const auto* buf = reinterpret_cast<const uint8_t*>(bytes.constData());
|
||||
|
||||
QString result;
|
||||
int off = 0;
|
||||
while (off < len) {
|
||||
FdInstr instr;
|
||||
int ret = fd_decode(buf + off, len - off, bitness, baseAddr + off, &instr);
|
||||
if (ret < 0)
|
||||
break;
|
||||
|
||||
char fmtBuf[128];
|
||||
fd_format(&instr, fmtBuf, sizeof(fmtBuf));
|
||||
|
||||
if (!result.isEmpty())
|
||||
result += QLatin1Char('\n');
|
||||
result += QStringLiteral("%1 %2")
|
||||
.arg(baseAddr + off, bitness == 64 ? 16 : 8, 16, QLatin1Char('0'))
|
||||
.arg(QString::fromLatin1(fmtBuf));
|
||||
|
||||
off += ret;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString hexDump(const QByteArray& bytes, uint64_t baseAddr, int maxBytes) {
|
||||
if (bytes.isEmpty())
|
||||
return {};
|
||||
|
||||
int len = qMin((int)bytes.size(), maxBytes);
|
||||
QString result;
|
||||
|
||||
for (int off = 0; off < len; off += 16) {
|
||||
int lineLen = qMin(16, len - off);
|
||||
|
||||
if (!result.isEmpty())
|
||||
result += QLatin1Char('\n');
|
||||
|
||||
// Address
|
||||
bool wide = (baseAddr + len > 0xFFFFFFFFULL);
|
||||
result += QStringLiteral("%1 ").arg(baseAddr + off, wide ? 16 : 8, 16, QLatin1Char('0'));
|
||||
|
||||
// Hex bytes
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (i < lineLen) {
|
||||
uint8_t b = static_cast<uint8_t>(bytes[off + i]);
|
||||
result += QStringLiteral("%1 ").arg(b, 2, 16, QLatin1Char('0'));
|
||||
} else {
|
||||
result += QStringLiteral(" ");
|
||||
}
|
||||
if (i == 7) result += QLatin1Char(' ');
|
||||
}
|
||||
|
||||
// ASCII
|
||||
result += QLatin1Char(' ');
|
||||
for (int i = 0; i < lineLen; i++) {
|
||||
char c = bytes[off + i];
|
||||
result += (c >= 0x20 && c < 0x7f) ? QLatin1Char(c) : QLatin1Char('.');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
15
src/disasm.h
Normal file
15
src/disasm.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
#include <cstdint>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Disassemble up to maxBytes of x86 code, returning formatted asm lines.
|
||||
// bitness: 32 or 64. Returns one line per instruction, prefixed with offset.
|
||||
QString disassemble(const QByteArray& bytes, uint64_t baseAddr, int bitness, int maxBytes = 128);
|
||||
|
||||
// Format bytes as hex dump lines (16 bytes per line with ASCII sidebar).
|
||||
QString hexDump(const QByteArray& bytes, uint64_t baseAddr, int maxBytes = 128);
|
||||
|
||||
} // namespace rcx
|
||||
328
src/editor.cpp
328
src/editor.cpp
@@ -1,4 +1,5 @@
|
||||
#include "editor.h"
|
||||
#include "disasm.h"
|
||||
#include "providerregistry.h"
|
||||
#include <QDebug>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
@@ -131,7 +132,6 @@ public:
|
||||
}
|
||||
|
||||
void showAt(const QPoint& globalPos) {
|
||||
if (isVisible()) return;
|
||||
QSize sz = sizeHint();
|
||||
QRect screen = QApplication::screenAt(globalPos)
|
||||
? QApplication::screenAt(globalPos)->availableGeometry()
|
||||
@@ -141,7 +141,7 @@ public:
|
||||
if (y + sz.height() > screen.bottom())
|
||||
y = globalPos.y() - sz.height() - 4;
|
||||
move(x, y);
|
||||
show();
|
||||
if (!isVisible()) show();
|
||||
}
|
||||
|
||||
void dismiss() {
|
||||
@@ -152,6 +152,106 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// ── Disassembly / hex-dump hover popup ──
|
||||
|
||||
class DisasmPopup : public QFrame {
|
||||
uint64_t m_nodeId = 0;
|
||||
QString m_body;
|
||||
QLabel* m_titleLabel = nullptr;
|
||||
QLabel* m_bodyLabel = nullptr;
|
||||
public:
|
||||
explicit DisasmPopup(QWidget* parent)
|
||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setAutoFillBackground(true);
|
||||
|
||||
auto* vbox = new QVBoxLayout(this);
|
||||
vbox->setContentsMargins(8, 6, 8, 6);
|
||||
vbox->setSpacing(2);
|
||||
|
||||
m_titleLabel = new QLabel;
|
||||
QFont bold = m_titleLabel->font();
|
||||
bold.setBold(true);
|
||||
m_titleLabel->setFont(bold);
|
||||
vbox->addWidget(m_titleLabel);
|
||||
|
||||
auto* sep = new QFrame;
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setFrameShadow(QFrame::Plain);
|
||||
sep->setFixedHeight(1);
|
||||
vbox->addWidget(sep);
|
||||
|
||||
m_bodyLabel = new QLabel;
|
||||
m_bodyLabel->setTextFormat(Qt::PlainText);
|
||||
m_bodyLabel->setWordWrap(false);
|
||||
vbox->addWidget(m_bodyLabel);
|
||||
}
|
||||
|
||||
uint64_t nodeId() const { return m_nodeId; }
|
||||
|
||||
void populate(uint64_t nodeId, const QString& title, const QString& body,
|
||||
const QFont& font) {
|
||||
if (nodeId == m_nodeId && body == m_body && isVisible())
|
||||
return;
|
||||
|
||||
m_nodeId = nodeId;
|
||||
m_body = body;
|
||||
|
||||
const auto& theme = ThemeManager::instance().current();
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
setPalette(pal);
|
||||
|
||||
QFont bold = font;
|
||||
bold.setBold(true);
|
||||
m_titleLabel->setFont(bold);
|
||||
m_titleLabel->setText(title);
|
||||
m_titleLabel->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(theme.text.name()));
|
||||
|
||||
// Find and style the separator
|
||||
for (auto* child : findChildren<QFrame*>()) {
|
||||
if (child->frameShape() == QFrame::HLine) {
|
||||
QPalette sp;
|
||||
sp.setColor(QPalette::WindowText, theme.border);
|
||||
child->setPalette(sp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_bodyLabel->setFont(font);
|
||||
m_bodyLabel->setText(body);
|
||||
m_bodyLabel->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(theme.syntaxNumber.name()));
|
||||
|
||||
setMaximumWidth(600);
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void showAt(const QPoint& globalPos) {
|
||||
QSize sz = sizeHint();
|
||||
QRect screen = QApplication::screenAt(globalPos)
|
||||
? QApplication::screenAt(globalPos)->availableGeometry()
|
||||
: QRect(0, 0, 1920, 1080);
|
||||
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
||||
int y = globalPos.y();
|
||||
if (y + sz.height() > screen.bottom())
|
||||
y = globalPos.y() - sz.height() - 4;
|
||||
move(x, y);
|
||||
if (!isVisible()) show();
|
||||
}
|
||||
|
||||
void dismiss() {
|
||||
if (isVisible()) hide();
|
||||
m_nodeId = 0;
|
||||
m_body.clear();
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr int IND_EDITABLE = 8;
|
||||
static constexpr int IND_HEX_DIM = 9;
|
||||
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
||||
@@ -570,6 +670,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
applyFoldLevels(result.meta);
|
||||
applyHexDimming(result.meta);
|
||||
applyHeatmapHighlight(result.meta);
|
||||
applySymbolColoring(result.meta);
|
||||
applyCommandRowPills();
|
||||
|
||||
// Reset hint line - applySelectionOverlay will repaint indicators
|
||||
@@ -626,7 +727,8 @@ void RcxEditor::reformatMargins() {
|
||||
lm.lineKind == LineKind::CommandRow) {
|
||||
lm.offsetText = QString(hexDigits + 1, ' ');
|
||||
} else {
|
||||
uint64_t rel = lm.offsetAddr >= base ? lm.offsetAddr - base : 0;
|
||||
uint64_t rvaBase = lm.ptrBase ? lm.ptrBase : base;
|
||||
uint64_t rel = lm.offsetAddr >= rvaBase ? lm.offsetAddr - rvaBase : 0;
|
||||
lm.offsetText = (QStringLiteral("+") +
|
||||
QString::number(rel, 16).toUpper())
|
||||
.rightJustified(hexDigits, ' ') + QChar(' ');
|
||||
@@ -663,17 +765,22 @@ void RcxEditor::reformatMargins() {
|
||||
};
|
||||
|
||||
if (m_relativeOffsets) {
|
||||
// Derive local offset: find enclosing header or array element separator
|
||||
// Derive local offset: for pointer-expanded children use ptrBase,
|
||||
// otherwise find enclosing header or array element separator
|
||||
uint64_t parentAddr = base;
|
||||
for (int j = i - 1; j >= 0; j--) {
|
||||
const auto& pLm = m_meta[j];
|
||||
if (pLm.lineKind == LineKind::Header && pLm.depth < lm.depth) {
|
||||
parentAddr = pLm.offsetAddr;
|
||||
break;
|
||||
}
|
||||
if (pLm.lineKind == LineKind::ArrayElementSeparator && pLm.depth <= lm.depth) {
|
||||
parentAddr = pLm.offsetAddr;
|
||||
break;
|
||||
if (lm.ptrBase != 0) {
|
||||
parentAddr = lm.ptrBase;
|
||||
} else {
|
||||
for (int j = i - 1; j >= 0; j--) {
|
||||
const auto& pLm = m_meta[j];
|
||||
if (pLm.lineKind == LineKind::Header && pLm.depth < lm.depth) {
|
||||
parentAddr = pLm.offsetAddr;
|
||||
break;
|
||||
}
|
||||
if (pLm.lineKind == LineKind::ArrayElementSeparator && pLm.depth <= lm.depth) {
|
||||
parentAddr = pLm.offsetAddr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint64_t localOff = lm.offsetAddr >= parentAddr ? lm.offsetAddr - parentAddr : 0;
|
||||
@@ -908,6 +1015,22 @@ ColumnSpan RcxEditor::typeSpan(const LineMeta& lm, int typeW) { return typeSpan
|
||||
ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int typeW, int nameW) { return nameSpanFor(lm, typeW, nameW); }
|
||||
ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int typeW, int nameW) { return valueSpanFor(lm, lineLength, typeW, nameW); }
|
||||
|
||||
// For pointer-like nodes, narrow value span to just the address portion
|
||||
// (before the " // " separator that precedes the symbol like "Module+0x1234").
|
||||
static ColumnSpan narrowPtrValueSpan(const LineMeta& lm, const ColumnSpan& vs,
|
||||
const QString& lineText) {
|
||||
if (!vs.valid) return vs;
|
||||
if (!isFuncPtr(lm.nodeKind)
|
||||
&& lm.nodeKind != NodeKind::Pointer32
|
||||
&& lm.nodeKind != NodeKind::Pointer64)
|
||||
return vs;
|
||||
QString valText = lineText.mid(vs.start, vs.end - vs.start);
|
||||
int sep = valText.indexOf(QLatin1String(" // "));
|
||||
if (sep > 0)
|
||||
return {vs.start, vs.start + sep, true};
|
||||
return vs;
|
||||
}
|
||||
|
||||
// ── Multi-selection ──
|
||||
|
||||
QSet<int> RcxEditor::selectedNodeIndices() const {
|
||||
@@ -956,28 +1079,10 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
|
||||
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
||||
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
||||
|
||||
// For hex preview nodes: per-byte heat coloring on changed bytes
|
||||
if (isHexPreview(lm.nodeKind) && lm.dataChanged && !lm.changedByteIndices.isEmpty()) {
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int asciiStart = ind + typeW + kSepWidth;
|
||||
int hexStart = asciiStart + nameW + kSepWidth;
|
||||
|
||||
for (int byteIdx : lm.changedByteIndices) {
|
||||
fillIndicatorCols(activeInd, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
|
||||
int hexCol = hexStart + byteIdx * 3;
|
||||
fillIndicatorCols(activeInd, i, hexCol, hexCol + 2);
|
||||
}
|
||||
// Clear the other two heat indicators on this line
|
||||
for (int hi : heatIndicators) {
|
||||
if (hi != activeInd)
|
||||
clearIndicatorLine(hi, i);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-hex nodes: apply heat-level indicator to value span
|
||||
// Apply heat-level indicator to value span (narrowed for pointer-like nodes)
|
||||
QString lineText = getLineText(m_sci, i);
|
||||
ColumnSpan vs = valueSpan(lm, lineText.size(), typeW, nameW);
|
||||
ColumnSpan vs = narrowPtrValueSpan(lm,
|
||||
valueSpan(lm, lineText.size(), typeW, nameW), lineText);
|
||||
if (!vs.valid) continue;
|
||||
|
||||
fillIndicatorCols(activeInd, i, vs.start, vs.end);
|
||||
@@ -990,6 +1095,28 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::applySymbolColoring(const QVector<LineMeta>& meta) {
|
||||
for (int i = 0; i < meta.size(); i++) {
|
||||
const LineMeta& lm = meta[i];
|
||||
if (!isFuncPtr(lm.nodeKind)
|
||||
&& lm.nodeKind != NodeKind::Pointer32
|
||||
&& lm.nodeKind != NodeKind::Pointer64)
|
||||
continue;
|
||||
QString lineText = getLineText(m_sci, i);
|
||||
// Find " // " within the value region and color "// sym" portion green
|
||||
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
||||
if (!vs.valid) continue;
|
||||
int searchFrom = vs.start;
|
||||
int sep = lineText.indexOf(QLatin1String(" // "), searchFrom);
|
||||
if (sep < 0 || sep >= vs.end) continue;
|
||||
int symStart = sep + 2; // start of "// sym"
|
||||
int symEnd = vs.end;
|
||||
while (symEnd > symStart && lineText[symEnd - 1] == ' ') symEnd--;
|
||||
if (symEnd > symStart)
|
||||
fillIndicatorCols(IND_HINT_GREEN, i, symStart, symEnd);
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::applyBaseAddressColoring(const QVector<LineMeta>& meta) {
|
||||
if (meta.isEmpty() || meta[0].lineKind != LineKind::CommandRow) return;
|
||||
|
||||
@@ -1354,7 +1481,8 @@ static bool hitTestTarget(QsciScintilla* sci,
|
||||
|
||||
ColumnSpan ts = RcxEditor::typeSpan(lm, typeW);
|
||||
ColumnSpan ns = RcxEditor::nameSpan(lm, typeW, nameW);
|
||||
ColumnSpan vs = RcxEditor::valueSpan(lm, textLen, typeW, nameW);
|
||||
ColumnSpan vs = narrowPtrValueSpan(lm,
|
||||
RcxEditor::valueSpan(lm, textLen, typeW, nameW), lineText);
|
||||
|
||||
// Pointer fields/headers: check sub-spans within type column first
|
||||
if (lm.nodeKind == NodeKind::Pointer32 || lm.nodeKind == NodeKind::Pointer64) {
|
||||
@@ -2440,14 +2568,19 @@ void RcxEditor::applyHoverCursor() {
|
||||
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
// Always dismiss disasm popup during inline editing
|
||||
if (m_disasmPopup && m_disasmPopup->isVisible())
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// Mouse left viewport - set Arrow, dismiss history popup
|
||||
// Mouse left viewport - set Arrow, dismiss popups
|
||||
// (but not during applyDocument — the Leave is synthetic from setText)
|
||||
if (!m_hoverInside) {
|
||||
if (m_historyPopup && !m_applyingDocument)
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
if (m_disasmPopup && !m_applyingDocument)
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
@@ -2522,6 +2655,18 @@ void RcxEditor::applyHoverCursor() {
|
||||
m_hoverSpanLines.append(line);
|
||||
}
|
||||
}
|
||||
// Narrow pointer-like nodes to address portion only (exclude symbol)
|
||||
if (!narrowed && (isFuncPtr(lm.nodeKind)
|
||||
|| lm.nodeKind == NodeKind::Pointer32
|
||||
|| lm.nodeKind == NodeKind::Pointer64)) {
|
||||
ColumnSpan full = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
||||
ColumnSpan narrow = narrowPtrValueSpan(lm, full, lineText);
|
||||
if (h.col >= narrow.start && h.col < narrow.end) {
|
||||
fillIndicatorCols(IND_HOVER_SPAN, line, narrow.start, narrow.end);
|
||||
m_hoverSpanLines.append(line);
|
||||
}
|
||||
narrowed = true;
|
||||
}
|
||||
}
|
||||
if (!narrowed && h.col >= span.start && h.col < span.end) {
|
||||
fillIndicatorCols(IND_HOVER_SPAN, line, span.start, span.end);
|
||||
@@ -2537,11 +2682,16 @@ void RcxEditor::applyHoverCursor() {
|
||||
}
|
||||
|
||||
// Value history popup on hover (read-only, no buttons)
|
||||
// Skip FuncPtr and void-Pointer nodes — they use the disasm popup instead.
|
||||
{
|
||||
bool showPopup = false;
|
||||
if (m_valueHistory && h.line >= 0 && h.line < m_meta.size()) {
|
||||
const LineMeta& lm = m_meta[h.line];
|
||||
if (lm.heatLevel > 0 && lm.nodeId != 0) {
|
||||
bool skipForDisasm = isFuncPtr(lm.nodeKind)
|
||||
|| ((lm.nodeKind == NodeKind::Pointer32
|
||||
|| lm.nodeKind == NodeKind::Pointer64)
|
||||
&& lm.pointerTargetName.isEmpty());
|
||||
if (lm.heatLevel > 0 && lm.nodeId != 0 && !skipForDisasm) {
|
||||
auto it = m_valueHistory->find(lm.nodeId);
|
||||
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
|
||||
QString lineText = getLineText(m_sci, h.line);
|
||||
@@ -2571,6 +2721,110 @@ void RcxEditor::applyHoverCursor() {
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
|
||||
// Disasm / hex-dump popup on hover for FuncPtr and void Pointer nodes
|
||||
{
|
||||
bool showDisasm = false;
|
||||
if (m_disasmProvider && m_disasmTree && h.line >= 0 && h.line < m_meta.size()) {
|
||||
const LineMeta& lm = m_meta[h.line];
|
||||
bool isFP = isFuncPtr(lm.nodeKind);
|
||||
bool isVoidPtr = (lm.nodeKind == NodeKind::Pointer32
|
||||
|| lm.nodeKind == NodeKind::Pointer64)
|
||||
&& lm.pointerTargetName.isEmpty();
|
||||
if ((isFP || isVoidPtr) && lm.nodeIdx >= 0
|
||||
&& lm.nodeIdx < m_disasmTree->nodes.size()) {
|
||||
// Check hover is over the address portion of the value column
|
||||
QString lineText = getLineText(m_sci, h.line);
|
||||
ColumnSpan vs = narrowPtrValueSpan(lm,
|
||||
valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW),
|
||||
lineText);
|
||||
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
|
||||
const Node& node = m_disasmTree->nodes[lm.nodeIdx];
|
||||
// For void ptrs, only show hex dump if refId == 0
|
||||
if (!isVoidPtr || node.refId == 0) {
|
||||
bool is64 = (lm.nodeKind == NodeKind::FuncPtr64
|
||||
|| lm.nodeKind == NodeKind::Pointer64);
|
||||
// Use composed address (correct for pointer-expanded nodes)
|
||||
// not node.offset (which is just offset within struct definition).
|
||||
uint64_t provAddr = lm.offsetAddr >= m_disasmTree->baseAddress
|
||||
? lm.offsetAddr - m_disasmTree->baseAddress
|
||||
: static_cast<uint64_t>(node.offset);
|
||||
uint64_t ptrVal = is64
|
||||
? m_disasmProvider->readU64(provAddr)
|
||||
: (uint64_t)m_disasmProvider->readU32(provAddr);
|
||||
if (ptrVal != 0 && ptrVal != UINT64_MAX
|
||||
&& !(is64 == false && ptrVal == 0xFFFFFFFF)) {
|
||||
// Read code bytes from the function target address.
|
||||
// Use the real provider (not snapshot) because function
|
||||
// code lives at arbitrary process addresses that aren't
|
||||
// in the snapshot page table. The provider reads from
|
||||
// m_base + addr via ReadProcessMemory, so we convert
|
||||
// the absolute ptrVal to provider-relative.
|
||||
const Provider* codeProv = m_disasmRealProv
|
||||
? m_disasmRealProv : m_disasmProvider;
|
||||
constexpr int kMaxRead = 128;
|
||||
uint64_t codeAddr = ptrVal - m_disasmTree->baseAddress;
|
||||
QByteArray bytes(kMaxRead, Qt::Uninitialized);
|
||||
bool readOk = codeProv->read(codeAddr, bytes.data(), kMaxRead);
|
||||
if (readOk) {
|
||||
QString title, body;
|
||||
if (isFP) {
|
||||
title = QStringLiteral("Disassembly");
|
||||
body = disassemble(bytes, ptrVal,
|
||||
is64 ? 64 : 32, kMaxRead);
|
||||
} else {
|
||||
title = QStringLiteral("Hex Dump");
|
||||
body = hexDump(bytes, ptrVal, kMaxRead);
|
||||
}
|
||||
// Cap at 6 lines so the popup stays compact
|
||||
{
|
||||
const int kMaxLines = 6;
|
||||
int nth = 0, idx = 0;
|
||||
while (nth < kMaxLines && (idx = body.indexOf('\n', idx)) != -1)
|
||||
{ ++nth; ++idx; }
|
||||
if (nth == kMaxLines && idx < body.size()) {
|
||||
body.truncate(idx);
|
||||
body += QStringLiteral("...");
|
||||
}
|
||||
}
|
||||
if (!body.isEmpty()) {
|
||||
if (!m_disasmPopup)
|
||||
m_disasmPopup = new DisasmPopup(this);
|
||||
auto* popup = static_cast<DisasmPopup*>(
|
||||
m_disasmPopup);
|
||||
popup->populate(lm.nodeId, title, body,
|
||||
editorFont());
|
||||
long linePos = m_sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||
(unsigned long)h.line);
|
||||
long byteOff = lineText.left(vs.start)
|
||||
.toUtf8().size();
|
||||
int px = (int)m_sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
||||
(unsigned long)0, linePos + byteOff);
|
||||
int py = (int)m_sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
||||
(unsigned long)0, linePos);
|
||||
int lh = (int)m_sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_TEXTHEIGHT,
|
||||
(unsigned long)h.line);
|
||||
QPoint anchor = m_sci->viewport()->mapToGlobal(
|
||||
QPoint(px, py + lh));
|
||||
popup->showAt(anchor);
|
||||
showDisasm = true;
|
||||
// Dismiss value history popup to avoid fighting
|
||||
if (m_historyPopup && m_historyPopup->isVisible())
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!showDisasm && m_disasmPopup && m_disasmPopup->isVisible())
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||
}
|
||||
|
||||
// Determine cursor shape based on interaction type
|
||||
Qt::CursorShape desired = Qt::ArrowCursor;
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ public:
|
||||
QString textWithMargins() const;
|
||||
void setCustomTypeNames(const QStringList& names);
|
||||
void setValueHistoryRef(const QHash<uint64_t, ValueHistory>* ref) { m_valueHistory = ref; }
|
||||
void setProviderRef(const Provider* prov, const Provider* realProv, const NodeTree* tree) {
|
||||
m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree;
|
||||
}
|
||||
|
||||
// Saved sources for quick-switch in source picker
|
||||
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
||||
@@ -133,6 +136,10 @@ private:
|
||||
// ── Value history ref (owned by controller) ──
|
||||
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
|
||||
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
|
||||
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
|
||||
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
|
||||
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||
const NodeTree* m_disasmTree = nullptr;
|
||||
|
||||
// ── Reentrancy guards ──
|
||||
bool m_applyingDocument = false;
|
||||
@@ -152,6 +159,7 @@ private:
|
||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||
void applyHeatmapHighlight(const QVector<LineMeta>& meta);
|
||||
void applySymbolColoring(const QVector<LineMeta>& meta);
|
||||
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||
void applyCommandRowPills();
|
||||
|
||||
|
||||
1316
src/examples/KUSER_SHARED_DATA.rcx
Normal file
1316
src/examples/KUSER_SHARED_DATA.rcx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -262,7 +262,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
if (!display) return rawHex(val, 8);
|
||||
QString s = fmtPointer32(val);
|
||||
QString sym = prov.getSymbol((uint64_t)val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::Pointer64: {
|
||||
@@ -270,7 +270,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
if (!display) return rawHex(val, 16);
|
||||
QString s = fmtPointer64(val);
|
||||
QString sym = prov.getSymbol(val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::FuncPtr32: {
|
||||
@@ -278,7 +278,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
if (!display) return rawHex(val, 8);
|
||||
QString s = fmtPointer32(val);
|
||||
QString sym = prov.getSymbol((uint64_t)val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::FuncPtr64: {
|
||||
@@ -286,7 +286,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
if (!display) return rawHex(val, 16);
|
||||
QString s = fmtPointer64(val);
|
||||
QString sym = prov.getSymbol(val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::Vec2:
|
||||
|
||||
102
src/main.cpp
102
src/main.cpp
@@ -808,24 +808,96 @@ void MainWindow::newDocument() {
|
||||
rebuildWorkspaceModel();
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
#ifdef Q_OS_WIN
|
||||
// Auto-open KUSER_SHARED_DATA example if available
|
||||
QString exPath = QCoreApplication::applicationDirPath()
|
||||
+ "/examples/KUSER_SHARED_DATA.rcx";
|
||||
if (QFile::exists(exPath)) {
|
||||
project_open(exPath);
|
||||
} else {
|
||||
project_new();
|
||||
static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) {
|
||||
tree.nodes.clear();
|
||||
tree.invalidateIdCache();
|
||||
tree.m_nextId = 1;
|
||||
tree.baseAddress = static_cast<uint64_t>(editorAddr);
|
||||
|
||||
// ── Root struct: RcxEditor ──
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = QStringLiteral("editor");
|
||||
root.structTypeName = QStringLiteral("RcxEditor");
|
||||
root.classKeyword = QStringLiteral("class");
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// ── VTable struct definition (separate root) ──
|
||||
Node vtStruct;
|
||||
vtStruct.kind = NodeKind::Struct;
|
||||
vtStruct.name = QStringLiteral("VTable");
|
||||
vtStruct.structTypeName = QStringLiteral("QWidgetVTable");
|
||||
int vti = tree.addNode(vtStruct);
|
||||
uint64_t vtId = tree.nodes[vti].id;
|
||||
|
||||
// VTable entries — these are real virtual function pointers from QObject/QWidget
|
||||
static const char* vfNames[] = {
|
||||
"deleting_dtor", "metaObject", "qt_metacast", "qt_metacall",
|
||||
"event", "eventFilter", "timerEvent", "childEvent",
|
||||
"customEvent", "connectNotify", "disconnectNotify", "devType",
|
||||
"setVisible", "sizeHint", "minimumSizeHint", "heightForWidth",
|
||||
};
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Node fn;
|
||||
fn.kind = NodeKind::FuncPtr64;
|
||||
fn.name = QString::fromLatin1(vfNames[i]);
|
||||
fn.parentId = vtId;
|
||||
fn.offset = i * 8;
|
||||
tree.addNode(fn);
|
||||
}
|
||||
|
||||
// Auto-attach process memory plugin to self
|
||||
auto* ctrl = activeController();
|
||||
if (ctrl) {
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
QString target = QString("%1:Reclass.exe").arg(pid);
|
||||
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
|
||||
// ── RcxEditor fields ──
|
||||
// offset 0: vtable pointer → QWidgetVTable
|
||||
{
|
||||
Node n;
|
||||
n.kind = NodeKind::Pointer64;
|
||||
n.name = QStringLiteral("__vptr");
|
||||
n.parentId = rootId;
|
||||
n.offset = 0;
|
||||
n.refId = vtId;
|
||||
tree.addNode(n);
|
||||
}
|
||||
// offset 8: QObjectData* d_ptr (QObject internals)
|
||||
{
|
||||
Node n;
|
||||
n.kind = NodeKind::Pointer64;
|
||||
n.name = QStringLiteral("d_ptr");
|
||||
n.parentId = rootId;
|
||||
n.offset = 8;
|
||||
tree.addNode(n);
|
||||
}
|
||||
// The rest of the object: raw memory visible as Hex64 fields
|
||||
// QWidget base is large (~200+ bytes), then RcxEditor members follow.
|
||||
// Lay out enough to cover the interesting editor state.
|
||||
for (int off = 16; off < 512; off += 8) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Hex64;
|
||||
n.name = QStringLiteral("field_%1").arg(off, 3, 16, QLatin1Char('0'));
|
||||
n.parentId = rootId;
|
||||
n.offset = off;
|
||||
tree.addNode(n);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
#ifdef Q_OS_WIN
|
||||
// Create a new project, then point it at the live editor object
|
||||
project_new();
|
||||
|
||||
auto* ctrl = activeController();
|
||||
if (!ctrl || ctrl->editors().isEmpty()) return;
|
||||
|
||||
auto* editor = ctrl->editors().first();
|
||||
auto* doc = ctrl->document();
|
||||
|
||||
// Build a tree describing RcxEditor, based at the real object address
|
||||
buildEditorDemo(doc->tree, reinterpret_cast<uintptr_t>(editor));
|
||||
|
||||
// Attach process memory to self — provider base will be set to the editor address
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
QString target = QString("%1:Reclass.exe").arg(pid);
|
||||
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
|
||||
#else
|
||||
project_new();
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user