Add type selector popup, view root switching, and new type creation

- Type selector chevron [▸] on command row opens searchable popup
- Popup lists all root structs with filter, keyboard nav, side-triangle indicator
- Selecting a type switches the editor view via setViewRootId
- "Create new type" inserts a new root struct with no name
- Command row displays the active view root's name
- Tests for chevron detection, span compatibility, view switching, undo
This commit is contained in:
batallion2
2026-02-09 12:21:03 -07:00
committed by sysadmin
parent 0e65b9997e
commit f4149faa9a
15 changed files with 1611 additions and 291 deletions

View File

@@ -33,6 +33,8 @@ add_executable(ReclassX
src/providerregistry.h src/providerregistry.h
src/pluginmanager.cpp src/pluginmanager.cpp
src/pluginmanager.h src/pluginmanager.h
src/typeselectorpopup.h
src/typeselectorpopup.cpp
) )
target_include_directories(ReclassX PRIVATE src) target_include_directories(ReclassX PRIVATE src)
@@ -136,7 +138,8 @@ if(BUILD_TESTING)
add_executable(test_controller tests/test_controller.cpp add_executable(test_controller tests/test_controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp)
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::Concurrent Qt6::Test Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
@@ -145,7 +148,8 @@ if(BUILD_TESTING)
add_executable(test_validation tests/test_validation.cpp add_executable(test_validation tests/test_validation.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp)
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::Concurrent Qt6::Test Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
@@ -160,20 +164,40 @@ if(BUILD_TESTING)
add_executable(test_context_menu tests/test_context_menu.cpp add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp)
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::Concurrent 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)
add_executable(test_rendered_view tests/test_rendered_view.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
target_include_directories(test_rendered_view PRIVATE src)
target_link_libraries(test_rendered_view PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Test
QScintilla::QScintilla)
add_test(NAME test_rendered_view COMMAND test_rendered_view)
add_executable(test_new_features tests/test_new_features.cpp add_executable(test_new_features tests/test_new_features.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp)
target_include_directories(test_new_features PRIVATE src) target_include_directories(test_new_features PRIVATE src)
target_link_libraries(test_new_features PRIVATE target_link_libraries(test_new_features PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi) QScintilla::QScintilla dbghelp psapi)
add_test(NAME test_new_features COMMAND test_new_features) add_test(NAME test_new_features COMMAND test_new_features)
add_executable(test_type_selector tests/test_type_selector.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp)
target_include_directories(test_type_selector PRIVATE src)
target_link_libraries(test_type_selector PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi)
add_test(NAME test_type_selector COMMAND test_type_selector)
endif() endif()
add_subdirectory(plugins/ProcessMemory) add_subdirectory(plugins/ProcessMemory)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -348,9 +348,17 @@ void composeNode(ComposeState& state, const NodeTree& tree,
} }
} }
// Show referenced struct children: at dereferenced address if non-NULL, // Determine if pointer target is actually readable
// otherwise at offset 0 as a struct template preview
uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0; uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0;
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
// For invalid/unreadable pointers: use NullProvider (shows zeros)
// and reset margin offsets (unsigned wrap cancels baseAddress)
static NullProvider s_nullProv;
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv);
if (!ptrReadable)
pBase = (uint64_t)0 - tree.baseAddress;
qulonglong key = pBase ^ (node.refId * kGoldenRatio); qulonglong key = pBase ^ (node.refId * kGoldenRatio);
if (!state.ptrVisiting.contains(key)) { if (!state.ptrVisiting.contains(key)) {
state.ptrVisiting.insert(key); state.ptrVisiting.insert(key);
@@ -358,7 +366,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
if (refIdx >= 0) { if (refIdx >= 0) {
const Node& ref = tree.nodes[refIdx]; const Node& ref = tree.nodes[refIdx];
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array)
composeParent(state, tree, prov, refIdx, composeParent(state, tree, childProv, refIdx,
depth, pBase, ref.id, depth, pBase, ref.id,
/*isArrayChild=*/true); /*isArrayChild=*/true);
} }
@@ -474,7 +482,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
} }
// Emit CommandRow as line 0 (combined: source + address + root class type + name) // Emit CommandRow as line 0 (combined: source + address + root class type + name)
const QString cmdRowText = QStringLiteral("source\u25BE \u00B7 0x0 \u00B7 struct\u25BE <no class> {"); const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct\u25BE <no class> {");
{ {
LineMeta lm; LineMeta lm;
lm.nodeIdx = -1; lm.nodeIdx = -1;

View File

@@ -1,4 +1,5 @@
#include "controller.h" #include "controller.h"
#include "typeselectorpopup.h"
#include "providers/process_provider.h" #include "providers/process_provider.h"
#include "providerregistry.h" #include "providerregistry.h"
#include "processpicker.h" #include "processpicker.h"
@@ -15,6 +16,7 @@
#include <QApplication> #include <QApplication>
#include <QFileDialog> #include <QFileDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QSettings>
#include <QtConcurrent/QtConcurrentRun> #include <QtConcurrent/QtConcurrentRun>
#ifdef _WIN32 #ifdef _WIN32
#include <psapi.h> #include <psapi.h>
@@ -171,9 +173,8 @@ RcxEditor* RcxController::primaryEditor() const {
return m_editors.isEmpty() ? nullptr : m_editors.first(); return m_editors.isEmpty() ? nullptr : m_editors.first();
} }
RcxEditor* RcxController::addSplitEditor(QSplitter* splitter) { RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
auto* editor = new RcxEditor(splitter); auto* editor = new RcxEditor(parent);
splitter->addWidget(editor);
m_editors.append(editor); m_editors.append(editor);
connectEditor(editor); connectEditor(editor);
@@ -186,7 +187,7 @@ RcxEditor* RcxController::addSplitEditor(QSplitter* splitter) {
void RcxController::removeSplitEditor(RcxEditor* editor) { void RcxController::removeSplitEditor(RcxEditor* editor) {
m_editors.removeOne(editor); m_editors.removeOne(editor);
editor->deleteLater(); // Caller (MainWindow) owns the parent QTabWidget and handles widget destruction.
} }
void RcxController::connectEditor(RcxEditor* editor) { void RcxController::connectEditor(RcxEditor* editor) {
@@ -203,6 +204,12 @@ void RcxController::connectEditor(RcxEditor* editor) {
handleNodeClick(editor, line, nodeId, mods); handleNodeClick(editor, line, nodeId, mods);
}); });
// Type selector popup
connect(editor, &RcxEditor::typeSelectorRequested,
this, [this, editor]() {
showTypeSelectorPopup(editor);
});
// Inline editing signals // Inline editing signals
connect(editor, &RcxEditor::inlineEditCommitted, connect(editor, &RcxEditor::inlineEditCommitted,
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) { this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) {
@@ -1054,16 +1061,12 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
editor->beginInlineEdit(EditTarget::Name, line); editor->beginInlineEdit(EditTarget::Name, line);
}); });
menu.addAction(icon("symbol-structure.svg"), "Change &Type\tT", [editor, line]() { menu.addAction("Change &Type\tT", [editor, line]() {
editor->beginInlineEdit(EditTarget::Type, line); editor->beginInlineEdit(EditTarget::Type, line);
}); });
menu.addSeparator(); menu.addSeparator();
menu.addAction(icon("add.svg"), "&Add Field Below\tInsert", [this, parentId]() {
insertNode(parentId, -1, NodeKind::Hex64, "newField");
});
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) { if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
menu.addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() { menu.addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() {
insertNode(nodeId, 0, NodeKind::Hex64, "newField"); insertNode(nodeId, 0, NodeKind::Hex64, "newField");
@@ -1157,14 +1160,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
} }
} }
menu.addAction(icon("add.svg"), "Add Hex64 at Root", [this]() {
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]() { menu.addAction(icon("diff-added.svg"), "Append 128 bytes", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0; uint64_t target = m_viewRootId ? m_viewRootId : 0;
m_suppressRefresh = true; m_suppressRefresh = true;
@@ -1450,22 +1445,35 @@ void RcxController::updateCommandRow() {
.arg(elide(src, 40), elide(addr, 24), elide(sym, 40)); .arg(elide(src, 40), elide(addr, 24), elide(sym, 40));
} }
// Build row 2: root class type + name // Build row 2: root class type + name (uses current view root)
QString row2; QString row2;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) { if (m_viewRootId != 0) {
const auto& n = m_doc->tree.nodes[i]; int vi = m_doc->tree.indexOfId(m_viewRootId);
if (n.parentId == 0 && n.kind == NodeKind::Struct) { if (vi >= 0) {
const auto& n = m_doc->tree.nodes[vi];
QString keyword = n.resolvedClassKeyword(); QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName; QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
row2 = QStringLiteral("%1\u25BE %2 {") row2 = QStringLiteral("%1\u25BE %2 {")
.arg(keyword, className); .arg(keyword, className.isEmpty() ? QStringLiteral("<no name>") : className);
break; }
}
if (row2.isEmpty()) {
// Fallback: find first root struct
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
const auto& n = m_doc->tree.nodes[i];
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
row2 = QStringLiteral("%1\u25BE %2 {")
.arg(keyword, className);
break;
}
} }
} }
if (row2.isEmpty()) if (row2.isEmpty())
row2 = QStringLiteral("struct\u25BE <no class> {"); row2 = QStringLiteral("struct\u25BE <no class> {");
QString combined = row + QStringLiteral(" \u00B7 ") + row2; QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" \u00B7 ") + row2;
for (auto* ed : m_editors) { for (auto* ed : m_editors) {
ed->setCommandRowText(combined); ed->setCommandRowText(combined);
@@ -1473,6 +1481,63 @@ void RcxController::updateCommandRow() {
emit selectionChanged(m_selIds.size()); emit selectionChanged(m_selIds.size());
} }
void RcxController::showTypeSelectorPopup(RcxEditor* editor) {
// Collect all root-level struct types
QVector<TypeEntry> types;
for (const auto& n : m_doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
TypeEntry entry;
entry.id = n.id;
entry.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
entry.classKeyword = n.resolvedClassKeyword();
types.append(entry);
}
}
// Get font with zoom
QSettings settings("ReclassX", "ReclassX");
QString fontName = settings.value("font", "Consolas").toString();
QFont font(fontName, 12);
font.setFixedPitch(true);
auto* sci = editor->scintilla();
int zoom = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
font.setPointSize(font.pointSize() + zoom);
// Position: bottom-left of the [▸] span on line 0
long lineStart = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0);
int lineH = (int)sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
int x = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
0, lineStart);
int y = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
0, lineStart);
QPoint pos = sci->viewport()->mapToGlobal(QPoint(x, y + lineH));
auto* popup = new TypeSelectorPopup(editor);
popup->setFont(font);
popup->setTypes(types, m_viewRootId);
connect(popup, &TypeSelectorPopup::typeSelected,
this, [this](uint64_t structId) {
setViewRootId(structId);
});
connect(popup, &TypeSelectorPopup::createNewTypeRequested,
this, [this]() {
// Create a new root struct with no name
Node n;
n.kind = NodeKind::Struct;
n.name = QString();
n.parentId = 0;
n.offset = 0;
n.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
setViewRootId(n.id);
});
connect(popup, &TypeSelectorPopup::dismissed,
popup, &QObject::deleteLater);
popup->popup(pos);
}
void RcxController::attachToProcess(uint32_t pid, const QString& processName) { void RcxController::attachToProcess(uint32_t pid, const QString& processName) {
#ifdef _WIN32 #ifdef _WIN32
HANDLE hProc = OpenProcess( HANDLE hProc = OpenProcess(

View File

@@ -9,8 +9,6 @@
#include <QFutureWatcher> #include <QFutureWatcher>
#include <memory> #include <memory>
class QSplitter;
namespace rcx { namespace rcx {
class RcxController; class RcxController;
@@ -80,7 +78,7 @@ public:
~RcxController() override; ~RcxController() override;
RcxEditor* primaryEditor() const; RcxEditor* primaryEditor() const;
RcxEditor* addSplitEditor(QSplitter* splitter); RcxEditor* addSplitEditor(QWidget* parent = nullptr);
void removeSplitEditor(RcxEditor* editor); void removeSplitEditor(RcxEditor* editor);
QList<RcxEditor*> editors() const { return m_editors; } QList<RcxEditor*> editors() const { return m_editors; }
@@ -146,6 +144,7 @@ private:
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();
void showTypeSelectorPopup(RcxEditor* editor);
// ── Auto-refresh methods ── // ── Auto-refresh methods ──
void setupAutoRefresh(); void setupAutoRefresh();

View File

@@ -489,7 +489,7 @@ struct ColumnSpan {
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount, enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount,
ArrayElementType, ArrayElementCount, PointerTarget, ArrayElementType, ArrayElementCount, PointerTarget,
RootClassType, RootClassName }; RootClassType, RootClassName, TypeSelector };
// Column layout constants (shared with format.cpp span computation) // Column layout constants (shared with format.cpp span computation)
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
@@ -635,6 +635,16 @@ inline ColumnSpan commandRowRootNameSpan(const QString& lineText) {
return {nameStart, nameEnd, true}; return {nameStart, nameEnd, true};
} }
// ── CommandRow type-selector chevron span ──
// Detects "[▸]" at the start of the command row text
inline ColumnSpan commandRowChevronSpan(const QString& lineText) {
if (lineText.size() < 3) return {};
if (lineText[0] == '[' && lineText[1] == QChar(0x25B8) && lineText[2] == ']')
return {0, 3, true};
return {};
}
// ── Array element type/count spans (within type column of array headers) ── // ── Array element type/count spans (within type column of array headers) ──
// Line format: " int32_t[10] name {" // Line format: " int32_t[10] name {"
// arrayElemTypeSpan covers "int32_t", arrayElemCountSpan covers "10" // arrayElemTypeSpan covers "int32_t", arrayElemCountSpan covers "10"

View File

@@ -650,6 +650,11 @@ void RcxEditor::applyCommandRowPills() {
clearIndicatorLine(IND_HEX_DIM, line); clearIndicatorLine(IND_HEX_DIM, line);
clearIndicatorLine(IND_CLASS_NAME, line); clearIndicatorLine(IND_CLASS_NAME, line);
// Dim the [▾] type-selector chevron
ColumnSpan chevron = commandRowChevronSpan(t);
if (chevron.valid)
fillIndicatorCols(IND_HEX_DIM, line, chevron.start, chevron.end);
// Dim label text: source arrow/placeholder + its ▾ dropdown arrow // Dim label text: source arrow/placeholder + its ▾ dropdown arrow
ColumnSpan srcSpan = commandRowSrcSpan(t); ColumnSpan srcSpan = commandRowSrcSpan(t);
if (srcSpan.valid) { if (srcSpan.valid) {
@@ -838,10 +843,12 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
// CommandRow: Source / BaseAddress / Root class (type+name) editing // CommandRow: Source / BaseAddress / Root class (type+name) editing
if (lm->lineKind == LineKind::CommandRow) { if (lm->lineKind == LineKind::CommandRow) {
if (t != EditTarget::BaseAddress && t != EditTarget::Source if (t != EditTarget::BaseAddress && t != EditTarget::Source
&& t != EditTarget::RootClassType && t != EditTarget::RootClassName) return false; && t != EditTarget::RootClassType && t != EditTarget::RootClassName
&& t != EditTarget::TypeSelector) return false;
QString lineText = getLineText(m_sci, line); QString lineText = getLineText(m_sci, line);
ColumnSpan s; ColumnSpan s;
if (t == EditTarget::Source) s = commandRowSrcSpan(lineText); if (t == EditTarget::TypeSelector) s = commandRowChevronSpan(lineText);
else if (t == EditTarget::Source) s = commandRowSrcSpan(lineText);
else if (t == EditTarget::BaseAddress) s = commandRowAddrSpan(lineText); else if (t == EditTarget::BaseAddress) s = commandRowAddrSpan(lineText);
else if (t == EditTarget::RootClassType) s = commandRowRootTypeSpan(lineText); else if (t == EditTarget::RootClassType) s = commandRowRootTypeSpan(lineText);
else s = commandRowRootNameSpan(lineText); else s = commandRowRootNameSpan(lineText);
@@ -959,8 +966,10 @@ static bool hitTestTarget(QsciScintilla* sci,
return s.valid && col >= s.start && col < s.end; return s.valid && col >= s.start && col < s.end;
}; };
// CommandRow: interactive SRC/ADDR + root class (type+name) // CommandRow: interactive chevron/SRC/ADDR + root class (type+name)
if (lm.lineKind == LineKind::CommandRow) { if (lm.lineKind == LineKind::CommandRow) {
ColumnSpan chevron = commandRowChevronSpan(lineText);
if (inSpan(chevron)) { outTarget = EditTarget::TypeSelector; outLine = line; return true; }
ColumnSpan ss = commandRowSrcSpan(lineText); ColumnSpan ss = commandRowSrcSpan(lineText);
if (inSpan(ss)) { outTarget = EditTarget::Source; outLine = line; return true; } if (inSpan(ss)) { outTarget = EditTarget::Source; outLine = line; return true; }
ColumnSpan as = commandRowAddrSpan(lineText); ColumnSpan as = commandRowAddrSpan(lineText);
@@ -1102,11 +1111,15 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
emit marginClicked(0, h.line, me->modifiers()); emit marginClicked(0, h.line, me->modifiers());
return true; return true;
} }
// CommandRow: try ADDR edit or consume // CommandRow: try chevron/ADDR edit or consume
if (h.nodeId == kCommandRowId) { if (h.nodeId == kCommandRowId) {
int tLine; EditTarget t; int tLine; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) {
beginInlineEdit(t, tLine); if (t == EditTarget::TypeSelector)
emit typeSelectorRequested();
else
beginInlineEdit(t, tLine);
}
return true; // consume all CommandRow clicks return true; // consume all CommandRow clicks
} }
if (h.nodeId != 0) { if (h.nodeId != 0) {
@@ -1369,6 +1382,7 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
// ── Begin inline edit ── // ── Begin inline edit ──
bool RcxEditor::beginInlineEdit(EditTarget target, int line) { bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
if (target == EditTarget::TypeSelector) return false; // handled by popup, not inline edit
if (m_editState.active) return false; if (m_editState.active) return false;
m_hoveredNodeId = 0; m_hoveredNodeId = 0;
m_hoveredLine = -1; m_hoveredLine = -1;
@@ -1937,6 +1951,7 @@ void RcxEditor::applyHoverCursor() {
case EditTarget::ArrayElementType: case EditTarget::ArrayElementType:
case EditTarget::PointerTarget: case EditTarget::PointerTarget:
case EditTarget::RootClassType: case EditTarget::RootClassType:
case EditTarget::TypeSelector:
desired = Qt::PointingHandCursor; desired = Qt::PointingHandCursor;
break; break;
default: default:

View File

@@ -61,6 +61,7 @@ signals:
void inlineEditCommitted(int nodeIdx, int subLine, void inlineEditCommitted(int nodeIdx, int subLine,
EditTarget target, const QString& text); EditTarget target, const QString& text);
void inlineEditCancelled(); void inlineEditCancelled();
void typeSelectorRequested();
protected: protected:
bool eventFilter(QObject* obj, QEvent* event) override; bool eventFilter(QObject* obj, QEvent* event) override;

View File

@@ -93,51 +93,61 @@ struct GenContext {
// Forward declarations // Forward declarations
static void emitStruct(GenContext& ctx, uint64_t structId); static void emitStruct(GenContext& ctx, uint64_t structId);
// ── Emit a single field declaration ── // ── Field line with offset comment (code + marker + comment) ──
// We use a \x01 marker to separate the code part from the offset comment.
// After all output is generated, alignComments() replaces markers with padding.
static const QChar kCommentMarker = QChar(0x01);
static QString offsetComment(int offset) {
return QString(kCommentMarker) + QStringLiteral("// 0x%1").arg(QString::number(offset, 16).toUpper());
}
static QString emitField(GenContext& ctx, const Node& node) { static QString emitField(GenContext& ctx, const Node& node) {
const NodeTree& tree = ctx.tree; const NodeTree& tree = ctx.tree;
QString name = sanitizeIdent(node.name.isEmpty() QString name = sanitizeIdent(node.name.isEmpty()
? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0')) ? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0'))
: node.name); : node.name);
QString oc = offsetComment(node.offset);
switch (node.kind) { switch (node.kind) {
case NodeKind::Vec2: case NodeKind::Vec2:
return QStringLiteral(" %1 %2[2];").arg(ctx.cType(NodeKind::Float), name); return QStringLiteral(" %1 %2[2];").arg(ctx.cType(NodeKind::Float), name) + oc;
case NodeKind::Vec3: case NodeKind::Vec3:
return QStringLiteral(" %1 %2[3];").arg(ctx.cType(NodeKind::Float), name); return QStringLiteral(" %1 %2[3];").arg(ctx.cType(NodeKind::Float), name) + oc;
case NodeKind::Vec4: case NodeKind::Vec4:
return QStringLiteral(" %1 %2[4];").arg(ctx.cType(NodeKind::Float), name); return QStringLiteral(" %1 %2[4];").arg(ctx.cType(NodeKind::Float), name) + oc;
case NodeKind::Mat4x4: case NodeKind::Mat4x4:
return QStringLiteral(" %1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name); return QStringLiteral(" %1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name) + oc;
case NodeKind::UTF8: case NodeKind::UTF8:
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen); return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
case NodeKind::UTF16: case NodeKind::UTF16:
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen); return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
case NodeKind::Padding: case NodeKind::Padding:
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::Padding), name).arg(qMax(1, node.arrayLen)); return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::Padding), name).arg(qMax(1, node.arrayLen)) + oc;
case NodeKind::Pointer32: { case NodeKind::Pointer32: {
if (node.refId != 0) { if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId); int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) { if (refIdx >= 0) {
QString target = ctx.structName(tree.nodes[refIdx]); QString target = ctx.structName(tree.nodes[refIdx]);
return QStringLiteral(" %1 %2; // -> %3*").arg(ctx.cType(NodeKind::Pointer32), name, target); return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) +
offsetComment(node.offset).replace(QStringLiteral("//"), QStringLiteral("// -> %1*").arg(target));
} }
} }
return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name); return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) + oc;
} }
case NodeKind::Pointer64: { case NodeKind::Pointer64: {
if (node.refId != 0) { if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId); int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) { if (refIdx >= 0) {
QString target = ctx.structName(tree.nodes[refIdx]); QString target = ctx.structName(tree.nodes[refIdx]);
return QStringLiteral(" %1* %2;").arg(target, name); return QStringLiteral(" %1* %2;").arg(target, name) + oc;
} }
} }
return QStringLiteral(" void* %1;").arg(name); return QStringLiteral(" void* %1;").arg(name) + oc;
} }
default: default:
return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name); return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name) + oc;
} }
} }
@@ -155,10 +165,21 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
return tree.nodes[a].offset < tree.nodes[b].offset; return tree.nodes[a].offset < tree.nodes[b].offset;
}); });
int cursor = 0; // Helper: emit a padding/hex run as a single collapsed byte array
auto emitPadRun = [&](int offset, int size) {
if (size <= 0) return;
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
.arg(ctx.cType(NodeKind::Padding))
.arg(ctx.uniquePadName())
.arg(QString::number(size, 16).toUpper())
.arg(offsetComment(offset));
};
for (int ci : children) { int cursor = 0;
const Node& child = tree.nodes[ci]; int i = 0;
while (i < children.size()) {
const Node& child = tree.nodes[children[i]];
int childSize; int childSize;
if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
childSize = tree.structSpan(child.id, &ctx.childMap); childSize = tree.structSpan(child.id, &ctx.childMap);
@@ -166,28 +187,40 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
childSize = child.byteSize(); childSize = child.byteSize();
// Gap before this field // Gap before this field
if (child.offset > cursor) { if (child.offset > cursor)
int gap = child.offset - cursor; emitPadRun(cursor, child.offset - cursor);
ctx.output += QStringLiteral(" %1 %2[0x%3];\n") else if (child.offset < cursor)
.arg(ctx.cType(NodeKind::Padding))
.arg(ctx.uniquePadName())
.arg(QString::number(gap, 16).toUpper());
} else if (child.offset < cursor) {
// Overlap
ctx.output += QStringLiteral(" // WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n") ctx.output += QStringLiteral(" // WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n")
.arg(QString::number(child.offset, 16).toUpper()) .arg(QString::number(child.offset, 16).toUpper())
.arg(QString::number(cursor, 16).toUpper()); .arg(QString::number(cursor, 16).toUpper());
// Collapse consecutive hex nodes into a single padding array
if (isHexNode(child.kind)) {
int runStart = child.offset;
int runEnd = child.offset + childSize;
int j = i + 1;
while (j < children.size()) {
const Node& next = tree.nodes[children[j]];
if (!isHexNode(next.kind)) break;
int nextSize = next.byteSize();
// Allow gaps within the run (they become part of the pad)
if (next.offset < runEnd) break; // overlap — stop merging
runEnd = next.offset + nextSize;
j++;
}
emitPadRun(runStart, runEnd - runStart);
cursor = runEnd;
i = j;
continue;
} }
// Emit the field // Emit the field
if (child.kind == NodeKind::Struct) { if (child.kind == NodeKind::Struct) {
// Ensure the nested struct type is emitted first
emitStruct(ctx, child.id); emitStruct(ctx, child.id);
QString typeName = ctx.structName(child); QString typeName = ctx.structName(child);
QString fieldName = sanitizeIdent(child.name); QString fieldName = sanitizeIdent(child.name);
ctx.output += QStringLiteral(" %1 %2;\n").arg(typeName, fieldName); ctx.output += QStringLiteral(" %1 %2;%3\n").arg(typeName, fieldName, offsetComment(child.offset));
} else if (child.kind == NodeKind::Array) { } else if (child.kind == NodeKind::Array) {
// Check if array has struct element children
QVector<int> arrayKids = ctx.childMap.value(child.id); QVector<int> arrayKids = ctx.childMap.value(child.id);
bool hasStructChild = false; bool hasStructChild = false;
QString elemTypeName; QString elemTypeName;
@@ -203,11 +236,11 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
QString fieldName = sanitizeIdent(child.name); QString fieldName = sanitizeIdent(child.name);
if (hasStructChild && !elemTypeName.isEmpty()) { if (hasStructChild && !elemTypeName.isEmpty()) {
ctx.output += QStringLiteral(" %1 %2[%3];\n") ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
.arg(elemTypeName, fieldName).arg(child.arrayLen); .arg(elemTypeName, fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
} else { } else {
ctx.output += QStringLiteral(" %1 %2[%3];\n") ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen); .arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
} }
} else { } else {
ctx.output += emitField(ctx, child) + QStringLiteral("\n"); ctx.output += emitField(ctx, child) + QStringLiteral("\n");
@@ -215,16 +248,12 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
int childEnd = child.offset + childSize; int childEnd = child.offset + childSize;
if (childEnd > cursor) cursor = childEnd; if (childEnd > cursor) cursor = childEnd;
i++;
} }
// Tail padding // Tail padding
if (cursor < structSize) { if (cursor < structSize)
int gap = structSize - cursor; emitPadRun(cursor, structSize - cursor);
ctx.output += QStringLiteral(" %1 %2[0x%3];\n")
.arg(ctx.cType(NodeKind::Padding))
.arg(ctx.uniquePadName())
.arg(QString::number(gap, 16).toUpper());
}
} }
// ── Emit a complete struct definition ── // ── Emit a complete struct definition ──
@@ -294,7 +323,6 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
ctx.emittedTypeNames.insert(typeName); ctx.emittedTypeNames.insert(typeName);
int structSize = ctx.tree.structSpan(structId, &ctx.childMap); int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
ctx.output += QStringLiteral("#pragma pack(push, 1)\n");
QString kw = node.resolvedClassKeyword(); QString kw = node.resolvedClassKeyword();
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName); ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
@@ -302,7 +330,6 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
emitStructBody(ctx, structId); emitStructBody(ctx, structId);
ctx.output += QStringLiteral("};\n"); ctx.output += QStringLiteral("};\n");
ctx.output += QStringLiteral("#pragma pack(pop)\n");
ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n\n") ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n\n")
.arg(typeName) .arg(typeName)
.arg(QString::number(structSize, 16).toUpper()); .arg(QString::number(structSize, 16).toUpper());
@@ -319,22 +346,39 @@ static QHash<uint64_t, QVector<int>> buildChildMap(const NodeTree& tree) {
return map; return map;
} }
// ── Path breadcrumb for header comment ── // ── Align offset comments ──
// Replaces kCommentMarker with spaces so all "// 0x..." comments align to
// the same column (the longest code portion + 1 space).
static QString nodePath(const NodeTree& tree, uint64_t nodeId) { static QString alignComments(const QString& raw) {
QStringList parts; QStringList lines = raw.split('\n');
QSet<uint64_t> seen;
uint64_t cur = nodeId; // First pass: find the maximum code width (text before the marker)
while (cur != 0 && !seen.contains(cur)) { int maxCode = 0;
seen.insert(cur); for (const QString& line : lines) {
int idx = tree.indexOfId(cur); int pos = line.indexOf(kCommentMarker);
if (idx < 0) break; if (pos >= 0)
const Node& n = tree.nodes[idx]; maxCode = qMax(maxCode, pos);
parts << (n.name.isEmpty() ? QStringLiteral("<unnamed>") : n.name);
cur = n.parentId;
} }
std::reverse(parts.begin(), parts.end());
return parts.join(QStringLiteral(" > ")); // Second pass: replace markers with padding
QString result;
result.reserve(raw.size() + lines.size() * 8);
for (int i = 0; i < lines.size(); i++) {
if (i > 0) result += '\n';
const QString& line = lines[i];
int pos = line.indexOf(kCommentMarker);
if (pos >= 0) {
result += line.left(pos);
int pad = maxCode - pos + 1;
if (pad < 1) pad = 1;
result += QString(pad, ' ');
result += line.mid(pos + 1); // skip the marker char
} else {
result += line;
}
}
return result;
} }
} // anonymous namespace } // anonymous namespace
@@ -350,30 +394,19 @@ QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
if (root.kind != NodeKind::Struct) return {}; if (root.kind != NodeKind::Struct) return {};
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases}; GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
int rootSize = tree.structSpan(rootStructId, &ctx.childMap);
QString typeName = ctx.structName(root);
ctx.output += QStringLiteral("// Generated by ReclassX\n"); ctx.output += QStringLiteral("#pragma once\n\n");
ctx.output += QStringLiteral("// Rendered from: %1 (id=0x%2, size=0x%3)\n\n")
.arg(nodePath(tree, rootStructId))
.arg(QString::number(rootStructId, 16).toUpper())
.arg(QString::number(rootSize, 16).toUpper());
ctx.output += QStringLiteral("#pragma once\n");
ctx.output += QStringLiteral("#include <cstdint>\n\n");
emitStruct(ctx, rootStructId); emitStruct(ctx, rootStructId);
return ctx.output; return alignComments(ctx.output);
} }
QString renderCppAll(const NodeTree& tree, QString renderCppAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases) { const QHash<NodeKind, QString>* typeAliases) {
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases}; GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
ctx.output += QStringLiteral("// Generated by ReclassX\n"); ctx.output += QStringLiteral("#pragma once\n\n");
ctx.output += QStringLiteral("// Full SDK export\n\n");
ctx.output += QStringLiteral("#pragma once\n");
ctx.output += QStringLiteral("#include <cstdint>\n\n");
QVector<int> roots = ctx.childMap.value(0); QVector<int> roots = ctx.childMap.value(0);
std::sort(roots.begin(), roots.end(), [&](int a, int b) { std::sort(roots.begin(), roots.end(), [&](int a, int b) {
@@ -385,7 +418,7 @@ QString renderCppAll(const NodeTree& tree,
emitStruct(ctx, tree.nodes[ri].id); emitStruct(ctx, tree.nodes[ri].id);
} }
return ctx.output; return alignComments(ctx.output);
} }
QString renderNull(const NodeTree&, uint64_t) { QString renderNull(const NodeTree&, uint64_t) {

View File

@@ -10,7 +10,8 @@
#include <QStatusBar> #include <QStatusBar>
#include <QLabel> #include <QLabel>
#include <QSplitter> #include <QSplitter>
#include <QStackedWidget> #include <QTabWidget>
#include <QTabBar>
#include <QPointer> #include <QPointer>
#include <QFileDialog> #include <QFileDialog>
#include <QFileInfo> #include <QFileInfo>
@@ -38,6 +39,7 @@
#include <QDialog> #include <QDialog>
#include <Qsci/qsciscintilla.h> #include <Qsci/qsciscintilla.h>
#include <Qsci/qscilexercpp.h> #include <Qsci/qscilexercpp.h>
#include <QProxyStyle>
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> #include <windows.h>
@@ -113,6 +115,18 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
} }
#endif #endif
class MenuBarStyle : public QProxyStyle {
public:
using QProxyStyle::QProxyStyle;
QSize sizeFromContents(ContentsType type, const QStyleOption* opt,
const QSize& sz, const QWidget* w) const override {
QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w);
if (type == CT_MenuBarItem)
s.setHeight(s.height() + qRound(s.height() * 0.5));
return s;
}
};
namespace rcx { namespace rcx {
class MainWindow : public QMainWindow { class MainWindow : public QMainWindow {
@@ -122,6 +136,7 @@ public:
private slots: private slots:
void newFile(); void newFile();
void newDocument();
void selfTest(); void selfTest();
void openFile(); void openFile();
void saveFile(); void saveFile();
@@ -151,21 +166,26 @@ public:
void project_close(QMdiSubWindow* sub = nullptr); void project_close(QMdiSubWindow* sub = nullptr);
private: private:
enum ViewMode { VM_Reclass, VM_Rendered }; enum ViewMode { VM_Reclass, VM_Rendered, VM_Debug };
QMdiArea* m_mdiArea; QMdiArea* m_mdiArea;
QLabel* m_statusLabel; QLabel* m_statusLabel;
PluginManager m_pluginManager; PluginManager m_pluginManager;
struct SplitPane {
QTabWidget* tabWidget = nullptr;
RcxEditor* editor = nullptr;
QsciScintilla* rendered = nullptr;
ViewMode viewMode = VM_Reclass;
uint64_t lastRenderedRootId = 0;
};
struct TabState { struct TabState {
RcxDocument* doc; RcxDocument* doc;
RcxController* ctrl; RcxController* ctrl;
QSplitter* splitter; QSplitter* splitter;
QStackedWidget* stack = nullptr; QVector<SplitPane> panes;
QPointer<QsciScintilla> rendered; int activePaneIdx = 0;
ViewMode viewMode = VM_Reclass;
uint64_t lastRenderedRootId = 0;
int lastRenderedFirstLine = 0;
}; };
QMap<QMdiSubWindow*, TabState> m_tabs; QMap<QMdiSubWindow*, TabState> m_tabs;
@@ -183,11 +203,18 @@ private:
void updateWindowTitle(); void updateWindowTitle();
void setViewMode(ViewMode mode); void setViewMode(ViewMode mode);
void updateRenderedView(TabState& tab); void updateRenderedView(TabState& tab, SplitPane& pane);
void updateAllRenderedPanes(TabState& tab);
void syncRenderMenuState(); void syncRenderMenuState();
uint64_t findRootStructForNode(const NodeTree& tree, uint64_t nodeId) const; uint64_t findRootStructForNode(const NodeTree& tree, uint64_t nodeId) const;
void setupRenderedSci(QsciScintilla* sci); void setupRenderedSci(QsciScintilla* sci);
SplitPane createSplitPane(TabState& tab);
void applyTabWidgetStyle(QTabWidget* tw);
SplitPane* findPaneByTabWidget(QTabWidget* tw);
SplitPane* findActiveSplitPane();
RcxEditor* activePaneEditor();
// Workspace dock // Workspace dock
QDockWidget* m_workspaceDock = nullptr; QDockWidget* m_workspaceDock = nullptr;
QTreeView* m_workspaceTree = nullptr; QTreeView* m_workspaceTree = nullptr;
@@ -210,6 +237,15 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
createMenus(); createMenus();
createStatusBar(); createStatusBar();
// Larger click targets + subtle hover on menu bar
{
menuBar()->setStyle(new MenuBarStyle(menuBar()->style()));
QPalette mp = menuBar()->palette();
mp.setColor(QPalette::Highlight, QColor(43, 43, 43));
menuBar()->setPalette(mp);
}
// Load plugins // Load plugins
m_pluginManager.LoadPlugins(); m_pluginManager.LoadPlugins();
@@ -219,31 +255,30 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
syncRenderMenuState(); syncRenderMenuState();
rebuildWorkspaceModel(); rebuildWorkspaceModel();
}); });
// Track which split pane has focus (for menu-driven view switching)
connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* now) {
if (!now) return;
auto* tab = activeTab();
if (!tab) return;
for (int i = 0; i < tab->panes.size(); ++i) {
if (tab->panes[i].tabWidget && tab->panes[i].tabWidget->isAncestorOf(now)) {
tab->activePaneIdx = i;
return;
}
}
});
} }
QIcon MainWindow::makeIcon(const QString& svgPath) { QIcon MainWindow::makeIcon(const QString& svgPath) {
// Render SVG at 14x14 (2px smaller) return QIcon(svgPath);
QSvgRenderer renderer(svgPath);
QPixmap svgPixmap(14, 14);
svgPixmap.fill(Qt::transparent);
QPainter svgPainter(&svgPixmap);
renderer.render(&svgPainter);
svgPainter.end();
// Center it in a 16x16 canvas
QPixmap pixmap(16, 16);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.drawPixmap(1, 1, svgPixmap); // Offset by 1px on each side
painter.end();
return QIcon(pixmap);
} }
void MainWindow::createMenus() { void MainWindow::createMenus() {
// File // File
auto* file = menuBar()->addMenu("&File"); auto* file = menuBar()->addMenu("&File");
file->addAction(makeIcon(":/vsicons/file.svg"), "&New", QKeySequence::New, this, &MainWindow::newFile); file->addAction("&New", QKeySequence::New, this, &MainWindow::newDocument);
file->addAction("New &Tab", QKeySequence(Qt::CTRL | Qt::Key_T), this, &MainWindow::newFile);
file->addAction(makeIcon(":/vsicons/folder-opened.svg"), "&Open...", QKeySequence::Open, this, &MainWindow::openFile); file->addAction(makeIcon(":/vsicons/folder-opened.svg"), "&Open...", QKeySequence::Open, this, &MainWindow::openFile);
file->addSeparator(); file->addSeparator();
file->addAction(makeIcon(":/vsicons/save.svg"), "&Save", QKeySequence::Save, this, &MainWindow::saveFile); file->addAction(makeIcon(":/vsicons/save.svg"), "&Save", QKeySequence::Save, this, &MainWindow::saveFile);
@@ -260,7 +295,7 @@ void MainWindow::createMenus() {
edit->addAction(makeIcon(":/vsicons/arrow-left.svg"), "&Undo", QKeySequence::Undo, this, &MainWindow::undo); 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->addAction(makeIcon(":/vsicons/arrow-right.svg"), "&Redo", QKeySequence::Redo, this, &MainWindow::redo);
edit->addSeparator(); edit->addSeparator();
edit->addAction(makeIcon(":/vsicons/symbol-structure.svg"), "&Type Aliases...", this, &MainWindow::showTypeAliasesDialog); edit->addAction("&Type Aliases...", this, &MainWindow::showTypeAliasesDialog);
// View // View
auto* view = menuBar()->addMenu("&View"); auto* view = menuBar()->addMenu("&View");
@@ -311,30 +346,131 @@ void MainWindow::createStatusBar() {
m_statusLabel = new QLabel("Ready"); m_statusLabel = new QLabel("Ready");
statusBar()->addWidget(m_statusLabel, 1); statusBar()->addWidget(m_statusLabel, 1);
statusBar()->setStyleSheet("QStatusBar { background: #252526; color: #858585; }"); statusBar()->setStyleSheet("QStatusBar { background: #252526; color: #858585; }");
QSettings settings("ReclassX", "ReclassX");
QString fontName = settings.value("font", "Consolas").toString();
QFont f(fontName, 12);
f.setFixedPitch(true);
statusBar()->setFont(f);
}
void MainWindow::applyTabWidgetStyle(QTabWidget* tw) {
QSettings settings("ReclassX", "ReclassX");
QString fontName = settings.value("font", "Consolas").toString();
QFont tabFont(fontName, 12);
tabFont.setFixedPitch(true);
tw->tabBar()->setFont(tabFont);
tw->setStyleSheet(QStringLiteral(
"QTabWidget::pane { border: none; }"
"QTabBar::tab {"
" background: #1e1e1e;"
" color: #585858;"
" padding: 4px 12px;"
" border: none;"
" min-width: 60px;"
"}"
"QTabBar::tab:selected {"
" color: #d4d4d4;"
"}"
"QTabBar::tab:hover {"
" color: #d4d4d4;"
"}"
));
tw->tabBar()->setExpanding(false);
}
MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
SplitPane pane;
pane.tabWidget = new QTabWidget;
pane.tabWidget->setTabPosition(QTabWidget::South);
applyTabWidgetStyle(pane.tabWidget);
// Create editor via controller (parent = tabWidget for ownership)
pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget);
pane.tabWidget->addTab(pane.editor, "Reclass"); // index 0
// Create per-pane rendered C++ view
pane.rendered = new QsciScintilla;
setupRenderedSci(pane.rendered);
pane.tabWidget->addTab(pane.rendered, "C/C++"); // index 1
// Debug placeholder
auto* debugPage = new QWidget;
debugPage->setStyleSheet("background: #1e1e1e;");
pane.tabWidget->addTab(debugPage, "Debug"); // index 2
pane.tabWidget->setCurrentIndex(0);
pane.viewMode = VM_Reclass;
// Add to splitter
tab.splitter->addWidget(pane.tabWidget);
// Connect per-pane tab bar switching
QTabWidget* tw = pane.tabWidget;
connect(tw, &QTabWidget::currentChanged, this, [this, tw](int index) {
// Find which pane this QTabWidget belongs to
SplitPane* p = findPaneByTabWidget(tw);
if (!p) return;
if (index == 2) p->viewMode = VM_Debug;
else if (index == 1) p->viewMode = VM_Rendered;
else p->viewMode = VM_Reclass;
if (index == 1) {
// Find the TabState that owns this pane and update rendered view
for (auto& tab : m_tabs) {
for (auto& pane : tab.panes) {
if (&pane == p) {
updateRenderedView(tab, pane);
break;
}
}
}
}
syncRenderMenuState();
});
return pane;
}
MainWindow::SplitPane* MainWindow::findPaneByTabWidget(QTabWidget* tw) {
for (auto& tab : m_tabs) {
for (auto& pane : tab.panes) {
if (pane.tabWidget == tw)
return &pane;
}
}
return nullptr;
}
MainWindow::SplitPane* MainWindow::findActiveSplitPane() {
auto* tab = activeTab();
if (!tab || tab->panes.isEmpty()) return nullptr;
int idx = qBound(0, tab->activePaneIdx, tab->panes.size() - 1);
return &tab->panes[idx];
}
RcxEditor* MainWindow::activePaneEditor() {
auto* pane = findActiveSplitPane();
return pane ? pane->editor : nullptr;
} }
QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
// QStackedWidget wraps [0] splitter (Reclass view) and [1] rendered QsciScintilla
auto* stack = new QStackedWidget;
auto* splitter = new QSplitter(Qt::Horizontal); auto* splitter = new QSplitter(Qt::Horizontal);
auto* ctrl = new RcxController(doc, splitter); auto* ctrl = new RcxController(doc, splitter);
ctrl->addSplitEditor(splitter);
stack->addWidget(splitter); // index 0 = Reclass view auto* sub = m_mdiArea->addSubWindow(splitter);
auto* renderedSci = new QsciScintilla;
setupRenderedSci(renderedSci);
stack->addWidget(renderedSci); // index 1 = Rendered view
stack->setCurrentIndex(0);
auto* sub = m_mdiArea->addSubWindow(stack);
sub->setWindowTitle(doc->filePath.isEmpty() sub->setWindowTitle(doc->filePath.isEmpty()
? "Untitled" : QFileInfo(doc->filePath).fileName()); ? "Untitled" : QFileInfo(doc->filePath).fileName());
sub->setAttribute(Qt::WA_DeleteOnClose); sub->setAttribute(Qt::WA_DeleteOnClose);
sub->showMaximized(); sub->showMaximized();
m_tabs[sub] = { doc, ctrl, splitter, stack, renderedSci, m_tabs[sub] = { doc, ctrl, splitter, {}, 0 };
VM_Reclass, 0, 0 }; auto& tab = m_tabs[sub];
// Create the initial split pane
tab.panes.append(createSplitPane(tab));
connect(sub, &QObject::destroyed, this, [this, sub]() { connect(sub, &QObject::destroyed, this, [this, sub]() {
auto it = m_tabs.find(sub); auto it = m_tabs.find(sub);
@@ -349,8 +485,8 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
this, [this, ctrl, sub](int nodeIdx) { this, [this, ctrl, sub](int nodeIdx) {
if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) { if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) {
auto& node = ctrl->document()->tree.nodes[nodeIdx]; auto& node = ctrl->document()->tree.nodes[nodeIdx];
auto it = m_tabs.find(sub); auto* ap = findActiveSplitPane();
if (it != m_tabs.end() && it->viewMode == VM_Rendered) if (ap && ap->viewMode == VM_Rendered)
m_statusLabel->setText( m_statusLabel->setText(
QString("Rendered: %1 %2") QString("Rendered: %1 %2")
.arg(kindToString(node.kind)) .arg(kindToString(node.kind))
@@ -365,10 +501,10 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
} else { } else {
m_statusLabel->setText("Ready"); m_statusLabel->setText("Ready");
} }
// Update rendered view on selection change // Update all rendered panes on selection change
auto it = m_tabs.find(sub); auto it = m_tabs.find(sub);
if (it != m_tabs.end()) if (it != m_tabs.end())
updateRenderedView(*it); updateAllRenderedPanes(*it);
}); });
connect(ctrl, &RcxController::selectionChanged, connect(ctrl, &RcxController::selectionChanged,
this, [this](int count) { this, [this](int count) {
@@ -378,14 +514,14 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
m_statusLabel->setText(QString("%1 nodes selected").arg(count)); m_statusLabel->setText(QString("%1 nodes selected").arg(count));
}); });
// Update rendered view and workspace on document changes and undo/redo // Update rendered panes and workspace on document changes and undo/redo
connect(doc, &RcxDocument::documentChanged, connect(doc, &RcxDocument::documentChanged,
this, [this, sub]() { this, [this, sub]() {
auto it = m_tabs.find(sub); auto it = m_tabs.find(sub);
if (it != m_tabs.end()) if (it != m_tabs.end())
QTimer::singleShot(0, this, [this, sub]() { QTimer::singleShot(0, this, [this, sub]() {
auto it2 = m_tabs.find(sub); auto it2 = m_tabs.find(sub);
if (it2 != m_tabs.end()) updateRenderedView(*it2); if (it2 != m_tabs.end()) updateAllRenderedPanes(*it2);
rebuildWorkspaceModel(); rebuildWorkspaceModel();
}); });
}); });
@@ -395,7 +531,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
if (it != m_tabs.end()) if (it != m_tabs.end())
QTimer::singleShot(0, this, [this, sub]() { QTimer::singleShot(0, this, [this, sub]() {
auto it2 = m_tabs.find(sub); auto it2 = m_tabs.find(sub);
if (it2 != m_tabs.end()) updateRenderedView(*it2); if (it2 != m_tabs.end()) updateAllRenderedPanes(*it2);
}); });
}); });
@@ -412,72 +548,106 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
return sub; return sub;
} }
// Build Ball + Material demo structs into a tree
static void buildBallDemo(NodeTree& tree) {
// Ball struct (128 bytes = 0x80)
Node ball;
ball.kind = NodeKind::Struct;
ball.name = "aBall";
ball.structTypeName = "Ball";
ball.parentId = 0;
ball.offset = 0;
int bi = tree.addNode(ball);
uint64_t ballId = tree.nodes[bi].id;
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = ballId; n.offset = 0; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_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::Vec3; n.name = "velocity"; n.parentId = ballId; n.offset = 32; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_2C"; n.parentId = ballId; n.offset = 44; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 48; tree.addNode(n); }
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 52; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "radius"; n.parentId = ballId; n.offset = 56; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_3C"; n.parentId = ballId; n.offset = 60; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "mass"; n.parentId = ballId; n.offset = 64; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_44"; n.parentId = ballId; n.offset = 68; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 76; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_4D"; n.parentId = ballId; n.offset = 77; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_4E"; n.parentId = ballId; n.offset = 78; tree.addNode(n); }
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 80; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_54"; n.parentId = ballId; n.offset = 84; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_58"; n.parentId = ballId; n.offset = 88; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_60"; n.parentId = ballId; n.offset = 96; tree.addNode(n); }
// Material struct (renamed from Physics, 40 bytes = 0x28)
Node mat;
mat.kind = NodeKind::Struct;
mat.name = "aMaterial";
mat.structTypeName = "Material";
mat.parentId = 0;
mat.offset = 0;
int mi = tree.addNode(mat);
uint64_t matId = tree.nodes[mi].id;
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = matId; n.offset = 0; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = matId; n.offset = 8; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_10"; n.parentId = matId; n.offset = 16; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_18"; n.parentId = matId; n.offset = 24; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_20"; n.parentId = matId; n.offset = 32; tree.addNode(n); }
// Pointer to Material in Ball struct
{ Node n; n.kind = NodeKind::Pointer64; n.name = "material"; n.parentId = ballId; n.offset = 104; n.refId = matId; n.collapsed = true; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_70"; n.parentId = ballId; n.offset = 112; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_78"; n.parentId = ballId; n.offset = 120; tree.addNode(n); }
}
void MainWindow::newFile() { void MainWindow::newFile() {
project_new(); project_new();
} }
void MainWindow::selfTest() { void MainWindow::newDocument() {
QString demoPath = QCoreApplication::applicationDirPath() + "/demo.rcx"; auto* tab = activeTab();
if (QFile::exists(demoPath)) { if (!tab) {
project_open(demoPath); project_new();
} else { return;
// Create default demo with a single Ball struct
auto* doc = new RcxDocument(this);
doc->tree.baseAddress = 0x00400000;
Node ball;
ball.kind = NodeKind::Struct;
ball.name = "aBall";
ball.structTypeName = "ball";
ball.parentId = 0;
ball.offset = 0;
int bi = doc->tree.addNode(ball);
uint64_t ballId = doc->tree.nodes[bi].id;
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = ballId; n.offset = 0; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = ballId; n.offset = 8; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 16; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Vec3; n.name = "velocity"; n.parentId = ballId; n.offset = 32; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_2C"; n.parentId = ballId; n.offset = 44; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 48; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 52; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "radius"; n.parentId = ballId; n.offset = 56; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_3C"; n.parentId = ballId; n.offset = 60; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "mass"; n.parentId = ballId; n.offset = 64; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_44"; n.parentId = ballId; n.offset = 68; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 76; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_4D"; n.parentId = ballId; n.offset = 77; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_4E"; n.parentId = ballId; n.offset = 78; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 80; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_54"; n.parentId = ballId; n.offset = 84; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_58"; n.parentId = ballId; n.offset = 88; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_60"; n.parentId = ballId; n.offset = 96; doc->tree.addNode(n); }
// Physics struct (defined at root level)
Node phys;
phys.kind = NodeKind::Struct;
phys.name = "aPhysics";
phys.structTypeName = "Physics";
phys.parentId = 0;
phys.offset = 0;
int pi = doc->tree.addNode(phys);
uint64_t physId = doc->tree.nodes[pi].id;
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = physId; n.offset = 0; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = physId; n.offset = 8; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_10"; n.parentId = physId; n.offset = 16; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_18"; n.parentId = physId; n.offset = 24; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_20"; n.parentId = physId; n.offset = 32; doc->tree.addNode(n); }
// Pointer to Physics in ball struct
{ Node n; n.kind = NodeKind::Pointer64; n.name = "physics"; n.parentId = ballId; n.offset = 104; n.refId = physId; n.collapsed = true; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_70"; n.parentId = ballId; n.offset = 112; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_78"; n.parentId = ballId; n.offset = 120; doc->tree.addNode(n); }
doc->save(demoPath);
doc->load(demoPath);
createTab(doc);
} }
auto* doc = tab->doc;
auto* ctrl = tab->ctrl;
// Clear everything
doc->undoStack.clear();
doc->tree = NodeTree();
doc->tree.baseAddress = 0x00400000;
doc->filePath.clear();
doc->typeAliases.clear();
doc->modified = false;
// Build Ball + Material structs
buildBallDemo(doc->tree);
// Cross-platform writable buffer, zeroed (256 bytes covers Ball + spare)
QByteArray data(256, '\0');
doc->provider = std::make_shared<BufferProvider>(data);
// Focus on Ball struct
ctrl->setViewRootId(0);
for (const auto& n : doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
ctrl->setViewRootId(n.id);
break;
}
}
ctrl->clearSelection();
emit doc->documentChanged();
auto* sub = m_mdiArea->activeSubWindow();
if (sub) sub->setWindowTitle("Untitled");
updateWindowTitle();
rebuildWorkspaceModel();
}
void MainWindow::selfTest() {
project_new();
} }
void MainWindow::openFile() { void MainWindow::openFile() {
@@ -506,7 +676,7 @@ void MainWindow::addNode() {
if (!ctrl) return; if (!ctrl) return;
uint64_t parentId = ctrl->viewRootId(); // default to current view root uint64_t parentId = ctrl->viewRootId(); // default to current view root
auto* primary = ctrl->primaryEditor(); auto* primary = activePaneEditor();
if (primary && primary->isEditing()) return; if (primary && primary->isEditing()) return;
if (primary) { if (primary) {
int ni = primary->currentNodeIndex(); int ni = primary->currentNodeIndex();
@@ -524,7 +694,7 @@ void MainWindow::addNode() {
void MainWindow::removeNode() { void MainWindow::removeNode() {
auto* ctrl = activeController(); auto* ctrl = activeController();
if (!ctrl) return; if (!ctrl) return;
auto* primary = ctrl->primaryEditor(); auto* primary = activePaneEditor();
if (!primary || primary->isEditing()) return; if (!primary || primary->isEditing()) return;
QSet<int> indices = primary->selectedNodeIndices(); QSet<int> indices = primary->selectedNodeIndices();
if (indices.size() > 1) { if (indices.size() > 1) {
@@ -537,7 +707,7 @@ void MainWindow::removeNode() {
void MainWindow::changeNodeType() { void MainWindow::changeNodeType() {
auto* ctrl = activeController(); auto* ctrl = activeController();
if (!ctrl) return; if (!ctrl) return;
auto* primary = ctrl->primaryEditor(); auto* primary = activePaneEditor();
if (!primary) return; if (!primary) return;
primary->beginInlineEdit(EditTarget::Type); primary->beginInlineEdit(EditTarget::Type);
} }
@@ -545,7 +715,7 @@ void MainWindow::changeNodeType() {
void MainWindow::renameNodeAction() { void MainWindow::renameNodeAction() {
auto* ctrl = activeController(); auto* ctrl = activeController();
if (!ctrl) return; if (!ctrl) return;
auto* primary = ctrl->primaryEditor(); auto* primary = activePaneEditor();
if (!primary) return; if (!primary) return;
primary->beginInlineEdit(EditTarget::Name); primary->beginInlineEdit(EditTarget::Name);
} }
@@ -553,7 +723,7 @@ void MainWindow::renameNodeAction() {
void MainWindow::duplicateNodeAction() { void MainWindow::duplicateNodeAction() {
auto* ctrl = activeController(); auto* ctrl = activeController();
if (!ctrl) return; if (!ctrl) return;
auto* primary = ctrl->primaryEditor(); auto* primary = activePaneEditor();
if (!primary || primary->isEditing()) return; if (!primary || primary->isEditing()) return;
int ni = primary->currentNodeIndex(); int ni = primary->currentNodeIndex();
if (ni >= 0) ctrl->duplicateNode(ni); if (ni >= 0) ctrl->duplicateNode(ni);
@@ -562,15 +732,16 @@ void MainWindow::duplicateNodeAction() {
void MainWindow::splitView() { void MainWindow::splitView() {
auto* tab = activeTab(); auto* tab = activeTab();
if (!tab) return; if (!tab) return;
tab->ctrl->addSplitEditor(tab->splitter); tab->panes.append(createSplitPane(*tab));
} }
void MainWindow::unsplitView() { void MainWindow::unsplitView() {
auto* tab = activeTab(); auto* tab = activeTab();
if (!tab) return; if (!tab || tab->panes.size() <= 1) return;
auto editors = tab->ctrl->editors(); auto pane = tab->panes.takeLast();
if (editors.size() > 1) tab->ctrl->removeSplitEditor(pane.editor);
tab->ctrl->removeSplitEditor(editors.last()); pane.tabWidget->deleteLater();
tab->activePaneIdx = qBound(0, tab->activePaneIdx, tab->panes.size() - 1);
} }
void MainWindow::undo() { void MainWindow::undo() {
@@ -598,20 +769,27 @@ void MainWindow::setEditorFont(const QString& fontName) {
f.setFixedPitch(true); f.setFixedPitch(true);
for (auto& state : m_tabs) { for (auto& state : m_tabs) {
state.ctrl->setEditorFont(fontName); state.ctrl->setEditorFont(fontName);
// Also update the rendered view font for (auto& pane : state.panes) {
if (state.rendered) { // Update rendered view font
state.rendered->setFont(f); if (pane.rendered) {
if (auto* lex = state.rendered->lexer()) { pane.rendered->setFont(f);
lex->setFont(f); if (auto* lex = pane.rendered->lexer()) {
for (int i = 0; i <= 127; i++) lex->setFont(f);
lex->setFont(f, i); for (int i = 0; i <= 127; i++)
lex->setFont(f, i);
}
pane.rendered->setMarginsFont(f);
} }
state.rendered->setMarginsFont(f); // Update per-pane tab bar font
if (pane.tabWidget)
applyTabWidgetStyle(pane.tabWidget);
} }
} }
// Sync workspace tree font // Sync workspace tree font
if (m_workspaceTree) if (m_workspaceTree)
m_workspaceTree->setFont(f); m_workspaceTree->setFont(f);
// Sync status bar font
statusBar()->setFont(f);
} }
RcxController* MainWindow::activeController() const { RcxController* MainWindow::activeController() const {
@@ -650,14 +828,10 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) {
f.setFixedPitch(true); f.setFixedPitch(true);
sci->setFont(f); sci->setFont(f);
sci->setReadOnly(true); sci->setReadOnly(false);
sci->setWrapMode(QsciScintilla::WrapNone); sci->setWrapMode(QsciScintilla::WrapNone);
sci->setCaretLineVisible(false);
sci->setPaper(QColor("#1e1e1e"));
sci->setColor(QColor("#d4d4d4"));
sci->setTabWidth(4); sci->setTabWidth(4);
sci->setIndentationsUseTabs(false); sci->setIndentationsUseTabs(false);
sci->setCaretForegroundColor(QColor("#d4d4d4"));
sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2); sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2);
sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2); sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2);
@@ -672,7 +846,8 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) {
sci->setMarginWidth(1, 0); sci->setMarginWidth(1, 0);
sci->setMarginWidth(2, 0); sci->setMarginWidth(2, 0);
// C++ lexer for syntax highlighting // C++ lexer for syntax highlighting — must be set BEFORE colors below,
// because setLexer() resets caret line, selection, and paper colors.
auto* lexer = new QsciLexerCPP(sci); auto* lexer = new QsciLexerCPP(sci);
lexer->setFont(f); lexer->setFont(f);
lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword); lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword);
@@ -693,28 +868,34 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) {
} }
sci->setLexer(lexer); sci->setLexer(lexer);
sci->setBraceMatching(QsciScintilla::NoBraceMatch); sci->setBraceMatching(QsciScintilla::NoBraceMatch);
// Colors applied AFTER setLexer() — the lexer resets these on attach
sci->setPaper(QColor("#1e1e1e"));
sci->setColor(QColor("#d4d4d4"));
sci->setCaretForegroundColor(QColor("#d4d4d4"));
sci->setCaretLineVisible(true);
sci->setCaretLineBackgroundColor(QColor(43, 43, 43)); // Match Reclass M_HOVER
sci->setSelectionBackgroundColor(QColor("#264f78")); // Match Reclass edit selection
sci->setSelectionForegroundColor(QColor("#d4d4d4"));
} }
// ── View mode / generator switching ── // ── View mode / generator switching ──
void MainWindow::setViewMode(ViewMode mode) { void MainWindow::setViewMode(ViewMode mode) {
auto* tab = activeTab(); auto* pane = findActiveSplitPane();
if (!tab) return; if (!pane) return;
tab->viewMode = mode; pane->viewMode = mode;
if (tab->stack) { int idx = (mode == VM_Rendered) ? 1 : (mode == VM_Debug) ? 2 : 0;
tab->stack->setCurrentIndex(mode == VM_Rendered ? 1 : 0); pane->tabWidget->setCurrentIndex(idx);
} // The QTabWidget::currentChanged signal will handle updating the rendered view
if (mode == VM_Rendered) {
updateRenderedView(*tab);
}
syncRenderMenuState(); syncRenderMenuState();
} }
void MainWindow::syncRenderMenuState() { void MainWindow::syncRenderMenuState() {
auto* tab = activeTab(); auto* pane = findActiveSplitPane();
bool rendered = tab && tab->viewMode == VM_Rendered; ViewMode vm = pane ? pane->viewMode : VM_Reclass;
if (m_actViewRendered) m_actViewRendered->setEnabled(!rendered); if (m_actViewRendered) m_actViewRendered->setEnabled(vm != VM_Rendered);
if (m_actViewReclass) m_actViewReclass->setEnabled(rendered); if (m_actViewReclass) m_actViewReclass->setEnabled(vm != VM_Reclass);
} }
// ── Find the root-level struct ancestor for a node ── // ── Find the root-level struct ancestor for a node ──
@@ -737,11 +918,11 @@ uint64_t MainWindow::findRootStructForNode(const NodeTree& tree, uint64_t nodeId
return lastStruct; return lastStruct;
} }
// ── Update the rendered view for a tab ── // ── Update the rendered view for a single pane ──
void MainWindow::updateRenderedView(TabState& tab) { void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) {
if (tab.viewMode != VM_Rendered) return; if (pane.viewMode != VM_Rendered) return;
if (!tab.rendered) return; if (!pane.rendered) return;
// Determine which struct to render based on selection // Determine which struct to render based on selection
uint64_t rootId = 0; uint64_t rootId = 0;
@@ -763,26 +944,31 @@ void MainWindow::updateRenderedView(TabState& tab) {
// Scroll restoration: save if same root, reset if different // Scroll restoration: save if same root, reset if different
int restoreLine = 0; int restoreLine = 0;
if (rootId != 0 && rootId == tab.lastRenderedRootId) { if (rootId != 0 && rootId == pane.lastRenderedRootId) {
restoreLine = (int)tab.rendered->SendScintilla( restoreLine = (int)pane.rendered->SendScintilla(
QsciScintillaBase::SCI_GETFIRSTVISIBLELINE); QsciScintillaBase::SCI_GETFIRSTVISIBLELINE);
} }
tab.lastRenderedRootId = rootId; pane.lastRenderedRootId = rootId;
// Set text // Set text
tab.rendered->setReadOnly(false); pane.rendered->setText(text);
tab.rendered->setText(text);
tab.rendered->setReadOnly(true);
// Update margin width for line count // Update margin width for line count
int lineCount = tab.rendered->lines(); int lineCount = pane.rendered->lines();
QString marginStr = QString(QString::number(lineCount).size() + 2, '0'); QString marginStr = QString(QString::number(lineCount).size() + 2, '0');
tab.rendered->setMarginWidth(0, marginStr); pane.rendered->setMarginWidth(0, marginStr);
// Restore scroll // Restore scroll
if (restoreLine > 0) { if (restoreLine > 0) {
tab.rendered->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE, pane.rendered->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE,
(unsigned long)restoreLine); (unsigned long)restoreLine);
}
}
void MainWindow::updateAllRenderedPanes(TabState& tab) {
for (auto& pane : tab.panes) {
if (pane.viewMode == VM_Rendered)
updateRenderedView(tab, pane);
} }
} }
@@ -871,23 +1057,13 @@ void MainWindow::showTypeAliasesDialog() {
QMdiSubWindow* MainWindow::project_new() { QMdiSubWindow* MainWindow::project_new() {
auto* doc = new RcxDocument(this); auto* doc = new RcxDocument(this);
QByteArray data(16, '\0'); // Cross-platform writable buffer, zeroed (256 bytes covers Ball struct + spare)
QByteArray data(256, '\0');
doc->loadData(data); doc->loadData(data);
doc->tree.baseAddress = 0x00400000; doc->tree.baseAddress = 0x00400000;
Node root; // Build Ball + Material demo structs
root.kind = NodeKind::Struct; buildBallDemo(doc->tree);
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); auto* sub = createTab(doc);
rebuildWorkspaceModel(); rebuildWorkspaceModel();

350
src/typeselectorpopup.cpp Normal file
View File

@@ -0,0 +1,350 @@
#include "typeselectorpopup.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListView>
#include <QToolButton>
#include <QStringListModel>
#include <QStyledItemDelegate>
#include <QPainter>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QIcon>
#include <QApplication>
#include <QScreen>
namespace rcx {
// ── Custom delegate: gutter checkmark + icon + text ──
class TypeSelectorDelegate : public QStyledItemDelegate {
public:
explicit TypeSelectorDelegate(TypeSelectorPopup* popup, QObject* parent = nullptr)
: QStyledItemDelegate(parent), m_popup(popup) {}
void setFont(const QFont& f) { m_font = f; }
void setCurrentTypes(const QVector<TypeEntry>* filtered, uint64_t currentId) {
m_filtered = filtered;
m_currentId = currentId;
}
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const override {
painter->save();
// Background
if (option.state & QStyle::State_Selected)
painter->fillRect(option.rect, option.palette.highlight());
else if (option.state & QStyle::State_MouseOver)
painter->fillRect(option.rect, QColor(43, 43, 43));
int x = option.rect.x();
int y = option.rect.y();
int h = option.rect.height();
// 18px gutter: side triangle if current
int row = index.row();
if (m_filtered && row >= 0 && row < m_filtered->size()
&& (*m_filtered)[row].id == m_currentId) {
painter->setPen(QColor("#4ec9b0"));
QFont checkFont = m_font;
painter->setFont(checkFont);
painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter,
QString(QChar(0x25B8)));
}
x += 18;
// Icon 16x16
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg"));
structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16);
x += 20;
// Text
painter->setPen(option.state & QStyle::State_Selected
? option.palette.color(QPalette::HighlightedText)
: option.palette.color(QPalette::Text));
painter->setFont(m_font);
painter->drawText(QRect(x, y, option.rect.right() - x, h),
Qt::AlignVCenter | Qt::AlignLeft,
index.data().toString());
painter->restore();
}
QSize sizeHint(const QStyleOptionViewItem& /*option*/,
const QModelIndex& /*index*/) const override {
QFontMetrics fm(m_font);
return QSize(200, fm.height() + 8);
}
private:
TypeSelectorPopup* m_popup = nullptr;
QFont m_font;
const QVector<TypeEntry>* m_filtered = nullptr;
uint64_t m_currentId = 0;
};
// ── TypeSelectorPopup ──
TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
: QFrame(parent, Qt::Popup | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_DeleteOnClose, false);
// Dark palette (no CSS)
QPalette pal;
pal.setColor(QPalette::Window, QColor("#252526"));
pal.setColor(QPalette::WindowText, QColor("#d4d4d4"));
pal.setColor(QPalette::Base, QColor("#1e1e1e"));
pal.setColor(QPalette::AlternateBase, QColor("#2a2d2e"));
pal.setColor(QPalette::Text, QColor("#d4d4d4"));
pal.setColor(QPalette::Button, QColor("#333333"));
pal.setColor(QPalette::ButtonText, QColor("#d4d4d4"));
pal.setColor(QPalette::Highlight, QColor("#264f78"));
pal.setColor(QPalette::HighlightedText, QColor("#ffffff"));
setPalette(pal);
setAutoFillBackground(true);
// Thin border
setFrameShape(QFrame::Box);
setLineWidth(1);
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(6, 6, 6, 6);
layout->setSpacing(4);
// Row 1: title + Esc hint
{
auto* row = new QHBoxLayout;
row->setContentsMargins(0, 0, 0, 0);
m_titleLabel = new QLabel(QStringLiteral("View as type"));
m_titleLabel->setPalette(pal);
QFont bold = m_titleLabel->font();
bold.setBold(true);
m_titleLabel->setFont(bold);
row->addWidget(m_titleLabel);
row->addStretch();
m_escLabel = new QLabel(QStringLiteral("Esc"));
QPalette dimPal = pal;
dimPal.setColor(QPalette::WindowText, QColor("#858585"));
m_escLabel->setPalette(dimPal);
row->addWidget(m_escLabel);
layout->addLayout(row);
}
// Row 2: + Create new type button
{
m_createBtn = new QToolButton;
m_createBtn->setText(QStringLiteral("+ Create new type\u2026"));
m_createBtn->setIcon(QIcon(QStringLiteral(":/vsicons/add.svg")));
m_createBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
m_createBtn->setAutoRaise(true);
m_createBtn->setCursor(Qt::PointingHandCursor);
m_createBtn->setPalette(pal);
connect(m_createBtn, &QToolButton::clicked, this, [this]() {
emit createNewTypeRequested();
hide();
});
layout->addWidget(m_createBtn);
}
// Separator
{
auto* sep = new QFrame;
sep->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain);
QPalette sepPal = pal;
sepPal.setColor(QPalette::WindowText, QColor("#3c3c3c"));
sep->setPalette(sepPal);
sep->setFixedHeight(1);
layout->addWidget(sep);
}
// Row 3: Filter
{
m_filterEdit = new QLineEdit;
m_filterEdit->setPlaceholderText(QStringLiteral("Filter types\u2026"));
m_filterEdit->setClearButtonEnabled(true);
m_filterEdit->setPalette(pal);
m_filterEdit->installEventFilter(this);
connect(m_filterEdit, &QLineEdit::textChanged,
this, &TypeSelectorPopup::applyFilter);
layout->addWidget(m_filterEdit);
}
// Row 4: List
{
m_model = new QStringListModel(this);
m_listView = new QListView;
m_listView->setModel(m_model);
m_listView->setPalette(pal);
m_listView->setFrameShape(QFrame::NoFrame);
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_listView->setMouseTracking(true);
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
m_listView->installEventFilter(this);
auto* delegate = new TypeSelectorDelegate(this, m_listView);
m_listView->setItemDelegate(delegate);
layout->addWidget(m_listView, 1);
connect(m_listView, &QListView::clicked,
this, [this](const QModelIndex& index) {
acceptIndex(index.row());
});
}
}
void TypeSelectorPopup::setFont(const QFont& font) {
m_font = font;
m_titleLabel->setFont([&]() {
QFont f = font; f.setBold(true); return f;
}());
m_escLabel->setFont(font);
m_createBtn->setFont(font);
m_filterEdit->setFont(font);
m_listView->setFont(font);
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
if (delegate)
delegate->setFont(font);
}
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, uint64_t currentId) {
m_allTypes = types;
m_currentId = currentId;
m_filterEdit->clear();
applyFilter(QString());
}
void TypeSelectorPopup::popup(const QPoint& globalPos) {
// Size: width based on longest entry, height based on count
QFontMetrics fm(m_font);
int maxTextW = fm.horizontalAdvance(QStringLiteral("View as type Esc"));
for (const auto& t : m_allTypes) {
QString text = t.classKeyword + QStringLiteral(" ") + t.displayName;
int w = 18 + 20 + fm.horizontalAdvance(text) + 16; // gutter + icon + text + pad
if (w > maxTextW) maxTextW = w;
}
int popupW = qBound(250, maxTextW + 24, 500); // +margins
int rowH = fm.height() + 8;
int headerH = rowH * 3 + 30; // title + button + filter + separators/margins
int listH = qBound(rowH * 3, rowH * (int)m_allTypes.size(), rowH * 12);
int popupH = headerH + listH;
// Clamp to screen
QScreen* screen = QApplication::screenAt(globalPos);
if (screen) {
QRect avail = screen->availableGeometry();
if (globalPos.y() + popupH > avail.bottom())
popupH = avail.bottom() - globalPos.y();
if (globalPos.x() + popupW > avail.right())
popupW = avail.right() - globalPos.x();
}
setFixedSize(popupW, popupH);
move(globalPos);
show();
raise();
activateWindow();
m_filterEdit->setFocus();
// Pre-select current type in list
for (int i = 0; i < m_filteredTypes.size(); i++) {
if (m_filteredTypes[i].id == m_currentId) {
m_listView->setCurrentIndex(m_model->index(i));
break;
}
}
}
void TypeSelectorPopup::applyFilter(const QString& text) {
m_filteredTypes.clear();
QStringList displayStrings;
for (const auto& t : m_allTypes) {
if (text.isEmpty()
|| t.displayName.contains(text, Qt::CaseInsensitive)
|| t.classKeyword.contains(text, Qt::CaseInsensitive)) {
m_filteredTypes.append(t);
displayStrings << (t.classKeyword + QStringLiteral(" ") + t.displayName);
}
}
m_model->setStringList(displayStrings);
// Update delegate data
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
if (delegate)
delegate->setCurrentTypes(&m_filteredTypes, m_currentId);
// Select first match
if (!m_filteredTypes.isEmpty())
m_listView->setCurrentIndex(m_model->index(0));
}
void TypeSelectorPopup::acceptCurrent() {
QModelIndex idx = m_listView->currentIndex();
if (idx.isValid())
acceptIndex(idx.row());
}
void TypeSelectorPopup::acceptIndex(int row) {
if (row < 0 || row >= m_filteredTypes.size()) return;
emit typeSelected(m_filteredTypes[row].id);
hide();
}
bool TypeSelectorPopup::eventFilter(QObject* obj, QEvent* event) {
if (event->type() == QEvent::KeyPress) {
auto* ke = static_cast<QKeyEvent*>(event);
if (ke->key() == Qt::Key_Escape) {
hide();
return true;
}
if (obj == m_filterEdit) {
if (ke->key() == Qt::Key_Down) {
m_listView->setFocus();
if (!m_listView->currentIndex().isValid() && m_model->rowCount() > 0)
m_listView->setCurrentIndex(m_model->index(0));
return true;
}
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
acceptCurrent();
return true;
}
}
if (obj == m_listView) {
if (ke->key() == Qt::Key_Up) {
QModelIndex cur = m_listView->currentIndex();
if (!cur.isValid() || cur.row() == 0) {
m_filterEdit->setFocus();
return true;
}
}
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
acceptCurrent();
return true;
}
}
}
return QFrame::eventFilter(obj, event);
}
void TypeSelectorPopup::hideEvent(QHideEvent* event) {
QFrame::hideEvent(event);
emit dismissed();
}
} // namespace rcx

