mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Fix cursor jump on command row edit, data-change highlighting for containers, invalid pointer expansion, cycle detection with node IDs, O(n²) addNode cache, duplicate struct in C++ export, collapse bug for non-first root structs, remove hardcoded demo data
This commit is contained in:
@@ -75,17 +75,19 @@ static QString resolvePointerTarget(const NodeTree& tree, uint64_t refId) {
|
||||
}
|
||||
|
||||
static inline uint64_t ptrToProviderAddr(const NodeTree& tree, uint64_t ptr) {
|
||||
if (tree.baseAddress && ptr >= tree.baseAddress) return ptr - tree.baseAddress;
|
||||
return ptr;
|
||||
if (tree.baseAddress == 0) return ptr;
|
||||
if (ptr >= tree.baseAddress) return ptr - tree.baseAddress;
|
||||
return UINT64_MAX; // Invalid: ptr below base address
|
||||
}
|
||||
|
||||
static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) {
|
||||
int64_t total = 0;
|
||||
QSet<int> visited;
|
||||
QSet<uint64_t> visited;
|
||||
int cur = idx;
|
||||
while (cur >= 0 && cur < tree.nodes.size()) {
|
||||
if (visited.contains(cur)) break;
|
||||
visited.insert(cur);
|
||||
uint64_t nid = tree.nodes[cur].id;
|
||||
if (visited.contains(nid)) break;
|
||||
visited.insert(nid);
|
||||
const Node& n = tree.nodes[cur];
|
||||
if (n.id == rootId) break;
|
||||
total += n.offset;
|
||||
@@ -337,6 +339,10 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
|
||||
uint64_t ptrVal = (node.kind == NodeKind::Pointer32)
|
||||
? (uint64_t)prov.readU32(absAddr) : prov.readU64(absAddr);
|
||||
if (ptrVal != 0) {
|
||||
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
|
||||
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
|
||||
}
|
||||
if (ptrVal != 0) {
|
||||
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
|
||||
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
|
||||
@@ -507,9 +513,6 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
// If viewRootId is set, skip roots that don't match
|
||||
if (viewRootId != 0 && tree.nodes[idx].id != viewRootId)
|
||||
continue;
|
||||
// Skip collapsed roots unless specifically targeted by viewRootId
|
||||
if (viewRootId == 0 && tree.nodes[idx].collapsed)
|
||||
continue;
|
||||
composeNode(state, tree, prov, idx, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -540,8 +540,9 @@ void RcxController::refresh() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Existing boolean logic for non-hex nodes
|
||||
int sz = node.byteSize();
|
||||
// Use structSpan for containers (byteSize returns 0 for Array-of-Struct)
|
||||
int sz = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
|
||||
? m_doc->tree.structSpan(node.id) : node.byteSize();
|
||||
for (int64_t b = offset; b < offset + sz; b++) {
|
||||
if (m_changedOffsets.contains(b)) {
|
||||
lm.dataChanged = true;
|
||||
@@ -1108,16 +1109,19 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
}
|
||||
|
||||
menu.addAction(icon("add.svg"), "Add Hex64 at Root", [this]() {
|
||||
insertNode(0, -1, NodeKind::Hex64, "newField");
|
||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||
insertNode(target, -1, NodeKind::Hex64, "newField");
|
||||
});
|
||||
menu.addAction(icon("symbol-structure.svg"), "Add Struct at Root", [this]() {
|
||||
insertNode(0, -1, NodeKind::Struct, "NewClass");
|
||||
setViewRootId(0); // show all so the new struct is visible
|
||||
});
|
||||
menu.addAction(icon("diff-added.svg"), "Append 128 bytes", [this]() {
|
||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||
m_suppressRefresh = true;
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Append 128 bytes"));
|
||||
for (int i = 0; i < 16; i++)
|
||||
insertNode(0, -1, NodeKind::Hex64,
|
||||
insertNode(target, -1, NodeKind::Hex64,
|
||||
QStringLiteral("field_%1").arg(i));
|
||||
m_doc->undoStack.endMacro();
|
||||
m_suppressRefresh = false;
|
||||
|
||||
20
src/core.h
20
src/core.h
@@ -240,9 +240,11 @@ struct NodeTree {
|
||||
Node copy = n;
|
||||
if (copy.id == 0) copy.id = m_nextId++;
|
||||
else if (copy.id >= m_nextId) m_nextId = copy.id + 1;
|
||||
int idx = nodes.size();
|
||||
nodes.append(copy);
|
||||
m_idCache.clear();
|
||||
return nodes.size() - 1;
|
||||
if (!m_idCache.isEmpty())
|
||||
m_idCache[copy.id] = idx;
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Reserve a unique ID atomically (for use before pushing undo commands)
|
||||
@@ -297,11 +299,12 @@ struct NodeTree {
|
||||
|
||||
int depthOf(int idx) const {
|
||||
int d = 0;
|
||||
QSet<int> visited;
|
||||
QSet<uint64_t> visited;
|
||||
int cur = idx;
|
||||
while (cur >= 0 && cur < nodes.size() && nodes[cur].parentId != 0) {
|
||||
if (visited.contains(cur)) break;
|
||||
visited.insert(cur);
|
||||
uint64_t nid = nodes[cur].id;
|
||||
if (visited.contains(nid)) break;
|
||||
visited.insert(nid);
|
||||
cur = indexOfId(nodes[cur].parentId);
|
||||
if (cur < 0) break;
|
||||
d++;
|
||||
@@ -311,11 +314,12 @@ struct NodeTree {
|
||||
|
||||
int64_t computeOffset(int idx) const {
|
||||
int64_t total = 0;
|
||||
QSet<int> visited;
|
||||
QSet<uint64_t> visited;
|
||||
int cur = idx;
|
||||
while (cur >= 0 && cur < nodes.size()) {
|
||||
if (visited.contains(cur)) break;
|
||||
visited.insert(cur);
|
||||
uint64_t nid = nodes[cur].id;
|
||||
if (visited.contains(nid)) break;
|
||||
visited.insert(nid);
|
||||
total += nodes[cur].offset;
|
||||
if (nodes[cur].parentId == 0) break;
|
||||
cur = indexOfId(nodes[cur].parentId);
|
||||
|
||||
@@ -1982,10 +1982,16 @@ void RcxEditor::setCommandRowText(const QString& line) {
|
||||
long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0);
|
||||
long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 0);
|
||||
QByteArray utf8 = s.toUtf8();
|
||||
long oldLen = end - start;
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, start);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, end);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData());
|
||||
|
||||
// Adjust saved cursor/anchor for length change in line 0
|
||||
long delta = (long)utf8.size() - oldLen;
|
||||
if (savedPos > end) savedPos += delta;
|
||||
if (savedAnchor > end) savedAnchor += delta;
|
||||
|
||||
if (wasReadOnly) m_sci->setReadOnly(true);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 1);
|
||||
if (!wasModified) m_sci->SendScintilla(QsciScintillaBase::SCI_SETSAVEPOINT);
|
||||
@@ -2011,11 +2017,17 @@ void RcxEditor::setCommandRow2Text(const QString& line) {
|
||||
|
||||
long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 1);
|
||||
long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 1);
|
||||
long oldLen = end - start;
|
||||
QByteArray utf8 = s.toUtf8();
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, start);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, end);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData());
|
||||
|
||||
// Adjust saved cursor/anchor for length change in line 1
|
||||
long delta = (long)utf8.size() - oldLen;
|
||||
if (savedPos > end) savedPos += delta;
|
||||
if (savedAnchor > end) savedAnchor += delta;
|
||||
|
||||
if (wasReadOnly) m_sci->setReadOnly(true);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 1);
|
||||
if (!wasModified) m_sci->SendScintilla(QsciScintillaBase::SCI_SETSAVEPOINT);
|
||||
|
||||
@@ -255,6 +255,14 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduplicate by struct type name (different nodes may share the same type)
|
||||
QString typeName = ctx.structName(node);
|
||||
if (ctx.emittedTypeNames.contains(typeName)) {
|
||||
ctx.emittedIds.insert(structId);
|
||||
ctx.visiting.remove(structId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit nested struct types first (dependency order)
|
||||
QVector<int> children = ctx.childMap.value(structId);
|
||||
for (int ci : children) {
|
||||
@@ -283,8 +291,7 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
}
|
||||
|
||||
ctx.emittedIds.insert(structId);
|
||||
|
||||
QString typeName = ctx.structName(node);
|
||||
ctx.emittedTypeNames.insert(typeName);
|
||||
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
||||
|
||||
ctx.output += QStringLiteral("#pragma pack(push, 1)\n");
|
||||
|
||||
140
src/main.cpp
140
src/main.cpp
@@ -1,6 +1,5 @@
|
||||
#include "controller.h"
|
||||
#include "generator.h"
|
||||
#include "providers/process_provider.h"
|
||||
#include <QApplication>
|
||||
#include <QMainWindow>
|
||||
#include <QMdiArea>
|
||||
@@ -111,24 +110,6 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
||||
}
|
||||
#endif
|
||||
|
||||
// ── Self-test: live data for verifying auto-refresh ──
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <random>
|
||||
|
||||
static uint8_t* g_testData = nullptr;
|
||||
static constexpr int kTestDataSize = 128;
|
||||
static std::atomic<bool> g_testRunning{false};
|
||||
|
||||
static void testLiveThread() {
|
||||
std::mt19937 rng(42);
|
||||
std::uniform_int_distribution<int> dist(0, kTestDataSize - 1);
|
||||
while (g_testRunning.load()) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
g_testData[dist(rng)]++;
|
||||
}
|
||||
}
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
@@ -414,118 +395,13 @@ void MainWindow::newFile() {
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
#ifdef _WIN32
|
||||
// Allocate 128 bytes — lives until process exit
|
||||
g_testData = new uint8_t[kTestDataSize]();
|
||||
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, kTestDataSize, "ReclassX.exe");
|
||||
doc->tree.baseAddress = base;
|
||||
|
||||
// ── Pet (root struct, 64 bytes) ──
|
||||
{
|
||||
Node pet;
|
||||
pet.kind = NodeKind::Struct;
|
||||
pet.name = "aPet";
|
||||
pet.structTypeName = "Pet";
|
||||
pet.parentId = 0;
|
||||
pet.offset = 0;
|
||||
int pi = doc->tree.addNode(pet);
|
||||
uint64_t petId = doc->tree.nodes[pi].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::UTF8; n.name = "name"; n.parentId = petId; n.offset = 0; n.strLen = 24; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_18"; n.parentId = petId; n.offset = 24; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "age"; n.parentId = petId; n.offset = 32; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "weight"; n.parentId = petId; n.offset = 36; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = "owner"; n.parentId = petId; n.offset = 40; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Bool; n.name = "alive"; n.parentId = petId; n.offset = 48; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_31"; n.parentId = petId; n.offset = 49; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_32"; n.parentId = petId; n.offset = 50; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = "flags"; n.parentId = petId; n.offset = 52; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_38"; n.parentId = petId; n.offset = 56; doc->tree.addNode(n); }
|
||||
// Load demo.rcx if it exists, otherwise create a blank project
|
||||
QString demoPath = QCoreApplication::applicationDirPath() + "/demo.rcx";
|
||||
if (QFile::exists(demoPath)) {
|
||||
project_open(demoPath);
|
||||
} else {
|
||||
project_new();
|
||||
}
|
||||
|
||||
// ── Cat : Pet (root struct, inherits Pet at offset 0) ──
|
||||
{
|
||||
Node cat;
|
||||
cat.kind = NodeKind::Struct;
|
||||
cat.name = "aCat";
|
||||
cat.structTypeName = "Cat";
|
||||
cat.classKeyword = "class";
|
||||
cat.parentId = 0;
|
||||
cat.offset = 0;
|
||||
int ci = doc->tree.addNode(cat);
|
||||
uint64_t catId = doc->tree.nodes[ci].id;
|
||||
|
||||
// Embedded base Pet
|
||||
Node base;
|
||||
base.kind = NodeKind::Struct;
|
||||
base.name = "base";
|
||||
base.structTypeName = "Pet";
|
||||
base.parentId = catId;
|
||||
base.offset = 0;
|
||||
int bi = doc->tree.addNode(base);
|
||||
uint64_t baseId = doc->tree.nodes[bi].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::UTF8; n.name = "name"; n.parentId = baseId; n.offset = 0; n.strLen = 24; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_18"; n.parentId = baseId; n.offset = 24; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "age"; n.parentId = baseId; n.offset = 32; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "weight"; n.parentId = baseId; n.offset = 36; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = "owner"; n.parentId = baseId; n.offset = 40; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Bool; n.name = "alive"; n.parentId = baseId; n.offset = 48; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_31"; n.parentId = baseId; n.offset = 49; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_32"; n.parentId = baseId; n.offset = 50; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = "flags"; n.parentId = baseId; n.offset = 52; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_38"; n.parentId = baseId; n.offset = 56; doc->tree.addNode(n); }
|
||||
|
||||
// Cat's own fields after base
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "whiskerLen"; n.parentId = catId; n.offset = 64; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt8; n.name = "lives"; n.parentId = catId; n.offset = 68; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_45"; n.parentId = catId; n.offset = 69; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_46"; n.parentId = catId; n.offset = 70; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Bool; n.name = "indoor"; n.parentId = catId; n.offset = 72; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_49"; n.parentId = catId; n.offset = 73; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_4A"; n.parentId = catId; n.offset = 74; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "miceKilled"; n.parentId = catId; n.offset = 76; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_50"; n.parentId = catId; n.offset = 80; doc->tree.addNode(n); }
|
||||
}
|
||||
|
||||
// ── Ball (standalone root struct) ──
|
||||
{
|
||||
Node ball;
|
||||
ball.kind = NodeKind::Struct;
|
||||
ball.name = "aBall";
|
||||
ball.structTypeName = "Ball";
|
||||
ball.collapsed = true;
|
||||
ball.parentId = 0;
|
||||
ball.offset = 0;
|
||||
int bli = doc->tree.addNode(ball);
|
||||
uint64_t ballId = doc->tree.nodes[bli].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 0; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Vec3; n.name = "velocity"; n.parentId = ballId; n.offset = 16; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 28; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 32; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "radius"; n.parentId = ballId; n.offset = 36; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Double; n.name = "mass"; n.parentId = ballId; n.offset = 40; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 48; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_31"; n.parentId = ballId; n.offset = 49; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_32"; n.parentId = ballId; n.offset = 50; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = "bounceCount"; n.parentId = ballId; n.offset = 52; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_38"; n.parentId = ballId; n.offset = 56; doc->tree.addNode(n); }
|
||||
}
|
||||
|
||||
createTab(doc);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::openFile() {
|
||||
@@ -553,7 +429,7 @@ void MainWindow::addNode() {
|
||||
auto* ctrl = activeController();
|
||||
if (!ctrl) return;
|
||||
|
||||
uint64_t parentId = 0;
|
||||
uint64_t parentId = ctrl->viewRootId(); // default to current view root
|
||||
auto* primary = ctrl->primaryEditor();
|
||||
if (primary && primary->isEditing()) return;
|
||||
if (primary) {
|
||||
@@ -1123,7 +999,7 @@ int main(int argc, char* argv[]) {
|
||||
window.setWindowOpacity(0.0);
|
||||
window.show();
|
||||
|
||||
// Auto-open self-test tab (live data refresh test)
|
||||
// Auto-open demo project from saved .rcx file
|
||||
QMetaObject::invokeMethod(&window, "selfTest");
|
||||
|
||||
if (screenshotMode) {
|
||||
|
||||
@@ -99,10 +99,10 @@ private:
|
||||
int pi = tree.addNode(pet);
|
||||
uint64_t petId = tree.nodes[pi].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "hex_00"; n.parentId = petId; n.offset = 0; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "hex_01"; n.parentId = petId; n.offset = 1; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UTF8; n.name = "name"; n.parentId = petId; n.offset = 2; n.strLen = 32; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex16; n.name = "hex_22"; n.parentId = petId; n.offset = 34; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_00"; n.parentId = petId; n.offset = 0; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UTF8; n.name = "name"; n.parentId = petId; n.offset = 8; n.strLen = 16; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_18"; n.parentId = petId; n.offset = 24; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_20"; n.parentId = petId; n.offset = 32; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_24"; n.parentId = petId; n.offset = 36; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = "owner"; n.parentId = petId; n.offset = 40; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_30"; n.parentId = petId; n.offset = 48; tree.addNode(n); }
|
||||
@@ -129,15 +129,21 @@ private:
|
||||
uint64_t baseId = tree.nodes[bi].id;
|
||||
|
||||
// Children inside the nested Pet base
|
||||
{ Node n; n.kind = NodeKind::UTF8; n.name = "name"; n.parentId = baseId; n.offset = 0; n.strLen = 32; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_00"; n.parentId = baseId; n.offset = 0; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UTF8; n.name = "name"; n.parentId = baseId; n.offset = 8; n.strLen = 16; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_18"; n.parentId = baseId; n.offset = 24; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = "owner"; n.parentId = baseId; n.offset = 32; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_28"; n.parentId = baseId; n.offset = 40; tree.addNode(n); }
|
||||
|
||||
// Cat's own fields after base
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_40"; n.parentId = catId; n.offset = 64; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "whiskerLen"; n.parentId = catId; n.offset = 72; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_30"; n.parentId = catId; n.offset = 48; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_38"; n.parentId = catId; n.offset = 56; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "whiskerLen"; n.parentId = catId; n.offset = 64; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_44"; n.parentId = catId; n.offset = 68; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt8; n.name = "lives"; n.parentId = catId; n.offset = 72; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "hex_49"; n.parentId = catId; n.offset = 73; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex16; n.name = "hex_4A"; n.parentId = catId; n.offset = 74; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_4C"; n.parentId = catId; n.offset = 76; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt8; n.name = "lives"; n.parentId = catId; n.offset = 80; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "hex_51"; n.parentId = catId; n.offset = 81; tree.addNode(n); }
|
||||
|
||||
// ── Ball (independent root struct) ──
|
||||
Node ball;
|
||||
@@ -149,13 +155,16 @@ private:
|
||||
int bli = tree.addNode(ball);
|
||||
uint64_t ballId = tree.nodes[bli].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_00"; n.parentId = ballId; n.offset = 0; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 4; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_00"; n.parentId = ballId; n.offset = 0; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_08"; n.parentId = ballId; n.offset = 8; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 16; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 32; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_24"; n.parentId = ballId; n.offset = 36; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_28"; n.parentId = ballId; n.offset = 40; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 16; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_14"; n.parentId = ballId; n.offset = 20; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_18"; n.parentId = ballId; n.offset = 24; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 32; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_30"; n.parentId = ballId; n.offset = 48; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 56; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_3C"; n.parentId = ballId; n.offset = 60; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_40"; n.parentId = ballId; n.offset = 64; tree.addNode(n); }
|
||||
|
||||
return tree;
|
||||
}
|
||||
@@ -669,6 +678,147 @@ private slots:
|
||||
QVERIFY(!health->data(Qt::UserRole + 1).isValid());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Feature: Double-click navigation (viewRootId + scroll)
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
void testDoubleClick_switchToCollapsedClass() {
|
||||
// Simulates: Ball is collapsed (hidden). Double-click Ball in workspace
|
||||
// → uncollapse, set viewRootId, compose shows only Ball with children.
|
||||
RcxDocument doc;
|
||||
doc.tree = makeRichTree();
|
||||
|
||||
// Collapse Ball (3rd root struct)
|
||||
uint64_t ballId = 0;
|
||||
for (auto& node : doc.tree.nodes) {
|
||||
if (node.parentId == 0 && node.kind == NodeKind::Struct
|
||||
&& node.structTypeName == "Ball") {
|
||||
node.collapsed = true;
|
||||
ballId = node.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(ballId != 0);
|
||||
|
||||
// Compose with viewRootId=0 should skip collapsed Ball
|
||||
{
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(doc.tree, prov, 0);
|
||||
bool foundSpeed = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
int ni = lm.nodeIdx;
|
||||
if (ni >= 0 && ni < doc.tree.nodes.size()
|
||||
&& doc.tree.nodes[ni].name == "speed")
|
||||
foundSpeed = true;
|
||||
}
|
||||
QVERIFY2(!foundSpeed, "Collapsed Ball's children should not appear with viewRootId=0");
|
||||
}
|
||||
|
||||
// Simulate double-click: uncollapse Ball + set viewRootId
|
||||
int bi = doc.tree.indexOfId(ballId);
|
||||
QVERIFY(bi >= 0);
|
||||
doc.tree.nodes[bi].collapsed = false;
|
||||
|
||||
// Compose with viewRootId=Ball should show Ball and its children
|
||||
{
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(doc.tree, prov, ballId);
|
||||
bool foundSpeed = false, foundPosition = false, foundColor = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
int ni = lm.nodeIdx;
|
||||
if (ni < 0 || ni >= doc.tree.nodes.size()) continue;
|
||||
const QString& name = doc.tree.nodes[ni].name;
|
||||
if (name == "speed") foundSpeed = true;
|
||||
if (name == "position") foundPosition = true;
|
||||
if (name == "color") foundColor = true;
|
||||
}
|
||||
QVERIFY2(foundSpeed, "Ball's speed field should appear");
|
||||
QVERIFY2(foundPosition, "Ball's position field should appear");
|
||||
QVERIFY2(foundColor, "Ball's color field should appear");
|
||||
}
|
||||
|
||||
// Pet/Cat fields should NOT be in the Ball-filtered result
|
||||
{
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(doc.tree, prov, ballId);
|
||||
bool foundPetField = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
int ni = lm.nodeIdx;
|
||||
if (ni < 0 || ni >= doc.tree.nodes.size()) continue;
|
||||
if (doc.tree.nodes[ni].name == "owner") foundPetField = true;
|
||||
}
|
||||
QVERIFY2(!foundPetField, "Pet's owner should not appear when viewing Ball");
|
||||
}
|
||||
}
|
||||
|
||||
void testDoubleClick_fieldNavigatesToParentRoot() {
|
||||
// Simulates: double-click a field inside Ball → walk up to Ball root,
|
||||
// set viewRootId to Ball, and the field should be in the compose output.
|
||||
RcxDocument doc;
|
||||
doc.tree = makeRichTree();
|
||||
|
||||
// Find Ball's "speed" child
|
||||
uint64_t ballId = 0, speedId = 0;
|
||||
for (auto& node : doc.tree.nodes) {
|
||||
if (node.parentId == 0 && node.structTypeName == "Ball")
|
||||
ballId = node.id;
|
||||
}
|
||||
QVERIFY(ballId != 0);
|
||||
for (auto& node : doc.tree.nodes) {
|
||||
if (node.parentId == ballId && node.name == "speed")
|
||||
speedId = node.id;
|
||||
}
|
||||
QVERIFY(speedId != 0);
|
||||
|
||||
// Walk up from speed to find root struct (simulating handler logic)
|
||||
uint64_t rootId = 0;
|
||||
uint64_t cur = speedId;
|
||||
while (cur != 0) {
|
||||
int idx = doc.tree.indexOfId(cur);
|
||||
if (idx < 0) break;
|
||||
if (doc.tree.nodes[idx].parentId == 0) { rootId = cur; break; }
|
||||
cur = doc.tree.nodes[idx].parentId;
|
||||
}
|
||||
QCOMPARE(rootId, ballId);
|
||||
|
||||
// Compose with viewRootId=Ball should contain speed
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(doc.tree, prov, ballId);
|
||||
bool foundSpeed = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.nodeId == speedId) { foundSpeed = true; break; }
|
||||
}
|
||||
QVERIFY2(foundSpeed, "speed field should be in compose output when viewing its root");
|
||||
}
|
||||
|
||||
void testDoubleClick_projectRootShowsAll() {
|
||||
// Double-click project root clears viewRootId → all non-collapsed roots shown
|
||||
RcxDocument doc;
|
||||
doc.tree = makeRichTree();
|
||||
|
||||
// Collapse Ball
|
||||
for (auto& node : doc.tree.nodes) {
|
||||
if (node.parentId == 0 && node.structTypeName == "Ball")
|
||||
node.collapsed = true;
|
||||
}
|
||||
|
||||
// viewRootId=0 → Pet and Cat visible, Ball hidden
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(doc.tree, prov, 0);
|
||||
bool foundOwner = false, foundWhiskerLen = false, foundSpeed = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
int ni = lm.nodeIdx;
|
||||
if (ni < 0 || ni >= doc.tree.nodes.size()) continue;
|
||||
const QString& name = doc.tree.nodes[ni].name;
|
||||
if (name == "owner") foundOwner = true;
|
||||
if (name == "whiskerLen") foundWhiskerLen = true;
|
||||
if (name == "speed") foundSpeed = true;
|
||||
}
|
||||
QVERIFY2(foundOwner, "Pet's owner should appear with viewRootId=0");
|
||||
QVERIFY2(foundWhiskerLen, "Cat's whiskerLen should appear with viewRootId=0");
|
||||
QVERIFY2(!foundSpeed, "Collapsed Ball's speed should not appear with viewRootId=0");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Integration: Type aliases + compose + generator
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user