added project open save new

This commit is contained in:
megablocks(tm)
2026-02-07 11:27:46 -07:00
committed by sysadmin
parent 9ec06d9658
commit 39cac316de
12 changed files with 1266 additions and 92 deletions

View File

@@ -384,7 +384,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
} // anonymous namespace
ComposeResult compose(const NodeTree& tree, const Provider& prov) {
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId) {
ComposeState state;
// Precompute parent→children map
@@ -504,6 +504,12 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
});
for (int idx : roots) {
// 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);
}

View File

@@ -21,6 +21,14 @@
namespace rcx {
static thread_local const RcxDocument* s_composeDoc = nullptr;
static QString docTypeNameProvider(NodeKind k) {
if (s_composeDoc) return s_composeDoc->resolveTypeName(k);
auto* m = kindMeta(k);
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
}
static QString elide(QString s, int max) {
if (max <= 0) return {};
if (s.size() <= max) return s;
@@ -63,12 +71,21 @@ RcxDocument::RcxDocument(QObject* parent)
});
}
ComposeResult RcxDocument::compose() const {
return rcx::compose(tree, *provider);
ComposeResult RcxDocument::compose(uint64_t viewRootId) const {
return rcx::compose(tree, *provider, viewRootId);
}
bool RcxDocument::save(const QString& path) {
QJsonObject json = tree.toJson();
// Save type aliases
if (!typeAliases.isEmpty()) {
QJsonObject aliasObj;
for (auto it = typeAliases.begin(); it != typeAliases.end(); ++it)
aliasObj[kindToString(it.key())] = it.value();
json["typeAliases"] = aliasObj;
}
QJsonDocument jdoc(json);
QFile file(path);
if (!file.open(QIODevice::WriteOnly))
@@ -86,7 +103,19 @@ bool RcxDocument::load(const QString& path) {
return false;
undoStack.clear();
QJsonDocument jdoc = QJsonDocument::fromJson(file.readAll());
tree = NodeTree::fromJson(jdoc.object());
QJsonObject root = jdoc.object();
tree = NodeTree::fromJson(root);
// Load type aliases
typeAliases.clear();
QJsonObject aliasObj = root["typeAliases"].toObject();
for (auto it = aliasObj.begin(); it != aliasObj.end(); ++it) {
NodeKind k = kindFromString(it.key());
QString v = it.value().toString();
if (!v.isEmpty())
typeAliases[k] = v;
}
filePath = path;
modified = false;
emit documentChanged();
@@ -125,6 +154,7 @@ void RcxCommand::redo() { m_ctrl->applyCommand(m_cmd, false); }
RcxController::RcxController(RcxDocument* doc, QWidget* parent)
: QObject(parent), m_doc(doc)
{
fmt::setTypeNameProvider(docTypeNameProvider);
connect(m_doc, &RcxDocument::documentChanged, this, &RcxController::refresh);
setupAutoRefresh();
}
@@ -469,12 +499,28 @@ void RcxController::connectEditor(RcxEditor* editor) {
this, [this]() { refresh(); });
}
void RcxController::setViewRootId(uint64_t id) {
if (m_viewRootId == id) return;
m_viewRootId = id;
refresh();
}
void RcxController::scrollToNodeId(uint64_t nodeId) {
if (auto* editor = primaryEditor())
editor->scrollToNodeId(nodeId);
}
void RcxController::refresh() {
// Bracket compose with thread-local doc pointer for type name resolution
s_composeDoc = m_doc;
// Compose against snapshot provider if active, otherwise real provider
if (m_snapshotProv)
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv);
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId);
else
m_lastResult = m_doc->compose();
m_lastResult = m_doc->compose(m_viewRootId);
s_composeDoc = nullptr;
// Mark lines whose node data changed since last refresh
if (!m_changedOffsets.isEmpty()) {

View File

@@ -28,8 +28,17 @@ public:
QString filePath;
QString dataPath;
bool modified = false;
QHash<NodeKind, QString> typeAliases;
ComposeResult compose() const;
QString resolveTypeName(NodeKind kind) const {
auto it = typeAliases.find(kind);
if (it != typeAliases.end() && !it.value().isEmpty())
return it.value();
auto* m = kindMeta(kind);
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
}
ComposeResult compose(uint64_t viewRootId = 0) const;
bool save(const QString& path);
bool load(const QString& path);
void loadData(const QString& binaryPath);
@@ -96,6 +105,10 @@ public:
void applySelectionOverlays();
QSet<uint64_t> selectedIds() const { return m_selIds; }
void setViewRootId(uint64_t id);
uint64_t viewRootId() const { return m_viewRootId; }
void scrollToNodeId(uint64_t nodeId);
RcxDocument* document() const { return m_doc; }
void setEditorFont(const QString& fontName);
@@ -110,6 +123,7 @@ private:
QSet<uint64_t> m_selIds;
int m_anchorLine = -1;
bool m_suppressRefresh = false;
uint64_t m_viewRootId = 0;
// ── Saved sources for quick-switch ──
QVector<SavedSourceEntry> m_savedSources;

View File

@@ -747,6 +747,6 @@ namespace fmt {
// ── Compose function forward declaration ──
ComposeResult compose(const NodeTree& tree, const Provider& prov);
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0);
} // namespace rcx

View File

@@ -524,6 +524,16 @@ int RcxEditor::currentNodeIndex() const {
return lm ? lm->nodeIdx : -1;
}
void RcxEditor::scrollToNodeId(uint64_t nodeId) {
for (int i = 0; i < m_meta.size(); i++) {
if (m_meta[i].nodeId == nodeId && m_meta[i].lineKind != LineKind::Footer) {
m_sci->setCursorPosition(i, 0);
m_sci->ensureLineVisible(i);
return;
}
}
}
// ── Column span computation ──
ColumnSpan RcxEditor::typeSpan(const LineMeta& lm, int typeW) { return typeSpanFor(lm, typeW); }

View File

@@ -28,6 +28,7 @@ public:
QsciScintilla* scintilla() const { return m_sci; }
const LineMeta* metaForLine(int line) const;
int currentNodeIndex() const;
void scrollToNodeId(uint64_t nodeId);
// ── Column span computation ──
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);