58
src/typeselectorpopup.h Normal file
View File

@@ -0,0 +1,58 @@
#pragma once
#include <QFrame>
#include <QFont>
#include <QVector>
#include <QString>
#include <cstdint>
class QLineEdit;
class QListView;
class QStringListModel;
class QLabel;
class QToolButton;
namespace rcx {
struct TypeEntry {
uint64_t id = 0;
QString displayName;
QString classKeyword; // "struct", "class", or "enum"
};
class TypeSelectorPopup : public QFrame {
Q_OBJECT
public:
explicit TypeSelectorPopup(QWidget* parent = nullptr);
void setFont(const QFont& font);
void setTypes(const QVector<TypeEntry>& types, uint64_t currentId);
void popup(const QPoint& globalPos);
signals:
void typeSelected(uint64_t structId);
void createNewTypeRequested();
void dismissed();
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
void hideEvent(QHideEvent* event) override;
private:
QLabel* m_titleLabel = nullptr;
QLabel* m_escLabel = nullptr;
QToolButton* m_createBtn = nullptr;
QLineEdit* m_filterEdit = nullptr;
QListView* m_listView = nullptr;
QStringListModel* m_model = nullptr;
QVector<TypeEntry> m_allTypes;
QVector<TypeEntry> m_filteredTypes;
uint64_t m_currentId = 0;
QFont m_font;
void applyFilter(const QString& text);
void acceptCurrent();
void acceptIndex(int row);
};
} // namespace rcx

