mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add async auto-refresh with change detection and self-test harness
BBB
This commit is contained in:
@@ -7,7 +7,7 @@ set(CMAKE_AUTOMOC ON)
|
|||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Widgets PrintSupport Svg)
|
find_package(Qt6 REQUIRED COMPONENTS Widgets PrintSupport Svg Concurrent)
|
||||||
|
|
||||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||||
find_package(QScintilla REQUIRED)
|
find_package(QScintilla REQUIRED)
|
||||||
@@ -27,7 +27,7 @@ add_executable(ReclassX
|
|||||||
src/processpicker.ui
|
src/processpicker.ui
|
||||||
src/resources.qrc
|
src/resources.qrc
|
||||||
src/core.h
|
src/core.h
|
||||||
src/providers/buffer_provider.h src/providers/null_provider.h src/providers/process_provider.h src/providers/provider.h
|
src/providers/buffer_provider.h src/providers/null_provider.h src/providers/process_provider.h src/providers/provider.h src/providers/snapshot_provider.h
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(ReclassX PRIVATE src)
|
target_include_directories(ReclassX PRIVATE src)
|
||||||
@@ -36,6 +36,7 @@ target_link_libraries(ReclassX PRIVATE
|
|||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
Qt6::PrintSupport
|
Qt6::PrintSupport
|
||||||
Qt6::Svg
|
Qt6::Svg
|
||||||
|
Qt6::Concurrent
|
||||||
QScintilla::QScintilla
|
QScintilla::QScintilla
|
||||||
dbghelp
|
dbghelp
|
||||||
psapi
|
psapi
|
||||||
@@ -125,7 +126,7 @@ if(BUILD_TESTING)
|
|||||||
src/processpicker.cpp src/processpicker.ui)
|
src/processpicker.cpp src/processpicker.ui)
|
||||||
target_include_directories(test_controller PRIVATE src)
|
target_include_directories(test_controller PRIVATE src)
|
||||||
target_link_libraries(test_controller PRIVATE
|
target_link_libraries(test_controller PRIVATE
|
||||||
Qt6::Widgets Qt6::PrintSupport Qt6::Test
|
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||||
QScintilla::QScintilla dbghelp psapi)
|
QScintilla::QScintilla dbghelp psapi)
|
||||||
add_test(NAME test_controller COMMAND test_controller)
|
add_test(NAME test_controller COMMAND test_controller)
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ if(BUILD_TESTING)
|
|||||||
src/processpicker.cpp src/processpicker.ui)
|
src/processpicker.cpp src/processpicker.ui)
|
||||||
target_include_directories(test_validation PRIVATE src)
|
target_include_directories(test_validation PRIVATE src)
|
||||||
target_link_libraries(test_validation PRIVATE
|
target_link_libraries(test_validation PRIVATE
|
||||||
Qt6::Widgets Qt6::PrintSupport Qt6::Test
|
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||||
QScintilla::QScintilla dbghelp psapi)
|
QScintilla::QScintilla dbghelp psapi)
|
||||||
add_test(NAME test_validation COMMAND test_validation)
|
add_test(NAME test_validation COMMAND test_validation)
|
||||||
|
|
||||||
@@ -149,7 +150,7 @@ if(BUILD_TESTING)
|
|||||||
src/processpicker.cpp src/processpicker.ui)
|
src/processpicker.cpp src/processpicker.ui)
|
||||||
target_include_directories(test_context_menu PRIVATE src)
|
target_include_directories(test_context_menu PRIVATE src)
|
||||||
target_link_libraries(test_context_menu PRIVATE
|
target_link_libraries(test_context_menu PRIVATE
|
||||||
Qt6::Widgets Qt6::PrintSupport Qt6::Test
|
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||||
QScintilla::QScintilla dbghelp psapi)
|
QScintilla::QScintilla dbghelp psapi)
|
||||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.isContinuation = isCont;
|
lm.isContinuation = isCont;
|
||||||
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
|
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont);
|
||||||
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
||||||
lm.foldLevel = computeFoldLevel(depth, false);
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
lm.effectiveTypeW = typeW;
|
lm.effectiveTypeW = typeW;
|
||||||
@@ -178,7 +178,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = LineKind::Field;
|
lm.lineKind = LineKind::Field;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
||||||
lm.foldLevel = computeFoldLevel(depth, false);
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
@@ -195,7 +195,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = LineKind::ArrayElementSeparator;
|
lm.lineKind = LineKind::ArrayElementSeparator;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.foldLevel = computeFoldLevel(depth, false);
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
lm.markerMask = 0;
|
lm.markerMask = 0;
|
||||||
@@ -214,7 +214,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = LineKind::Header;
|
lm.lineKind = LineKind::Header;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.foldHead = true;
|
lm.foldHead = true;
|
||||||
lm.foldCollapsed = node.collapsed;
|
lm.foldCollapsed = node.collapsed;
|
||||||
@@ -300,7 +300,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header;
|
lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.foldHead = true;
|
lm.foldHead = true;
|
||||||
lm.foldCollapsed = node.collapsed;
|
lm.foldCollapsed = node.collapsed;
|
||||||
@@ -401,8 +401,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
|||||||
// Include struct/array names - they now use columnar layout too
|
// Include struct/array names - they now use columnar layout too
|
||||||
int maxNameLen = kMinNameW;
|
int maxNameLen = kMinNameW;
|
||||||
for (const Node& node : tree.nodes) {
|
for (const Node& node : tree.nodes) {
|
||||||
// Skip padding (it shows ASCII preview, not name column)
|
// Skip hex/padding (they show ASCII preview, not name column)
|
||||||
if (node.kind == NodeKind::Padding) continue;
|
if (isHexPreview(node.kind)) continue;
|
||||||
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
||||||
}
|
}
|
||||||
state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
|
state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
|
||||||
@@ -420,8 +420,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
|||||||
const Node& child = tree.nodes[childIdx];
|
const Node& child = tree.nodes[childIdx];
|
||||||
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
||||||
|
|
||||||
// Name width (skip padding, but include hex and containers)
|
// Name width (skip hex/padding, but include containers)
|
||||||
if (child.kind != NodeKind::Padding) {
|
if (!isHexPreview(child.kind)) {
|
||||||
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
|
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,8 +439,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
|||||||
const Node& child = tree.nodes[childIdx];
|
const Node& child = tree.nodes[childIdx];
|
||||||
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
||||||
|
|
||||||
// Name width (skip padding, include hex and containers)
|
// Name width (skip hex/padding, include containers)
|
||||||
if (child.kind != NodeKind::Padding) {
|
if (!isHexPreview(child.kind)) {
|
||||||
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QtConcurrent/QtConcurrentRun>
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <psapi.h>
|
#include <psapi.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -55,7 +56,7 @@ static QString crumbFor(const rcx::NodeTree& t, uint64_t nodeId) {
|
|||||||
|
|
||||||
RcxDocument::RcxDocument(QObject* parent)
|
RcxDocument::RcxDocument(QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, provider(std::make_unique<NullProvider>())
|
, provider(std::make_shared<NullProvider>())
|
||||||
{
|
{
|
||||||
connect(&undoStack, &QUndoStack::cleanChanged, this, [this](bool clean) {
|
connect(&undoStack, &QUndoStack::cleanChanged, this, [this](bool clean) {
|
||||||
modified = !clean;
|
modified = !clean;
|
||||||
@@ -97,7 +98,7 @@ void RcxDocument::loadData(const QString& binaryPath) {
|
|||||||
if (!file.open(QIODevice::ReadOnly))
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
return;
|
return;
|
||||||
undoStack.clear();
|
undoStack.clear();
|
||||||
provider = std::make_unique<BufferProvider>(
|
provider = std::make_shared<BufferProvider>(
|
||||||
file.readAll(), QFileInfo(binaryPath).fileName());
|
file.readAll(), QFileInfo(binaryPath).fileName());
|
||||||
dataPath = binaryPath;
|
dataPath = binaryPath;
|
||||||
tree.baseAddress = 0;
|
tree.baseAddress = 0;
|
||||||
@@ -106,7 +107,7 @@ void RcxDocument::loadData(const QString& binaryPath) {
|
|||||||
|
|
||||||
void RcxDocument::loadData(const QByteArray& data) {
|
void RcxDocument::loadData(const QByteArray& data) {
|
||||||
undoStack.clear();
|
undoStack.clear();
|
||||||
provider = std::make_unique<BufferProvider>(data);
|
provider = std::make_shared<BufferProvider>(data);
|
||||||
tree.baseAddress = 0;
|
tree.baseAddress = 0;
|
||||||
emit documentChanged();
|
emit documentChanged();
|
||||||
}
|
}
|
||||||
@@ -125,6 +126,14 @@ RcxController::RcxController(RcxDocument* doc, QWidget* parent)
|
|||||||
: QObject(parent), m_doc(doc)
|
: QObject(parent), m_doc(doc)
|
||||||
{
|
{
|
||||||
connect(m_doc, &RcxDocument::documentChanged, this, &RcxController::refresh);
|
connect(m_doc, &RcxDocument::documentChanged, this, &RcxController::refresh);
|
||||||
|
setupAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
RcxController::~RcxController() {
|
||||||
|
if (m_refreshWatcher) {
|
||||||
|
m_refreshWatcher->cancel();
|
||||||
|
m_refreshWatcher->waitForFinished();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RcxEditor* RcxController::primaryEditor() const {
|
RcxEditor* RcxController::primaryEditor() const {
|
||||||
@@ -172,8 +181,8 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
case EditTarget::Name: {
|
case EditTarget::Name: {
|
||||||
if (text.isEmpty()) break;
|
if (text.isEmpty()) break;
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||||
// ASCII edit on Padding nodes
|
// ASCII edit on Hex/Padding nodes
|
||||||
if (node.kind == NodeKind::Padding) {
|
if (isHexPreview(node.kind)) {
|
||||||
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true);
|
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true);
|
||||||
} else {
|
} else {
|
||||||
renameNode(nodeIdx, text);
|
renameNode(nodeIdx, text);
|
||||||
@@ -427,8 +436,27 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::refresh() {
|
void RcxController::refresh() {
|
||||||
|
// Compose against snapshot provider if active, otherwise real provider
|
||||||
|
if (m_snapshotProv)
|
||||||
|
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv);
|
||||||
|
else
|
||||||
m_lastResult = m_doc->compose();
|
m_lastResult = m_doc->compose();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
int sz = m_doc->tree.nodes[lm.nodeIdx].byteSize();
|
||||||
|
for (int64_t b = offset; b < offset + sz; b++) {
|
||||||
|
if (m_changedOffsets.contains(b)) {
|
||||||
|
lm.dataChanged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prune stale selections (nodes removed by undo/redo/delete)
|
// Prune stale selections (nodes removed by undo/redo/delete)
|
||||||
QSet<uint64_t> valid;
|
QSet<uint64_t> valid;
|
||||||
for (uint64_t id : m_selIds) {
|
for (uint64_t id : m_selIds) {
|
||||||
@@ -624,6 +652,11 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
tree.nodes[idx].collapsed = isUndo ? c.oldState : c.newState;
|
tree.nodes[idx].collapsed = isUndo ? c.oldState : c.newState;
|
||||||
} else if constexpr (std::is_same_v<T, cmd::Insert>) {
|
} else if constexpr (std::is_same_v<T, cmd::Insert>) {
|
||||||
if (isUndo) {
|
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);
|
int idx = tree.indexOfId(c.node.id);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
tree.nodes.remove(idx);
|
tree.nodes.remove(idx);
|
||||||
@@ -631,6 +664,11 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tree.addNode(c.node);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
|
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
|
||||||
if (isUndo) {
|
if (isUndo) {
|
||||||
@@ -661,6 +699,9 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
||||||
if (!m_doc->provider->writeBytes(c.addr, bytes))
|
if (!m_doc->provider->writeBytes(c.addr, bytes))
|
||||||
qWarning() << "WriteBytes failed at address" << Qt::hex << c.addr;
|
qWarning() << "WriteBytes failed at address" << Qt::hex << c.addr;
|
||||||
|
// Patch snapshot so compose sees the new value immediately
|
||||||
|
if (m_snapshotProv)
|
||||||
|
m_snapshotProv->patchSnapshot(c.addr, bytes.constData(), bytes.size());
|
||||||
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
|
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
|
||||||
int idx = tree.indexOfId(c.nodeId);
|
int idx = tree.indexOfId(c.nodeId);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
@@ -735,7 +776,30 @@ void RcxController::duplicateNode(int nodeIdx) {
|
|||||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||||
const Node& src = m_doc->tree.nodes[nodeIdx];
|
const Node& src = m_doc->tree.nodes[nodeIdx];
|
||||||
if (src.kind == NodeKind::Struct || src.kind == NodeKind::Array) return;
|
if (src.kind == NodeKind::Struct || src.kind == NodeKind::Array) return;
|
||||||
insertNode(src.parentId, src.offset + src.byteSize(), src.kind, src.name + "_copy");
|
|
||||||
|
int copySize = src.byteSize();
|
||||||
|
int copyOffset = src.offset + copySize;
|
||||||
|
|
||||||
|
// Shift later siblings down to make room for the copy
|
||||||
|
QVector<cmd::OffsetAdj> adjs;
|
||||||
|
if (src.parentId != 0) {
|
||||||
|
auto siblings = m_doc->tree.childrenOf(src.parentId);
|
||||||
|
for (int si : siblings) {
|
||||||
|
if (si == nodeIdx) continue;
|
||||||
|
auto& sib = m_doc->tree.nodes[si];
|
||||||
|
if (sib.offset >= copyOffset)
|
||||||
|
adjs.append({sib.id, sib.offset, sib.offset + copySize});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Node n;
|
||||||
|
n.kind = src.kind;
|
||||||
|
n.name = src.name + "_copy";
|
||||||
|
n.parentId = src.parentId;
|
||||||
|
n.offset = copyOffset;
|
||||||
|
n.id = m_doc->tree.reserveId();
|
||||||
|
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||||
@@ -1101,10 +1165,11 @@ void RcxController::attachToProcess(uint32_t pid, const QString& processName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_doc->undoStack.clear();
|
m_doc->undoStack.clear();
|
||||||
m_doc->provider = std::make_unique<ProcessProvider>(
|
m_doc->provider = std::make_shared<ProcessProvider>(
|
||||||
hProc, base, regionSize, processName);
|
hProc, base, regionSize, processName);
|
||||||
m_doc->dataPath.clear();
|
m_doc->dataPath.clear();
|
||||||
m_doc->tree.baseAddress = base;
|
m_doc->tree.baseAddress = base;
|
||||||
|
resetSnapshot();
|
||||||
emit m_doc->documentChanged();
|
emit m_doc->documentChanged();
|
||||||
refresh();
|
refresh();
|
||||||
#else
|
#else
|
||||||
@@ -1148,6 +1213,104 @@ void RcxController::pushSavedSourcesToEditors() {
|
|||||||
editor->setSavedSources(display);
|
editor->setSavedSources(display);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Auto-refresh ──
|
||||||
|
|
||||||
|
void RcxController::setupAutoRefresh() {
|
||||||
|
m_refreshTimer = new QTimer(this);
|
||||||
|
m_refreshTimer->setInterval(2000);
|
||||||
|
connect(m_refreshTimer, &QTimer::timeout, this, &RcxController::onRefreshTick);
|
||||||
|
m_refreshTimer->start();
|
||||||
|
|
||||||
|
m_refreshWatcher = new QFutureWatcher<QByteArray>(this);
|
||||||
|
connect(m_refreshWatcher, &QFutureWatcher<QByteArray>::finished,
|
||||||
|
this, &RcxController::onReadComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
m_readInFlight = true;
|
||||||
|
m_readGen = m_refreshGen;
|
||||||
|
|
||||||
|
// Capture shared_ptr copy — keeps provider alive during async read
|
||||||
|
auto prov = m_doc->provider;
|
||||||
|
m_refreshWatcher->setFuture(QtConcurrent::run([prov, extent]() -> QByteArray {
|
||||||
|
return prov->readBytes(0, extent);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RcxController::onReadComplete() {
|
||||||
|
m_readInFlight = false;
|
||||||
|
|
||||||
|
// Stale read (provider changed while we were reading) — discard
|
||||||
|
if (m_readGen != m_refreshGen) return;
|
||||||
|
|
||||||
|
QByteArray newData = m_refreshWatcher->result();
|
||||||
|
|
||||||
|
// Fast path: no changes at all — skip full recompose
|
||||||
|
if (!m_prevSnapshot.isEmpty() && m_prevSnapshot.size() == newData.size()
|
||||||
|
&& memcmp(m_prevSnapshot.constData(), newData.constData(), newData.size()) == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Compute which byte offsets changed
|
||||||
|
m_changedOffsets.clear();
|
||||||
|
if (!m_prevSnapshot.isEmpty()) {
|
||||||
|
int compareLen = qMin(m_prevSnapshot.size(), newData.size());
|
||||||
|
const char* oldP = m_prevSnapshot.constData();
|
||||||
|
const char* newP = newData.constData();
|
||||||
|
for (int i = 0; i < compareLen; i++) {
|
||||||
|
if (oldP[i] != newP[i])
|
||||||
|
m_changedOffsets.insert(i);
|
||||||
|
}
|
||||||
|
// Bytes beyond old snapshot are all "new"
|
||||||
|
for (int i = compareLen; i < newData.size(); i++)
|
||||||
|
m_changedOffsets.insert(i);
|
||||||
|
}
|
||||||
|
m_prevSnapshot = newData;
|
||||||
|
|
||||||
|
// Update or create snapshot provider
|
||||||
|
if (m_snapshotProv)
|
||||||
|
m_snapshotProv->updateSnapshot(std::move(newData));
|
||||||
|
else
|
||||||
|
m_snapshotProv = std::make_unique<SnapshotProvider>(m_doc->provider, std::move(newData));
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
// Clear changed offsets after refresh consumed them
|
||||||
|
m_changedOffsets.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
int RcxController::computeDataExtent() const {
|
||||||
|
// Use provider size as the extent (for ProcessProvider this is the module/region size)
|
||||||
|
int provSize = m_doc->provider->size();
|
||||||
|
if (provSize > 0) return provSize;
|
||||||
|
|
||||||
|
// Fallback: walk tree to find maximum byte offset
|
||||||
|
int maxEnd = 0;
|
||||||
|
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||||
|
int64_t off = m_doc->tree.computeOffset(i);
|
||||||
|
int sz = m_doc->tree.nodes[i].byteSize();
|
||||||
|
int end = (int)(off + sz);
|
||||||
|
if (end > maxEnd) maxEnd = end;
|
||||||
|
}
|
||||||
|
return maxEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RcxController::resetSnapshot() {
|
||||||
|
m_refreshGen++;
|
||||||
|
m_readInFlight = false;
|
||||||
|
m_snapshotProv.reset();
|
||||||
|
m_prevSnapshot.clear();
|
||||||
|
m_changedOffsets.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::handleMarginClick(RcxEditor* editor, int margin,
|
void RcxController::handleMarginClick(RcxEditor* editor, int margin,
|
||||||
int line, Qt::KeyboardModifiers) {
|
int line, Qt::KeyboardModifiers) {
|
||||||
const LineMeta* lm = editor->metaForLine(line);
|
const LineMeta* lm = editor->metaForLine(line);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "core.h"
|
#include "core.h"
|
||||||
#include "editor.h"
|
#include "editor.h"
|
||||||
|
#include "providers/snapshot_provider.h"
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QUndoStack>
|
#include <QUndoStack>
|
||||||
#include <QUndoCommand>
|
#include <QUndoCommand>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QFutureWatcher>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
class QSplitter;
|
class QSplitter;
|
||||||
@@ -20,7 +23,7 @@ public:
|
|||||||
explicit RcxDocument(QObject* parent = nullptr);
|
explicit RcxDocument(QObject* parent = nullptr);
|
||||||
|
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
std::unique_ptr<Provider> provider;
|
std::shared_ptr<Provider> provider;
|
||||||
QUndoStack undoStack;
|
QUndoStack undoStack;
|
||||||
QString filePath;
|
QString filePath;
|
||||||
QString dataPath;
|
QString dataPath;
|
||||||
@@ -65,6 +68,7 @@ class RcxController : public QObject {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit RcxController(RcxDocument* doc, QWidget* parent = nullptr);
|
explicit RcxController(RcxDocument* doc, QWidget* parent = nullptr);
|
||||||
|
~RcxController() override;
|
||||||
|
|
||||||
RcxEditor* primaryEditor() const;
|
RcxEditor* primaryEditor() const;
|
||||||
RcxEditor* addSplitEditor(QSplitter* splitter);
|
RcxEditor* addSplitEditor(QSplitter* splitter);
|
||||||
@@ -111,12 +115,29 @@ private:
|
|||||||
QVector<SavedSourceEntry> m_savedSources;
|
QVector<SavedSourceEntry> m_savedSources;
|
||||||
int m_activeSourceIdx = -1;
|
int m_activeSourceIdx = -1;
|
||||||
|
|
||||||
|
// ── Auto-refresh state ──
|
||||||
|
QTimer* m_refreshTimer = nullptr;
|
||||||
|
QFutureWatcher<QByteArray>* m_refreshWatcher = nullptr;
|
||||||
|
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
||||||
|
QByteArray m_prevSnapshot;
|
||||||
|
QSet<int64_t> m_changedOffsets;
|
||||||
|
uint64_t m_refreshGen = 0;
|
||||||
|
uint64_t m_readGen = 0;
|
||||||
|
bool m_readInFlight = false;
|
||||||
|
|
||||||
void connectEditor(RcxEditor* editor);
|
void connectEditor(RcxEditor* editor);
|
||||||
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
|
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
|
||||||
void updateCommandRow();
|
void updateCommandRow();
|
||||||
void attachToProcess(uint32_t pid, const QString& processName);
|
void attachToProcess(uint32_t pid, const QString& processName);
|
||||||
void switchToSavedSource(int idx);
|
void switchToSavedSource(int idx);
|
||||||
void pushSavedSourcesToEditors();
|
void pushSavedSourcesToEditors();
|
||||||
|
|
||||||
|
// ── Auto-refresh methods ──
|
||||||
|
void setupAutoRefresh();
|
||||||
|
void onRefreshTick();
|
||||||
|
void onReadComplete();
|
||||||
|
int computeDataExtent() const;
|
||||||
|
void resetSnapshot();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
25
src/core.h
25
src/core.h
@@ -399,6 +399,7 @@ struct LineMeta {
|
|||||||
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
|
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
|
||||||
QString offsetText;
|
QString offsetText;
|
||||||
uint32_t markerMask = 0;
|
uint32_t markerMask = 0;
|
||||||
|
bool dataChanged = false; // true if any byte in this node changed since last refresh
|
||||||
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
||||||
int effectiveNameW = 22; // Per-line name column width used for rendering
|
int effectiveNameW = 22; // Per-line name column width used for rendering
|
||||||
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
|
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
|
||||||
@@ -431,7 +432,7 @@ namespace cmd {
|
|||||||
QVector<OffsetAdj> offAdjs; };
|
QVector<OffsetAdj> offAdjs; };
|
||||||
struct Rename { uint64_t nodeId; QString oldName, newName; };
|
struct Rename { uint64_t nodeId; QString oldName, newName; };
|
||||||
struct Collapse { uint64_t nodeId; bool oldState, newState; };
|
struct Collapse { uint64_t nodeId; bool oldState, newState; };
|
||||||
struct Insert { Node node; };
|
struct Insert { Node node; QVector<OffsetAdj> offAdjs; };
|
||||||
struct Remove { uint64_t nodeId; QVector<Node> subtree;
|
struct Remove { uint64_t nodeId; QVector<Node> subtree;
|
||||||
QVector<OffsetAdj> offAdjs; };
|
QVector<OffsetAdj> offAdjs; };
|
||||||
struct ChangeBase { uint64_t oldBase, newBase; };
|
struct ChangeBase { uint64_t oldBase, newBase; };
|
||||||
@@ -486,8 +487,8 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
|
|||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
int start = ind + typeW + kSepWidth;
|
int start = ind + typeW + kSepWidth;
|
||||||
|
|
||||||
// Padding: ASCII preview takes the name column position (8 chars)
|
// Hex/Padding: ASCII preview takes the name column position (8 chars)
|
||||||
if (lm.nodeKind == NodeKind::Padding)
|
if (isHexPreview(lm.nodeKind))
|
||||||
return {start, start + 8, true};
|
return {start, start + 8, true};
|
||||||
|
|
||||||
return {start, start + nameW, true};
|
return {start, start + nameW, true};
|
||||||
@@ -498,12 +499,12 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
|||||||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
|
|
||||||
// Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
|
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
|
||||||
bool isPad = (lm.nodeKind == NodeKind::Padding);
|
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||||
int valWidth = isPad ? 23 : kColValue;
|
int valWidth = isHexPad ? 23 : kColValue;
|
||||||
|
|
||||||
if (lm.isContinuation) {
|
if (lm.isContinuation) {
|
||||||
int prefixW = isPad
|
int prefixW = isHexPad
|
||||||
? (typeW + kSepWidth + 8 + kSepWidth)
|
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||||
: (typeW + nameW + 2 * kSepWidth);
|
: (typeW + nameW + 2 * kSepWidth);
|
||||||
int start = ind + prefixW;
|
int start = ind + prefixW;
|
||||||
@@ -511,7 +512,7 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
|||||||
}
|
}
|
||||||
if (lm.lineKind != LineKind::Field) return {};
|
if (lm.lineKind != LineKind::Field) return {};
|
||||||
|
|
||||||
int start = isPad
|
int start = isHexPad
|
||||||
? (ind + typeW + kSepWidth + 8 + kSepWidth)
|
? (ind + typeW + kSepWidth + 8 + kSepWidth)
|
||||||
: (ind + typeW + kSepWidth + nameW + kSepWidth);
|
: (ind + typeW + kSepWidth + nameW + kSepWidth);
|
||||||
return {start, start + valWidth, true};
|
return {start, start + valWidth, true};
|
||||||
@@ -521,17 +522,17 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
|||||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
|
|
||||||
bool isPad = (lm.nodeKind == NodeKind::Padding);
|
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||||
int valWidth = isPad ? 23 : kColValue;
|
int valWidth = isHexPad ? 23 : kColValue;
|
||||||
|
|
||||||
int start;
|
int start;
|
||||||
if (lm.isContinuation) {
|
if (lm.isContinuation) {
|
||||||
int prefixW = isPad
|
int prefixW = isHexPad
|
||||||
? (typeW + kSepWidth + 8 + kSepWidth)
|
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||||
: (typeW + nameW + 2 * kSepWidth);
|
: (typeW + nameW + 2 * kSepWidth);
|
||||||
start = ind + prefixW + valWidth;
|
start = ind + prefixW + valWidth;
|
||||||
} else {
|
} else {
|
||||||
start = isPad
|
start = isHexPad
|
||||||
? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth)
|
? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth)
|
||||||
: (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth);
|
: (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ static constexpr int IND_HEX_DIM = 9;
|
|||||||
static constexpr int IND_BASE_ADDR = 10; // Green color for base address
|
static constexpr int IND_BASE_ADDR = 10; // Green color for base address
|
||||||
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
||||||
static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans
|
static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans
|
||||||
|
static constexpr int IND_DATA_CHANGED = 13; // Amber text for changed data values
|
||||||
|
|
||||||
static QString g_fontName = "Consolas";
|
static QString g_fontName = "Consolas";
|
||||||
|
|
||||||
@@ -168,6 +169,12 @@ void RcxEditor::setupScintilla() {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
||||||
IND_CMD_PILL, (long)1);
|
IND_CMD_PILL, (long)1);
|
||||||
|
|
||||||
|
// Data-changed indicator — amber text for values that changed since last refresh
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
|
IND_DATA_CHANGED, 17 /*INDIC_TEXTFORE*/);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
|
IND_DATA_CHANGED, QColor("#E5A00D"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::setupLexer() {
|
void RcxEditor::setupLexer() {
|
||||||
@@ -321,6 +328,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
applyMarkers(result.meta);
|
applyMarkers(result.meta);
|
||||||
applyFoldLevels(result.meta);
|
applyFoldLevels(result.meta);
|
||||||
applyHexDimming(result.meta);
|
applyHexDimming(result.meta);
|
||||||
|
applyDataChangedHighlight(result.meta);
|
||||||
applyCommandRowPills();
|
applyCommandRowPills();
|
||||||
|
|
||||||
// Reset hint line - applySelectionOverlay will repaint indicators
|
// Reset hint line - applySelectionOverlay will repaint indicators
|
||||||
@@ -404,7 +412,7 @@ void RcxEditor::fillIndicatorCols(int indic, int line, int colA, int colB) {
|
|||||||
void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
|
void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM);
|
||||||
for (int i = 0; i < meta.size(); i++) {
|
for (int i = 0; i < meta.size(); i++) {
|
||||||
if (meta[i].nodeKind == NodeKind::Padding) {
|
if (isHexPreview(meta[i].nodeKind)) {
|
||||||
long pos, len; lineRangeNoEol(m_sci, i, pos, len);
|
long pos, len; lineRangeNoEol(m_sci, i, pos, len);
|
||||||
if (len > 0)
|
if (len > 0)
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len);
|
||||||
@@ -544,6 +552,20 @@ static QString getLineText(QsciScintilla* sci, int line) {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxEditor::applyDataChangedHighlight(const QVector<LineMeta>& meta) {
|
||||||
|
for (int i = 0; i < meta.size(); i++) {
|
||||||
|
if (!meta[i].dataChanged) continue;
|
||||||
|
if (isSyntheticLine(meta[i])) continue;
|
||||||
|
|
||||||
|
QString lineText = getLineText(m_sci, i);
|
||||||
|
int typeW = meta[i].effectiveTypeW;
|
||||||
|
int nameW = meta[i].effectiveNameW;
|
||||||
|
ColumnSpan vs = valueSpan(meta[i], lineText.size(), typeW, nameW);
|
||||||
|
if (vs.valid)
|
||||||
|
fillIndicatorCols(IND_DATA_CHANGED, i, vs.start, vs.end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RcxEditor::applyBaseAddressColoring(const QVector<LineMeta>& meta) {
|
void RcxEditor::applyBaseAddressColoring(const QVector<LineMeta>& meta) {
|
||||||
if (meta.isEmpty() || meta[0].lineKind != LineKind::CommandRow) return;
|
if (meta.isEmpty() || meta[0].lineKind != LineKind::CommandRow) return;
|
||||||
|
|
||||||
@@ -609,16 +631,20 @@ static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
|
|||||||
|
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
int typeW = lm.effectiveTypeW;
|
int typeW = lm.effectiveTypeW;
|
||||||
int nameW = lm.effectiveNameW;
|
|
||||||
int nameStart = ind + typeW + kSepWidth;
|
int nameStart = ind + typeW + kSepWidth;
|
||||||
int nameEnd = nameStart + nameW;
|
|
||||||
|
|
||||||
// Clamp to line length
|
|
||||||
if (nameStart >= lineText.size()) return {};
|
if (nameStart >= lineText.size()) return {};
|
||||||
if (nameEnd > lineText.size()) nameEnd = lineText.size();
|
|
||||||
|
// Name ends before " {" suffix (expanded) or at line end (collapsed)
|
||||||
|
int nameEnd = lineText.size();
|
||||||
|
if (lineText.endsWith(QStringLiteral(" {")))
|
||||||
|
nameEnd = lineText.size() - 2;
|
||||||
|
|
||||||
|
if (nameEnd <= nameStart) return {};
|
||||||
|
|
||||||
// Don't allow editing array element names like "[0]", "[1]", etc.
|
// Don't allow editing array element names like "[0]", "[1]", etc.
|
||||||
QString name = lineText.mid(nameStart, nameEnd - nameStart).trimmed();
|
QString name = lineText.mid(nameStart, nameEnd - nameStart).trimmed();
|
||||||
|
if (name.isEmpty()) return {};
|
||||||
if (name.startsWith('[') && name.endsWith(']'))
|
if (name.startsWith('[') && name.endsWith(']'))
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
@@ -1678,7 +1704,7 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
bool inHexDataArea = false;
|
bool inHexDataArea = false;
|
||||||
uint64_t hoverNodeId = 0;
|
uint64_t hoverNodeId = 0;
|
||||||
if (hoverLine >= 0 && hoverLine < m_meta.size()
|
if (hoverLine >= 0 && hoverLine < m_meta.size()
|
||||||
&& m_meta[hoverLine].nodeKind == NodeKind::Padding) {
|
&& isHexPreview(m_meta[hoverLine].nodeKind)) {
|
||||||
hoverNodeId = m_meta[hoverLine].nodeId;
|
hoverNodeId = m_meta[hoverLine].nodeId;
|
||||||
if (hoverNodeId != 0 && h.col >= 0) {
|
if (hoverNodeId != 0 && h.col >= 0) {
|
||||||
int ind = kFoldCol + m_meta[hoverLine].depth * 3;
|
int ind = kFoldCol + m_meta[hoverLine].depth * 3;
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ private:
|
|||||||
void applyMarkers(const QVector<LineMeta>& meta);
|
void applyMarkers(const QVector<LineMeta>& meta);
|
||||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||||
|
void applyDataChangedHighlight(const QVector<LineMeta>& meta);
|
||||||
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||||
void applyCommandRowPills();
|
void applyCommandRowPills();
|
||||||
|
|
||||||
|
|||||||
@@ -121,9 +121,8 @@ QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType
|
|||||||
// Columnar format: <type> <name> { (or no brace when collapsed)
|
// Columnar format: <type> <name> { (or no brace when collapsed)
|
||||||
QString ind = indent(depth);
|
QString ind = indent(depth);
|
||||||
QString type = fit(structTypeName(node), colType);
|
QString type = fit(structTypeName(node), colType);
|
||||||
QString name = fit(node.name, colName);
|
|
||||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||||
return ind + type + SEP + name + SEP + suffix;
|
return ind + type + SEP + node.name + SEP + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
||||||
@@ -135,9 +134,8 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
|||||||
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName) {
|
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName) {
|
||||||
QString ind = indent(depth);
|
QString ind = indent(depth);
|
||||||
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen), colType);
|
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen), colType);
|
||||||
QString name = fit(node.name, colName);
|
|
||||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||||
return ind + type + SEP + name + SEP + suffix;
|
return ind + type + SEP + node.name + SEP + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Pointer header (merged pointer + struct header) ──
|
// ── Pointer header (merged pointer + struct header) ──
|
||||||
@@ -147,13 +145,13 @@ QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
|||||||
const QString& ptrTypeName, int colType, int colName) {
|
const QString& ptrTypeName, int colType, int colName) {
|
||||||
QString ind = indent(depth);
|
QString ind = indent(depth);
|
||||||
QString type = fit(ptrTypeName, colType);
|
QString type = fit(ptrTypeName, colType);
|
||||||
QString name = fit(node.name, colName);
|
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
// Collapsed: show pointer value instead of brace
|
// Collapsed: show pointer value instead of brace (name padded for value alignment)
|
||||||
|
QString name = fit(node.name, colName);
|
||||||
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
|
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
|
||||||
return ind + type + SEP + name + SEP + val;
|
return ind + type + SEP + name + SEP + val;
|
||||||
}
|
}
|
||||||
return ind + type + SEP + name + SEP + QStringLiteral("{");
|
return ind + type + SEP + node.name + SEP + QStringLiteral("{");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hex / ASCII preview ──
|
// ── Hex / ASCII preview ──
|
||||||
@@ -211,10 +209,6 @@ enum class ValueMode { Display, Editable };
|
|||||||
|
|
||||||
static QString readValueImpl(const Node& node, const Provider& prov,
|
static QString readValueImpl(const Node& node, const Provider& prov,
|
||||||
uint64_t addr, int subLine, ValueMode mode) {
|
uint64_t addr, int subLine, ValueMode mode) {
|
||||||
int sz = node.byteSize();
|
|
||||||
if (sz > 0 && !prov.isReadable(addr, sz))
|
|
||||||
return (mode == ValueMode::Display) ? QStringLiteral("???") : QString();
|
|
||||||
|
|
||||||
const bool display = (mode == ValueMode::Display);
|
const bool display = (mode == ValueMode::Display);
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
case NodeKind::Hex8: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
|
case NodeKind::Hex8: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
|
||||||
@@ -328,7 +322,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
|||||||
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Padding: ASCII preview + hex bytes (compact, multi-line)
|
// Hex nodes and Padding: hex byte preview
|
||||||
|
if (isHexPreview(node.kind)) {
|
||||||
if (node.kind == NodeKind::Padding) {
|
if (node.kind == NodeKind::Padding) {
|
||||||
const int totalSz = qMax(1, node.arrayLen);
|
const int totalSz = qMax(1, node.arrayLen);
|
||||||
const int lineOff = subLine * 8;
|
const int lineOff = subLine * 8;
|
||||||
@@ -341,6 +336,14 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
|||||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||||
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
||||||
}
|
}
|
||||||
|
// Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns
|
||||||
|
const int sz = sizeForKind(node.kind);
|
||||||
|
QByteArray b = prov.isReadable(addr, sz)
|
||||||
|
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
||||||
|
QString ascii = bytesToAscii(b, sz).leftJustified(8, ' ');
|
||||||
|
QString hex = bytesToHex(b, sz).leftJustified(23, ' ');
|
||||||
|
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||||
|
}
|
||||||
|
|
||||||
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||||
return ind + type + SEP + name + SEP + val + cmtSuffix;
|
return ind + type + SEP + name + SEP + val + cmtSuffix;
|
||||||
|
|||||||
67
src/main.cpp
67
src/main.cpp
@@ -1,5 +1,6 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "generator.h"
|
#include "generator.h"
|
||||||
|
#include "providers/process_provider.h"
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMdiArea>
|
#include <QMdiArea>
|
||||||
@@ -101,6 +102,31 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// ── Self-test: live data for verifying auto-refresh ──
|
||||||
|
#include <thread>
|
||||||
|
#include <atomic>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
struct TestLiveData {
|
||||||
|
int32_t valA = 100;
|
||||||
|
int32_t valB = 200;
|
||||||
|
int32_t valC = 300;
|
||||||
|
int32_t valD = 400;
|
||||||
|
};
|
||||||
|
|
||||||
|
static TestLiveData* g_testData = nullptr;
|
||||||
|
static std::atomic<bool> g_testRunning{false};
|
||||||
|
|
||||||
|
static void testLiveThread() {
|
||||||
|
std::mt19937 rng(42);
|
||||||
|
std::uniform_int_distribution<int> dist(0, 3);
|
||||||
|
while (g_testRunning.load()) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
|
int32_t* fields = &g_testData->valA;
|
||||||
|
fields[dist(rng)]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
class MainWindow : public QMainWindow {
|
class MainWindow : public QMainWindow {
|
||||||
@@ -110,6 +136,7 @@ public:
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void newFile();
|
void newFile();
|
||||||
|
void selfTest();
|
||||||
void openFile();
|
void openFile();
|
||||||
void saveFile();
|
void saveFile();
|
||||||
void saveFileAs();
|
void saveFileAs();
|
||||||
@@ -382,6 +409,42 @@ void MainWindow::newFile() {
|
|||||||
createTab(doc);
|
createTab(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::selfTest() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Allocate test struct — lives until process exit
|
||||||
|
g_testData = new TestLiveData();
|
||||||
|
g_testRunning = true;
|
||||||
|
std::thread(testLiveThread).detach();
|
||||||
|
|
||||||
|
auto* doc = new RcxDocument(this);
|
||||||
|
uint64_t base = (uint64_t)g_testData;
|
||||||
|
|
||||||
|
HANDLE hProc = OpenProcess(
|
||||||
|
PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION
|
||||||
|
| PROCESS_QUERY_INFORMATION,
|
||||||
|
FALSE, GetCurrentProcessId());
|
||||||
|
doc->provider = std::make_shared<ProcessProvider>(
|
||||||
|
hProc, base, (int)sizeof(TestLiveData), "ReclassX.exe");
|
||||||
|
doc->tree.baseAddress = base;
|
||||||
|
|
||||||
|
Node root;
|
||||||
|
root.kind = NodeKind::Struct;
|
||||||
|
root.name = "TestLiveData";
|
||||||
|
root.structTypeName = "TestLiveData";
|
||||||
|
root.parentId = 0;
|
||||||
|
root.offset = 0;
|
||||||
|
int ri = doc->tree.addNode(root);
|
||||||
|
uint64_t rootId = doc->tree.nodes[ri].id;
|
||||||
|
|
||||||
|
{ Node n; n.kind = NodeKind::Int32; n.name = "valA"; n.parentId = rootId; n.offset = 0; doc->tree.addNode(n); }
|
||||||
|
{ Node n; n.kind = NodeKind::Int32; n.name = "valB"; n.parentId = rootId; n.offset = 4; doc->tree.addNode(n); }
|
||||||
|
{ Node n; n.kind = NodeKind::Int32; n.name = "valC"; n.parentId = rootId; n.offset = 8; doc->tree.addNode(n); }
|
||||||
|
{ Node n; n.kind = NodeKind::Int32; n.name = "valD"; n.parentId = rootId; n.offset = 12; doc->tree.addNode(n); }
|
||||||
|
|
||||||
|
createTab(doc);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::openFile() {
|
void MainWindow::openFile() {
|
||||||
QString path = QFileDialog::getOpenFileName(this,
|
QString path = QFileDialog::getOpenFileName(this,
|
||||||
"Open Definition", {}, "ReclassX (*.rcx);;JSON (*.json);;All (*)");
|
"Open Definition", {}, "ReclassX (*.rcx);;JSON (*.json);;All (*)");
|
||||||
@@ -774,8 +837,8 @@ int main(int argc, char* argv[]) {
|
|||||||
window.setWindowOpacity(0.0);
|
window.setWindowOpacity(0.0);
|
||||||
window.show();
|
window.show();
|
||||||
|
|
||||||
// Always auto-open PEB64 demo on startup
|
// Auto-open self-test tab (live data refresh test)
|
||||||
QMetaObject::invokeMethod(&window, "newFile");
|
QMetaObject::invokeMethod(&window, "selfTest");
|
||||||
|
|
||||||
if (screenshotMode) {
|
if (screenshotMode) {
|
||||||
QString out = "screenshot.png";
|
QString out = "screenshot.png";
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ public:
|
|||||||
|
|
||||||
QString name() const override { return m_name; }
|
QString name() const override { return m_name; }
|
||||||
QString kind() const override { return QStringLiteral("Process"); }
|
QString kind() const override { return QStringLiteral("Process"); }
|
||||||
|
bool isLive() const override { return true; }
|
||||||
|
|
||||||
// getSymbol takes an absolute virtual address and resolves it to
|
// getSymbol takes an absolute virtual address and resolves it to
|
||||||
// "module.dll+0xOFFSET" using the cached module list.
|
// "module.dll+0xOFFSET" using the cached module list.
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ public:
|
|||||||
// Examples: "notepad.exe", "dump.bin", "tcp://10.0.0.1:1337"
|
// Examples: "notepad.exe", "dump.bin", "tcp://10.0.0.1:1337"
|
||||||
virtual QString name() const { return {}; }
|
virtual QString name() const { return {}; }
|
||||||
|
|
||||||
|
// Whether data can change externally (e.g. live process, network socket).
|
||||||
|
// Auto-refresh is only active for live providers.
|
||||||
|
virtual bool isLive() const { return false; }
|
||||||
|
|
||||||
// Category tag for the command row Source span.
|
// Category tag for the command row Source span.
|
||||||
// Examples: "File", "Process", "Socket"
|
// Examples: "File", "Process", "Socket"
|
||||||
virtual QString kind() const { return QStringLiteral("File"); }
|
virtual QString kind() const { return QStringLiteral("File"); }
|
||||||
|
|||||||
54
src/providers/snapshot_provider.h
Normal file
54
src/providers/snapshot_provider.h
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "provider.h"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
// Provider that reads from a cached QByteArray snapshot but delegates
|
||||||
|
// metadata (name, kind, getSymbol) to the underlying real provider.
|
||||||
|
// Used for async refresh: worker thread reads bulk data into a snapshot,
|
||||||
|
// UI thread composes against it without blocking.
|
||||||
|
class SnapshotProvider : public Provider {
|
||||||
|
std::shared_ptr<Provider> m_real;
|
||||||
|
QByteArray m_data;
|
||||||
|
|
||||||
|
public:
|
||||||
|
SnapshotProvider(std::shared_ptr<Provider> real, QByteArray snapshot)
|
||||||
|
: m_real(std::move(real)), m_data(std::move(snapshot)) {}
|
||||||
|
|
||||||
|
bool read(uint64_t addr, void* buf, int len) const override {
|
||||||
|
if (!isReadable(addr, len)) return false;
|
||||||
|
std::memcpy(buf, m_data.constData() + addr, len);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size() const override { return m_data.size(); }
|
||||||
|
bool isWritable() const override { return m_real ? m_real->isWritable() : false; }
|
||||||
|
bool isLive() const override { return m_real ? m_real->isLive() : false; }
|
||||||
|
QString name() const override { return m_real ? m_real->name() : QString(); }
|
||||||
|
QString kind() const override { return m_real ? m_real->kind() : QStringLiteral("File"); }
|
||||||
|
QString getSymbol(uint64_t addr) const override {
|
||||||
|
return m_real ? m_real->getSymbol(addr) : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool write(uint64_t addr, const void* buf, int len) override {
|
||||||
|
if (!m_real) return false;
|
||||||
|
bool ok = m_real->write(addr, buf, len);
|
||||||
|
if (ok && isReadable(addr, len))
|
||||||
|
std::memcpy(m_data.data() + addr, buf, len);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the entire snapshot (called after async read completes)
|
||||||
|
void updateSnapshot(QByteArray data) { m_data = std::move(data); }
|
||||||
|
|
||||||
|
// Patch specific bytes in the snapshot (called after user writes a value)
|
||||||
|
void patchSnapshot(uint64_t addr, const void* buf, int len) {
|
||||||
|
if (isReadable(addr, len))
|
||||||
|
std::memcpy(m_data.data() + addr, buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray& snapshot() const { return m_data; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
Reference in New Issue
Block a user