View File

@@ -66,11 +66,22 @@ struct GenContext {
QSet<uint64_t> forwardDeclared; // forward-declared type IDs
QString output;
int padCounter = 0;
const QHash<NodeKind, QString>* typeAliases = nullptr;
QString uniquePadName() {
return QStringLiteral("_pad%1").arg(padCounter++, 4, 16, QChar('0'));
}
// Resolve the C type name for a primitive, consulting aliases first
QString cType(NodeKind kind) const {
if (typeAliases) {
auto it = typeAliases->find(kind);
if (it != typeAliases->end() && !it.value().isEmpty())
return it.value();
}
return cTypeName(kind);
}
// Resolve the canonical type name for a struct/array node
QString structName(const Node& n) const {
if (!n.structTypeName.isEmpty()) return sanitizeIdent(n.structTypeName);
@@ -92,28 +103,28 @@ static QString emitField(GenContext& ctx, const Node& node) {
switch (node.kind) {
case NodeKind::Vec2:
return QStringLiteral(" float %1[2];").arg(name);
return QStringLiteral(" %1 %2[2];").arg(ctx.cType(NodeKind::Float), name);
case NodeKind::Vec3:
return QStringLiteral(" float %1[3];").arg(name);
return QStringLiteral(" %1 %2[3];").arg(ctx.cType(NodeKind::Float), name);
case NodeKind::Vec4:
return QStringLiteral(" float %1[4];").arg(name);
return QStringLiteral(" %1 %2[4];").arg(ctx.cType(NodeKind::Float), name);
case NodeKind::Mat4x4:
return QStringLiteral(" float %1[4][4];").arg(name);
return QStringLiteral(" %1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name);
case NodeKind::UTF8:
return QStringLiteral(" char %1[%2];").arg(name).arg(node.strLen);
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen);
case NodeKind::UTF16:
return QStringLiteral(" wchar_t %1[%2];").arg(name).arg(node.strLen);
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen);
case NodeKind::Padding:
return QStringLiteral(" uint8_t %1[%2];").arg(name).arg(qMax(1, node.arrayLen));
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::Padding), name).arg(qMax(1, node.arrayLen));
case NodeKind::Pointer32: {
if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
QString target = ctx.structName(tree.nodes[refIdx]);
return QStringLiteral(" uint32_t %1; // -> %2*").arg(name, target);
return QStringLiteral(" %1 %2; // -> %3*").arg(ctx.cType(NodeKind::Pointer32), name, target);
}
}
return QStringLiteral(" uint32_t %1;").arg(name);
return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name);
}
case NodeKind::Pointer64: {
if (node.refId != 0) {
@@ -126,7 +137,7 @@ static QString emitField(GenContext& ctx, const Node& node) {
return QStringLiteral(" void* %1;").arg(name);
}
default:
return QStringLiteral(" %1 %2;").arg(cTypeName(node.kind), name);
return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name);
}
}
@@ -157,7 +168,8 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
// Gap before this field
if (child.offset > cursor) {
int gap = child.offset - cursor;
ctx.output += QStringLiteral(" uint8_t %1[0x%2];\n")
ctx.output += QStringLiteral(" %1 %2[0x%3];\n")
.arg(ctx.cType(NodeKind::Padding))
.arg(ctx.uniquePadName())
.arg(QString::number(gap, 16).toUpper());
} else if (child.offset < cursor) {
@@ -195,7 +207,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
.arg(elemTypeName, fieldName).arg(child.arrayLen);
} else {
ctx.output += QStringLiteral(" %1 %2[%3];\n")
.arg(cTypeName(child.elementKind), fieldName).arg(child.arrayLen);
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen);
}
} else {
ctx.output += emitField(ctx, child) + QStringLiteral("\n");
@@ -208,7 +220,8 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
// Tail padding
if (cursor < structSize) {
int gap = structSize - cursor;
ctx.output += QStringLiteral(" uint8_t %1[0x%2];\n")
ctx.output += QStringLiteral(" %1 %2[0x%3];\n")
.arg(ctx.cType(NodeKind::Padding))
.arg(ctx.uniquePadName())
.arg(QString::number(gap, 16).toUpper());
}
@@ -321,14 +334,15 @@ static QString nodePath(const NodeTree& tree, uint64_t nodeId) {
// ── Public API ──
QString renderCpp(const NodeTree& tree, uint64_t rootStructId) {
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases) {
int idx = tree.indexOfId(rootStructId);
if (idx < 0) return {};
const Node& root = tree.nodes[idx];
if (root.kind != NodeKind::Struct) return {};
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0};
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
int rootSize = tree.structSpan(rootStructId, &ctx.childMap);
QString typeName = ctx.structName(root);
@@ -345,8 +359,9 @@ QString renderCpp(const NodeTree& tree, uint64_t rootStructId) {
return ctx.output;
}
QString renderCppAll(const NodeTree& tree) {
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0};
QString renderCppAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases) {
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
ctx.output += QStringLiteral("// Generated by ReclassX\n");
ctx.output += QStringLiteral("// Full SDK export\n\n");

View File

@@ -1,16 +1,19 @@
#pragma once
#include "core.h"
#include <QString>
#include <QHash>
#include <QSet>
namespace rcx {
// Generate C++ struct definitions for a single root struct and all
// nested/referenced types reachable from it.
QString renderCpp(const NodeTree& tree, uint64_t rootStructId);
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr);
// Generate C++ struct definitions for every root-level struct (full SDK).
QString renderCppAll(const NodeTree& tree);
QString renderCppAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr);
// Null generator placeholder (returns empty string).
QString renderNull(const NodeTree& tree, uint64_t rootStructId);

View File

@@ -25,6 +25,15 @@
#include <QPainter>
#include <QSvgRenderer>
#include <QSettings>
#include <QDockWidget>
#include <QTreeView>
#include <QStandardItemModel>
#include "workspace_model.h"
#include <QTableWidget>
#include <QHeaderView>
#include <QDialogButtonBox>
#include <QVBoxLayout>
#include <QDialog>
#include <Qsci/qsciscintilla.h>
#include <Qsci/qscilexercpp.h>
@@ -148,6 +157,14 @@ private slots:
void about();
void setEditorFont(const QString& fontName);
void exportCpp();
void showTypeAliasesDialog();
public:
// Project Lifecycle API
QMdiSubWindow* project_new();
QMdiSubWindow* project_open(const QString& path = {});
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
void project_close(QMdiSubWindow* sub = nullptr);
private:
enum ViewMode { VM_Reclass, VM_Rendered };
@@ -184,6 +201,13 @@ private:
void syncRenderMenuState();
uint64_t findRootStructForNode(const NodeTree& tree, uint64_t nodeId) const;
void setupRenderedSci(QsciScintilla* sci);
// Workspace dock
QDockWidget* m_workspaceDock = nullptr;
QTreeView* m_workspaceTree = nullptr;
QStandardItemModel* m_workspaceModel = nullptr;
void createWorkspaceDock();
void rebuildWorkspaceModel();
};
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
@@ -198,11 +222,13 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
createMenus();
createStatusBar();
createWorkspaceDock();
connect(m_mdiArea, &QMdiArea::subWindowActivated,
this, [this](QMdiSubWindow*) {
updateWindowTitle();
syncRenderMenuState();
rebuildWorkspaceModel();
});
}
@@ -244,6 +270,8 @@ void MainWindow::createMenus() {
auto* edit = menuBar()->addMenu("&Edit");
edit->addAction(makeIcon(":/vsicons/arrow-left.svg"), "&Undo", QKeySequence::Undo, this, &MainWindow::undo);
edit->addAction(makeIcon(":/vsicons/arrow-right.svg"), "&Redo", QKeySequence::Redo, this, &MainWindow::redo);
edit->addSeparator();
edit->addAction(makeIcon(":/vsicons/symbol-structure.svg"), "&Type Aliases...", this, &MainWindow::showTypeAliasesDialog);
// View
auto* view = menuBar()->addMenu("&View");
@@ -319,6 +347,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
it->doc->deleteLater();
m_tabs.erase(it);
}
rebuildWorkspaceModel();
});
connect(ctrl, &RcxController::nodeSelected,
@@ -354,7 +383,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
m_statusLabel->setText(QString("%1 nodes selected").arg(count));
});
// Update rendered view on document changes and undo/redo
// Update rendered view and workspace on document changes and undo/redo
connect(doc, &RcxDocument::documentChanged,
this, [this, sub]() {
auto it = m_tabs.find(sub);
@@ -362,6 +391,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
QTimer::singleShot(0, this, [this, sub]() {
auto it2 = m_tabs.find(sub);
if (it2 != m_tabs.end()) updateRenderedView(*it2);
rebuildWorkspaceModel();
});
});
connect(&doc->undoStack, &QUndoStack::indexChanged,
@@ -375,31 +405,12 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
});
ctrl->refresh();
rebuildWorkspaceModel();
return sub;
}
void MainWindow::newFile() {
auto* doc = new RcxDocument(this);
QByteArray data(16, '\0');
doc->loadData(data);
doc->tree.baseAddress = 0x00400000;
Node root;
root.kind = NodeKind::Struct;
root.name = "Entity";
root.structTypeName = "Entity";
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 = "health"; n.parentId = rootId; n.offset = 0; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Int32; n.name = "armor"; n.parentId = rootId; n.offset = 4; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = rootId; n.offset = 8; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "flags"; n.parentId = rootId; n.offset = 12; doc->tree.addNode(n); }
createTab(doc);
project_new();
}
void MainWindow::selfTest() {
@@ -420,22 +431,97 @@ void MainWindow::selfTest() {
hProc, base, kTestDataSize, "ReclassX.exe");
doc->tree.baseAddress = base;
Node root;
root.kind = NodeKind::Struct;
root.name = "MyClass";
root.structTypeName = "MyClass";
root.parentId = 0;
root.offset = 0;
int ri = doc->tree.addNode(root);
uint64_t rootId = doc->tree.nodes[ri].id;
// ── 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;
for (int i = 0; i < 16; i++) {
Node n;
n.kind = NodeKind::Hex64;
n.name = QStringLiteral("field_%1").arg(i);
n.parentId = rootId;
n.offset = i * 8;
doc->tree.addNode(n);
{ 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); }
}
// ── 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);
@@ -443,35 +529,15 @@ void MainWindow::selfTest() {
}
void MainWindow::openFile() {
QString path = QFileDialog::getOpenFileName(this,
"Open Definition", {}, "ReclassX (*.rcx);;JSON (*.json);;All (*)");
if (path.isEmpty()) return;
auto* doc = new RcxDocument(this);
if (!doc->load(path)) {
QMessageBox::warning(this, "Error", "Failed to load: " + path);
delete doc;
return;
}
createTab(doc);
project_open();
}
void MainWindow::saveFile() {
auto* tab = activeTab();
if (!tab) return;
if (tab->doc->filePath.isEmpty()) { saveFileAs(); return; }
tab->doc->save(tab->doc->filePath);
updateWindowTitle();
project_save(nullptr, false);
}
void MainWindow::saveFileAs() {
auto* tab = activeTab();
if (!tab) return;
QString path = QFileDialog::getSaveFileName(this,
"Save Definition", {}, "ReclassX (*.rcx);;JSON (*.json)");
if (path.isEmpty()) return;
tab->doc->save(path);
updateWindowTitle();
project_save(nullptr, true);
}
void MainWindow::loadBinary() {
@@ -576,12 +642,12 @@ void MainWindow::about() {
void MainWindow::setEditorFont(const QString& fontName) {
QSettings settings("ReclassX", "ReclassX");
settings.setValue("font", fontName);
QFont f(fontName, 12);
f.setFixedPitch(true);
for (auto& state : m_tabs) {
state.ctrl->setEditorFont(fontName);
// Also update the rendered view font
if (state.rendered) {
QFont f(fontName, 12);
f.setFixedPitch(true);
state.rendered->setFont(f);
if (auto* lex = state.rendered->lexer()) {
lex->setFont(f);
@@ -591,6 +657,9 @@ void MainWindow::setEditorFont(const QString& fontName) {
state.rendered->setMarginsFont(f);
}
}
// Sync workspace tree font
if (m_workspaceTree)
m_workspaceTree->setFont(f);
}
RcxController* MainWindow::activeController() const {
@@ -732,11 +801,13 @@ void MainWindow::updateRenderedView(TabState& tab) {
}
// Generate text
const QHash<NodeKind, QString>* aliases =
tab.doc->typeAliases.isEmpty() ? nullptr : &tab.doc->typeAliases;
QString text;
if (rootId != 0)
text = renderCpp(tab.doc->tree, rootId);
text = renderCpp(tab.doc->tree, rootId, aliases);
else
text = renderCppAll(tab.doc->tree);
text = renderCppAll(tab.doc->tree, aliases);
// Scroll restoration: save if same root, reset if different
int restoreLine = 0;
@@ -773,7 +844,9 @@ void MainWindow::exportCpp() {
"Export C++ Header", {}, "C++ Header (*.h);;All Files (*)");
if (path.isEmpty()) return;
QString text = renderCppAll(tab->doc->tree);
const QHash<NodeKind, QString>* aliases =
tab->doc->typeAliases.isEmpty() ? nullptr : &tab->doc->typeAliases;
QString text = renderCppAll(tab->doc->tree, aliases);
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, "Export Failed",
@@ -784,6 +857,222 @@ void MainWindow::exportCpp() {
m_statusLabel->setText("Exported to " + QFileInfo(path).fileName());
}
// ── Type Aliases Dialog ──
void MainWindow::showTypeAliasesDialog() {
auto* tab = activeTab();
if (!tab) return;
QDialog dlg(this);
dlg.setWindowTitle("Type Aliases");
dlg.resize(500, 400);
auto* layout = new QVBoxLayout(&dlg);
auto* table = new QTableWidget(&dlg);
table->setColumnCount(2);
table->setHorizontalHeaderLabels({"NodeKind", "Alias (C type)"});
table->horizontalHeader()->setStretchLastSection(true);
table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
table->setSelectionMode(QAbstractItemView::SingleSelection);
// Populate with all NodeKind entries
int rowCount = static_cast<int>(std::size(kKindMeta));
table->setRowCount(rowCount);
for (int i = 0; i < rowCount; i++) {
const auto& meta = kKindMeta[i];
auto* kindItem = new QTableWidgetItem(QString::fromLatin1(meta.name));
kindItem->setFlags(kindItem->flags() & ~Qt::ItemIsEditable);
table->setItem(i, 0, kindItem);
QString alias = tab->doc->typeAliases.value(meta.kind);
table->setItem(i, 1, new QTableWidgetItem(alias));
}
layout->addWidget(table);
auto* buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
if (dlg.exec() != QDialog::Accepted) return;
// Collect new aliases
QHash<NodeKind, QString> newAliases;
for (int i = 0; i < rowCount; i++) {
QString val = table->item(i, 1)->text().trimmed();
if (!val.isEmpty())
newAliases[kKindMeta[i].kind] = val;
}
tab->doc->typeAliases = newAliases;
tab->doc->modified = true;
tab->ctrl->refresh();
updateWindowTitle();
}
// ── Project Lifecycle API ──
QMdiSubWindow* MainWindow::project_new() {
auto* doc = new RcxDocument(this);
QByteArray data(16, '\0');
doc->loadData(data);
doc->tree.baseAddress = 0x00400000;
Node root;
root.kind = NodeKind::Struct;
root.name = "Entity";
root.structTypeName = "Entity";
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 = "health"; n.parentId = rootId; n.offset = 0; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Int32; n.name = "armor"; n.parentId = rootId; n.offset = 4; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = rootId; n.offset = 8; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "flags"; n.parentId = rootId; n.offset = 12; doc->tree.addNode(n); }
auto* sub = createTab(doc);
rebuildWorkspaceModel();
return sub;
}
QMdiSubWindow* MainWindow::project_open(const QString& path) {
QString filePath = path;
if (filePath.isEmpty()) {
filePath = QFileDialog::getOpenFileName(this,
"Open Definition", {}, "ReclassX (*.rcx);;JSON (*.json);;All (*)");
if (filePath.isEmpty()) return nullptr;
}
auto* doc = new RcxDocument(this);
if (!doc->load(filePath)) {
QMessageBox::warning(this, "Error", "Failed to load: " + filePath);
delete doc;
return nullptr;
}
auto* sub = createTab(doc);
rebuildWorkspaceModel();
return sub;
}
bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) {
if (!sub) sub = m_mdiArea->activeSubWindow();
if (!sub || !m_tabs.contains(sub)) return false;
auto& tab = m_tabs[sub];
if (saveAs || tab.doc->filePath.isEmpty()) {
QString path = QFileDialog::getSaveFileName(this,
"Save Definition", {}, "ReclassX (*.rcx);;JSON (*.json)");
if (path.isEmpty()) return false;
tab.doc->save(path);
} else {
tab.doc->save(tab.doc->filePath);
}
updateWindowTitle();
return true;
}
void MainWindow::project_close(QMdiSubWindow* sub) {
if (!sub) sub = m_mdiArea->activeSubWindow();
if (!sub) return;
sub->close();
rebuildWorkspaceModel();
}
// ── Workspace Dock ──
void MainWindow::createWorkspaceDock() {
m_workspaceDock = new QDockWidget("Workspace", this);
m_workspaceDock->setObjectName("WorkspaceDock");
m_workspaceDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
m_workspaceTree = new QTreeView(m_workspaceDock);
m_workspaceModel = new QStandardItemModel(this);
m_workspaceModel->setHorizontalHeaderLabels({"Name"});
m_workspaceTree->setModel(m_workspaceModel);
m_workspaceTree->setHeaderHidden(true);
m_workspaceTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
// Match editor font
{
QSettings settings("ReclassX", "ReclassX");
QString fontName = settings.value("font", "Consolas").toString();
QFont f(fontName, 12);
f.setFixedPitch(true);
m_workspaceTree->setFont(f);
}
m_workspaceDock->setWidget(m_workspaceTree);
addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock);
connect(m_workspaceTree, &QTreeView::doubleClicked, this, [this](const QModelIndex& index) {
// Data roles: UserRole=QMdiSubWindow*, UserRole+1=structId, UserRole+2=nodeId
auto subVar = index.data(Qt::UserRole);
if (!subVar.isValid()) return;
auto* sub = static_cast<QMdiSubWindow*>(subVar.value<void*>());
if (!sub || !m_tabs.contains(sub)) return;
m_mdiArea->setActiveSubWindow(sub);
auto structIdVar = index.data(Qt::UserRole + 1);
auto nodeIdVar = index.data(Qt::UserRole + 2);
if (structIdVar.isValid()) {
// Double-clicked a struct: set as view root
uint64_t structId = structIdVar.toULongLong();
auto& tree = m_tabs[sub].doc->tree;
int ni = tree.indexOfId(structId);
if (ni >= 0) tree.nodes[ni].collapsed = false;
m_tabs[sub].ctrl->setViewRootId(structId);
m_tabs[sub].ctrl->scrollToNodeId(structId);
} else if (nodeIdVar.isValid()) {
// Double-clicked a field: find its root struct, set as view root, scroll to field
uint64_t nodeId = nodeIdVar.toULongLong();
auto& tree = m_tabs[sub].doc->tree;
// Walk up to find root struct
uint64_t rootId = 0;
uint64_t cur = nodeId;
while (cur != 0) {
int idx = tree.indexOfId(cur);
if (idx < 0) break;
if (tree.nodes[idx].parentId == 0) { rootId = cur; break; }
cur = tree.nodes[idx].parentId;
}
if (rootId != 0) {
int ri = tree.indexOfId(rootId);
if (ri >= 0) tree.nodes[ri].collapsed = false;
m_tabs[sub].ctrl->setViewRootId(rootId);
}
m_tabs[sub].ctrl->scrollToNodeId(nodeId);
} else if (!index.parent().isValid()) {
// Double-clicked project root: clear view root to show all
m_tabs[sub].ctrl->setViewRootId(0);
}
});
}
void MainWindow::rebuildWorkspaceModel() {
m_workspaceModel->clear();
auto* sub = m_mdiArea->activeSubWindow();
if (!sub || !m_tabs.contains(sub)) return;
TabState& tab = m_tabs[sub];
QString tabName = tab.doc->filePath.isEmpty()
? "Untitled" : QFileInfo(tab.doc->filePath).fileName();
buildWorkspaceModel(m_workspaceModel, tab.doc->tree, tabName,
static_cast<void*>(sub));
m_workspaceTree->expandAll();
}
} // namespace rcx
// ── Entry point ──

64
src/workspace_model.h Normal file
View File

@@ -0,0 +1,64 @@
#pragma once
#include "core.h"
#include <QStandardItemModel>
#include <QStandardItem>
#include <algorithm>
namespace rcx {
// Recursively add children of parentId as tree items under parentItem.
inline void addWorkspaceChildren(QStandardItem* parentItem,
const NodeTree& tree,
uint64_t parentId,
void* subPtr) {
QVector<int> children = tree.childrenOf(parentId);
std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
for (int idx : children) {
const Node& node = tree.nodes[idx];
// Skip hex preview nodes — they are padding/filler, not meaningful fields
if (isHexNode(node.kind)) continue;
QString display;
if (node.kind == NodeKind::Struct) {
QString typeName = node.structTypeName.isEmpty()
? node.name : node.structTypeName;
display = QStringLiteral("%1 (%2)")
.arg(typeName, node.resolvedClassKeyword());
} else {
display = QStringLiteral("%1 (%2)")
.arg(node.name, QString::fromLatin1(kindToString(node.kind)));
}
auto* item = new QStandardItem(display);
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
if (node.kind == NodeKind::Struct)
item->setData(QVariant::fromValue(node.id), Qt::UserRole + 1);
item->setData(QVariant::fromValue(node.id), Qt::UserRole + 2); // nodeId for scroll
if (node.kind == NodeKind::Struct)
addWorkspaceChildren(item, tree, node.id, subPtr);
parentItem->appendRow(item);
}
}
inline void buildWorkspaceModel(QStandardItemModel* model,
const NodeTree& tree,
const QString& projectName,
void* subPtr = nullptr) {
model->clear();
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
auto* projectItem = new QStandardItem(projectName);
projectItem->setData(QVariant::fromValue(subPtr), Qt::UserRole);
addWorkspaceChildren(projectItem, tree, 0, subPtr);
model->appendRow(projectItem);
}
} // namespace rcx