View File

@@ -54,18 +54,16 @@ private slots:
QString result = rcx::renderCpp(tree, rootId); QString result = rcx::renderCpp(tree, rootId);
// Header // Header
QVERIFY(result.contains("Generated by ReclassX"));
QVERIFY(result.contains("#pragma once")); QVERIFY(result.contains("#pragma once"));
QVERIFY(result.contains("#include <cstdint>")); QVERIFY(!result.contains("#include <cstdint>"));
QVERIFY(!result.contains("#pragma pack"));
// Struct definition // Struct definition
QVERIFY(result.contains("#pragma pack(push, 1)"));
QVERIFY(result.contains("struct Player {")); QVERIFY(result.contains("struct Player {"));
QVERIFY(result.contains("int32_t health;")); QVERIFY(result.contains("int32_t health;"));
QVERIFY(result.contains("float speed;")); QVERIFY(result.contains("float speed;"));
QVERIFY(result.contains("uint64_t id;")); QVERIFY(result.contains("uint64_t id;"));
QVERIFY(result.contains("};")); QVERIFY(result.contains("};"));
QVERIFY(result.contains("#pragma pack(pop)"));
// static_assert - struct is 16 bytes (0+4 + 4+4 + 8+8 = 16) // static_assert - struct is 16 bytes (0+4 + 4+4 + 8+8 = 16)
QVERIFY(result.contains("static_assert(sizeof(Player) == 0x10")); QVERIFY(result.contains("static_assert(sizeof(Player) == 0x10"));
@@ -485,7 +483,6 @@ private slots:
QString result = rcx::renderCppAll(tree); QString result = rcx::renderCppAll(tree);
QVERIFY(result.contains("Full SDK export"));
QVERIFY(result.contains("struct StructA {")); QVERIFY(result.contains("struct StructA {"));
QVERIFY(result.contains("struct StructB {")); QVERIFY(result.contains("struct StructB {"));
QVERIFY(result.contains("uint32_t valueA;")); QVERIFY(result.contains("uint32_t valueA;"));

View File

@@ -0,0 +1,361 @@
#include <QtTest/QTest>
#include <QApplication>
#include <Qsci/qsciscintilla.h>
#include <Qsci/qsciscintillabase.h>
#include <Qsci/qscilexercpp.h>
#include <QColor>
#include <QFont>
#include "core.h"
#include "generator.h"
// Raw Scintilla message IDs not exposed by QsciScintillaBase wrapper
static constexpr int SCI_GETSELBACK = 2477;
static constexpr int SCI_GETSELFORE = 2476;
// ── Helper: extract BGR long from QColor (Scintilla stores colors as 0x00BBGGRR) ──
static long toBGR(const QColor& c) {
return (long)c.red() | ((long)c.green() << 8) | ((long)c.blue() << 16);
}
// ── Replicates MainWindow::setupRenderedSci so the test stays in sync ──
static void setupRenderedSci(QsciScintilla* sci) {
QFont f("Consolas", 12);
f.setFixedPitch(true);
sci->setFont(f);
sci->setReadOnly(false);
sci->setWrapMode(QsciScintilla::WrapNone);
sci->setTabWidth(4);
sci->setIndentationsUseTabs(false);
sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2);
sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2);
// Line number margin
sci->setMarginType(0, QsciScintilla::NumberMargin);
sci->setMarginWidth(0, "00000");
sci->setMarginsBackgroundColor(QColor("#252526"));
sci->setMarginsForegroundColor(QColor("#858585"));
sci->setMarginsFont(f);
sci->setMarginWidth(1, 0);
sci->setMarginWidth(2, 0);
// Lexer FIRST — setLexer() resets caret/selection/paper colors
auto* lexer = new QsciLexerCPP(sci);
lexer->setFont(f);
lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword);
lexer->setColor(QColor("#569cd6"), QsciLexerCPP::KeywordSet2);
lexer->setColor(QColor("#b5cea8"), QsciLexerCPP::Number);
lexer->setColor(QColor("#ce9178"), QsciLexerCPP::DoubleQuotedString);
lexer->setColor(QColor("#ce9178"), QsciLexerCPP::SingleQuotedString);
lexer->setColor(QColor("#6a9955"), QsciLexerCPP::Comment);
lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine);
lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc);
lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Default);
lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Identifier);
lexer->setColor(QColor("#c586c0"), QsciLexerCPP::PreProcessor);
lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator);
for (int i = 0; i <= 127; i++) {
lexer->setPaper(QColor("#1e1e1e"), i);
lexer->setFont(f, i);
}
sci->setLexer(lexer);
sci->setBraceMatching(QsciScintilla::NoBraceMatch);
// Colors AFTER setLexer() — the lexer resets these on attach
sci->setPaper(QColor("#1e1e1e"));
sci->setColor(QColor("#d4d4d4"));
sci->setCaretForegroundColor(QColor("#d4d4d4"));
sci->setCaretLineVisible(true);
sci->setCaretLineBackgroundColor(QColor(43, 43, 43));
sci->setSelectionBackgroundColor(QColor("#264f78"));
sci->setSelectionForegroundColor(QColor("#d4d4d4"));
}
// ── Test tree helper ──
static rcx::NodeTree makeTestTree() {
rcx::NodeTree tree;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "TestStruct";
root.structTypeName = "TestStruct";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node f1;
f1.kind = rcx::NodeKind::Int32;
f1.name = "health";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
rcx::Node f2;
f2.kind = rcx::NodeKind::Float;
f2.name = "speed";
f2.parentId = rootId;
f2.offset = 4;
tree.addNode(f2);
return tree;
}
// ── Test class ──
class TestRenderedView : public QObject {
Q_OBJECT
private slots:
// ── Verify caret line background is NOT yellow after setup ──
void testCaretLineBackgroundNotYellow() {
QsciScintilla sci;
setupRenderedSci(&sci);
sci.show();
sci.setText("struct Foo {\n int x;\n};\n");
QTest::qWait(50);
long bgr = sci.SendScintilla(QsciScintillaBase::SCI_GETCARETLINEBACK);
long expected = toBGR(QColor(43, 43, 43));
// Yellow would be 0x00FFFF or similar high-value — ours should be dark
long yellow = toBGR(QColor(255, 255, 0));
QVERIFY2(bgr != yellow,
qPrintable(QString("Caret line is yellow (0x%1), expected dark (0x%2)")
.arg(bgr, 6, 16, QChar('0'))
.arg(expected, 6, 16, QChar('0'))));
QCOMPARE(bgr, expected);
}
// ── Verify caret line is enabled ──
void testCaretLineEnabled() {
QsciScintilla sci;
setupRenderedSci(&sci);
long visible = sci.SendScintilla(QsciScintillaBase::SCI_GETCARETLINEVISIBLE);
QCOMPARE(visible, (long)1);
}
// ── Verify editor background (paper) is dark ──
void testPaperColor() {
QsciScintilla sci;
setupRenderedSci(&sci);
// Query default style background via Scintilla
long bgr = sci.SendScintilla(QsciScintillaBase::SCI_STYLEGETBACK,
(unsigned long)0 /*STYLE_DEFAULT*/);
long expected = toBGR(QColor("#1e1e1e"));
QCOMPARE(bgr, expected);
}
// ── Verify caret (cursor) foreground color ──
void testCaretForegroundColor() {
QsciScintilla sci;
setupRenderedSci(&sci);
long bgr = sci.SendScintilla(QsciScintillaBase::SCI_GETCARETFORE);
long expected = toBGR(QColor("#d4d4d4"));
QCOMPARE(bgr, expected);
}
// ── Verify selection colors are set (no direct Scintilla getter, but we can
// verify they survive a round-trip through the SCI_SETSEL* messages by
// checking the element colour API introduced in Scintilla 5.x) ──
void testSelectionColorsApplied() {
QsciScintilla sci;
setupRenderedSci(&sci);
sci.show();
sci.setText("int x = 42;\n");
QTest::qWait(50);
// Select text and verify rendering doesn't crash
sci.SendScintilla(QsciScintillaBase::SCI_SETSEL, (unsigned long)0, (long)3);
QTest::qWait(50);
// SCI_GETELEMENTCOLOUR (element 10 = SC_ELEMENT_SELECTION_BACK) returns
// the selection back colour on Scintilla >= 5.2. If not available, fall
// back to verifying the calls didn't throw and caret line is still correct.
constexpr int SCI_GETELEMENTCOLOUR = 2753;
constexpr int SC_ELEMENT_SELECTION_BACK = 10;
long selBack = sci.SendScintilla(SCI_GETELEMENTCOLOUR,
(unsigned long)SC_ELEMENT_SELECTION_BACK);
if (selBack != 0) {
// Scintilla 5.x: colour stored as 0xAABBGGRR (with alpha in high byte)
long bgrMask = selBack & 0x00FFFFFF;
long expected = toBGR(QColor("#264f78"));
QCOMPARE(bgrMask, expected);
} else {
// Older Scintilla: just verify caret line is still correct as a proxy
long caretBg = sci.SendScintilla(QsciScintillaBase::SCI_GETCARETLINEBACK);
long expected = toBGR(QColor(43, 43, 43));
QCOMPARE(caretBg, expected);
}
}
// ── Verify lexer keyword color is VS Code blue, not default ──
void testKeywordColor() {
QsciScintilla sci;
setupRenderedSci(&sci);
auto* lexer = qobject_cast<QsciLexerCPP*>(sci.lexer());
QVERIFY(lexer != nullptr);
QColor kw = lexer->color(QsciLexerCPP::Keyword);
QCOMPARE(kw, QColor("#569cd6"));
}
// ── Verify comment color is VS Code green ──
void testCommentColor() {
QsciScintilla sci;
setupRenderedSci(&sci);
auto* lexer = qobject_cast<QsciLexerCPP*>(sci.lexer());
QVERIFY(lexer != nullptr);
QCOMPARE(lexer->color(QsciLexerCPP::Comment), QColor("#6a9955"));
QCOMPARE(lexer->color(QsciLexerCPP::CommentLine), QColor("#6a9955"));
}
// ── Verify number color is VS Code light green ──
void testNumberColor() {
QsciScintilla sci;
setupRenderedSci(&sci);
auto* lexer = qobject_cast<QsciLexerCPP*>(sci.lexer());
QVERIFY(lexer != nullptr);
QCOMPARE(lexer->color(QsciLexerCPP::Number), QColor("#b5cea8"));
}
// ── Verify string color is VS Code orange ──
void testStringColor() {
QsciScintilla sci;
setupRenderedSci(&sci);
auto* lexer = qobject_cast<QsciLexerCPP*>(sci.lexer());
QVERIFY(lexer != nullptr);
QCOMPARE(lexer->color(QsciLexerCPP::DoubleQuotedString), QColor("#ce9178"));
QCOMPARE(lexer->color(QsciLexerCPP::SingleQuotedString), QColor("#ce9178"));
}
// ── Verify preprocessor color is VS Code purple ──
void testPreprocessorColor() {
QsciScintilla sci;
setupRenderedSci(&sci);
auto* lexer = qobject_cast<QsciLexerCPP*>(sci.lexer());
QVERIFY(lexer != nullptr);
QCOMPARE(lexer->color(QsciLexerCPP::PreProcessor), QColor("#c586c0"));
}
// ── Verify default/identifier text color ──
void testDefaultTextColor() {
QsciScintilla sci;
setupRenderedSci(&sci);
auto* lexer = qobject_cast<QsciLexerCPP*>(sci.lexer());
QVERIFY(lexer != nullptr);
QCOMPARE(lexer->color(QsciLexerCPP::Default), QColor("#d4d4d4"));
QCOMPARE(lexer->color(QsciLexerCPP::Identifier), QColor("#d4d4d4"));
QCOMPARE(lexer->color(QsciLexerCPP::Operator), QColor("#d4d4d4"));
}
// ── Verify all 128 lexer styles have dark paper ──
void testAllStylesHaveDarkPaper() {
QsciScintilla sci;
setupRenderedSci(&sci);
auto* lexer = qobject_cast<QsciLexerCPP*>(sci.lexer());
QVERIFY(lexer != nullptr);
QColor expected("#1e1e1e");
for (int i = 0; i <= 127; i++) {
QColor paper = lexer->paper(i);
QVERIFY2(paper == expected,
qPrintable(QString("Style %1 paper is %2, expected %3")
.arg(i).arg(paper.name()).arg(expected.name())));
}
}
// ── Verify margin colors match dark theme ──
void testMarginColors() {
QsciScintilla sci;
setupRenderedSci(&sci);
// Query margin background via Scintilla (style 33 = STYLE_LINENUMBER)
long marginBg = sci.SendScintilla(QsciScintillaBase::SCI_STYLEGETBACK,
(unsigned long)33);
long expectedBg = toBGR(QColor("#252526"));
QCOMPARE(marginBg, expectedBg);
long marginFg = sci.SendScintilla(QsciScintillaBase::SCI_STYLEGETFORE,
(unsigned long)33);
long expectedFg = toBGR(QColor("#858585"));
QCOMPARE(marginFg, expectedFg);
}
// ── End-to-end: generate C++ and load into rendered view ──
void testGeneratedCodeInRenderedView() {
auto tree = makeTestTree();
uint64_t rootId = tree.nodes[0].id;
QString code = rcx::renderCpp(tree, rootId);
// Verify generated code has no pragma pack / cstdint
QVERIFY(!code.contains("#pragma pack"));
QVERIFY(!code.contains("#include <cstdint>"));
QVERIFY(code.contains("#pragma once"));
QVERIFY(code.contains("struct TestStruct {"));
// Load into rendered sci and verify colors survive
QsciScintilla sci;
setupRenderedSci(&sci);
sci.show();
sci.setText(code);
QTest::qWait(100);
// Caret line must still be dark after text load
long caretBg = sci.SendScintilla(QsciScintillaBase::SCI_GETCARETLINEBACK);
long expected = toBGR(QColor(43, 43, 43));
QCOMPARE(caretBg, expected);
// Paper must still be dark
long paperBg = sci.SendScintilla(QsciScintillaBase::SCI_STYLEGETBACK,
(unsigned long)0);
QCOMPARE(paperBg, toBGR(QColor("#1e1e1e")));
}
// ── Verify brace matching is disabled ──
void testBraceMatchDisabled() {
QsciScintilla sci;
setupRenderedSci(&sci);
QCOMPARE(sci.braceMatching(), QsciScintilla::NoBraceMatch);
}
};
QTEST_MAIN(TestRenderedView)
#include "test_rendered_view.moc"

View File

@@ -0,0 +1,223 @@
#include <QtTest/QTest>
#include <QtTest/QSignalSpy>
#include <QApplication>
#include <QSplitter>
#include <Qsci/qsciscintilla.h>
#include "controller.h"
#include "typeselectorpopup.h"
#include "core.h"
using namespace rcx;
static void buildTwoRootTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
Node a;
a.kind = NodeKind::Struct;
a.name = "Alpha";
a.structTypeName = "Alpha";
a.parentId = 0;
a.offset = 0;
int ai = tree.addNode(a);
uint64_t aId = tree.nodes[ai].id;
{ Node n; n.kind = NodeKind::Int32; n.name = "x"; n.parentId = aId; n.offset = 0; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Int32; n.name = "y"; n.parentId = aId; n.offset = 4; tree.addNode(n); }
Node b;
b.kind = NodeKind::Struct;
b.name = "Bravo";
b.structTypeName = "Bravo";
b.parentId = 0;
b.offset = 0x100;
int bi = tree.addNode(b);
uint64_t bId = tree.nodes[bi].id;
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = bId; n.offset = 0; tree.addNode(n); }
}
static QByteArray makeBuffer() {
return QByteArray(0x200, '\0');
}
class TestTypeSelector : public QObject {
Q_OBJECT
private slots:
// ── Chevron span detection ──
void testChevronSpanDetected() {
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {");
ColumnSpan span = commandRowChevronSpan(text);
QVERIFY(span.valid);
QCOMPARE(span.start, 0);
QCOMPARE(span.end, 3);
}
void testChevronSpanRejects() {
QVERIFY(!commandRowChevronSpan(QStringLiteral("Hi")).valid);
QVERIFY(!commandRowChevronSpan(QStringLiteral("\u25B8 source")).valid);
// Old down-triangle glyph must not match
QVERIFY(!commandRowChevronSpan(QStringLiteral("[\u25BE] source")).valid);
}
// ── Existing spans unbroken by chevron prefix ──
void testSpansWithPrefix() {
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {");
ColumnSpan src = commandRowSrcSpan(text);
QVERIFY(src.valid);
QVERIFY(text.mid(src.start, src.end - src.start).contains("source"));
ColumnSpan addr = commandRowAddrSpan(text);
QVERIFY(addr.valid);
QVERIFY(text.mid(addr.start, addr.end - addr.start).contains("0x1000"));
ColumnSpan rootName = commandRowRootNameSpan(text);
QVERIFY(rootName.valid);
QCOMPARE(text.mid(rootName.start, rootName.end - rootName.start).trimmed(), QString("Alpha"));
}
// ── Popup data model ──
void testPopupListsRootStructs() {
NodeTree tree;
buildTwoRootTree(tree);
QVector<TypeEntry> types;
for (const auto& n : tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
types.append({n.id, n.structTypeName.isEmpty() ? n.name : n.structTypeName,
n.resolvedClassKeyword()});
}
}
QCOMPARE(types.size(), 2);
QCOMPARE(types[0].displayName, QString("Alpha"));
QCOMPARE(types[1].displayName, QString("Bravo"));
}
// ── Popup signals ──
void testPopupSignals() {
TypeSelectorPopup popup;
popup.setTypes({{1, "A", "struct"}, {2, "B", "struct"}}, 1);
QSignalSpy typeSpy(&popup, &TypeSelectorPopup::typeSelected);
QSignalSpy createSpy(&popup, &TypeSelectorPopup::createNewTypeRequested);
emit popup.typeSelected(2);
QCOMPARE(typeSpy.count(), 1);
QCOMPARE(typeSpy.at(0).at(0).toULongLong(), (uint64_t)2);
emit popup.createNewTypeRequested();
QCOMPARE(createSpy.count(), 1);
}
// ── Full GUI integration ──
// Single test method to avoid QScintilla reinit issues.
void testViewSwitchingAndCreateType() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
auto* editor = ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
// Initial refresh so compose populates meta + editor text
ctrl->refresh();
QApplication::processEvents();
auto* sci = editor->scintilla();
// -- Command row starts with [U+25B8] --
{
const LineMeta* meta = editor->metaForLine(0);
QVERIFY(meta);
QCOMPARE(meta->lineKind, LineKind::CommandRow);
QString line0 = sci->text(0);
if (line0.endsWith('\n')) line0.chop(1);
QVERIFY2(line0.startsWith(QStringLiteral("[\u25B8]")),
qPrintable("Expected chevron prefix, got: " + line0.left(10)));
}
// -- Find root IDs --
uint64_t alphaId = 0, bravoId = 0;
for (const auto& n : doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
if (n.name == "Alpha") alphaId = n.id;
if (n.name == "Bravo") bravoId = n.id;
}
}
QVERIFY(alphaId != 0);
QVERIFY(bravoId != 0);
QCOMPARE(ctrl->viewRootId(), (uint64_t)0);
// -- Switch to Bravo: command row + fields update --
ctrl->setViewRootId(bravoId);
QApplication::processEvents();
QCOMPARE(ctrl->viewRootId(), bravoId);
QVERIFY2(sci->text(0).contains("Bravo"),
qPrintable("Expected 'Bravo' in command row, got: " + sci->text(0)));
QVERIFY2(sci->text().contains("speed"),
"View should show Bravo's 'speed' field");
// -- Switch to Alpha --
ctrl->setViewRootId(alphaId);
QApplication::processEvents();
QCOMPARE(ctrl->viewRootId(), alphaId);
QVERIFY2(sci->text(0).contains("Alpha"),
qPrintable("Expected 'Alpha' in command row, got: " + sci->text(0)));
// -- Create new type (no name) --
int nodesBefore = doc->tree.nodes.size();
Node newNode;
newNode.kind = NodeKind::Struct;
newNode.name = QString();
newNode.parentId = 0;
newNode.offset = 0;
newNode.id = doc->tree.reserveId();
uint64_t newId = newNode.id;
doc->undoStack.push(new RcxCommand(ctrl, cmd::Insert{newNode}));
ctrl->setViewRootId(newId);
QApplication::processEvents();
// Verify new struct
int idx = doc->tree.indexOfId(newId);
QVERIFY(idx >= 0);
QVERIFY(doc->tree.nodes[idx].name.isEmpty());
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Struct);
QCOMPARE(doc->tree.nodes[idx].parentId, (uint64_t)0);
QCOMPARE(ctrl->viewRootId(), newId);
// Command row shows "<no name>"
QVERIFY2(sci->text(0).contains("<no name>"),
qPrintable("Expected '<no name>' in command row, got: " + sci->text(0)));
// -- Undo removes the new struct --
doc->undoStack.undo();
QApplication::processEvents();
QCOMPARE(doc->tree.nodes.size(), nodesBefore);
// Cleanup
delete ctrl;
delete splitter;
delete doc;
}
};
QTEST_MAIN(TestTypeSelector)
#include "test_type_selector.moc"