mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
@@ -33,6 +33,8 @@ add_executable(ReclassX
|
||||
src/providerregistry.h
|
||||
src/pluginmanager.cpp
|
||||
src/pluginmanager.h
|
||||
src/typeselectorpopup.h
|
||||
src/typeselectorpopup.cpp
|
||||
)
|
||||
|
||||
target_include_directories(ReclassX PRIVATE src)
|
||||
@@ -136,7 +138,8 @@ if(BUILD_TESTING)
|
||||
|
||||
add_executable(test_controller tests/test_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_link_libraries(test_controller PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
@@ -145,7 +148,8 @@ if(BUILD_TESTING)
|
||||
|
||||
add_executable(test_validation tests/test_validation.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_link_libraries(test_validation PRIVATE
|
||||
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
|
||||
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_link_libraries(test_context_menu PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
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
|
||||
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_link_libraries(test_new_features PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
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()
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
|
||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 20 KiB |
@@ -348,9 +348,17 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
}
|
||||
}
|
||||
|
||||
// Show referenced struct children: at dereferenced address if non-NULL,
|
||||
// otherwise at offset 0 as a struct template preview
|
||||
// Determine if pointer target is actually readable
|
||||
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);
|
||||
if (!state.ptrVisiting.contains(key)) {
|
||||
state.ptrVisiting.insert(key);
|
||||
@@ -358,7 +366,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
if (refIdx >= 0) {
|
||||
const Node& ref = tree.nodes[refIdx];
|
||||
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array)
|
||||
composeParent(state, tree, prov, refIdx,
|
||||
composeParent(state, tree, childProv, refIdx,
|
||||
depth, pBase, ref.id,
|
||||
/*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)
|
||||
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;
|
||||
lm.nodeIdx = -1;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "controller.h"
|
||||
#include "typeselectorpopup.h"
|
||||
#include "providers/process_provider.h"
|
||||
#include "providerregistry.h"
|
||||
#include "processpicker.h"
|
||||
@@ -15,6 +16,7 @@
|
||||
#include <QApplication>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QSettings>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
#ifdef _WIN32
|
||||
#include <psapi.h>
|
||||
@@ -171,9 +173,8 @@ RcxEditor* RcxController::primaryEditor() const {
|
||||
return m_editors.isEmpty() ? nullptr : m_editors.first();
|
||||
}
|
||||
|
||||
RcxEditor* RcxController::addSplitEditor(QSplitter* splitter) {
|
||||
auto* editor = new RcxEditor(splitter);
|
||||
splitter->addWidget(editor);
|
||||
RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
|
||||
auto* editor = new RcxEditor(parent);
|
||||
m_editors.append(editor);
|
||||
connectEditor(editor);
|
||||
|
||||
@@ -186,7 +187,7 @@ RcxEditor* RcxController::addSplitEditor(QSplitter* splitter) {
|
||||
|
||||
void RcxController::removeSplitEditor(RcxEditor* editor) {
|
||||
m_editors.removeOne(editor);
|
||||
editor->deleteLater();
|
||||
// Caller (MainWindow) owns the parent QTabWidget and handles widget destruction.
|
||||
}
|
||||
|
||||
void RcxController::connectEditor(RcxEditor* editor) {
|
||||
@@ -203,6 +204,12 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
handleNodeClick(editor, line, nodeId, mods);
|
||||
});
|
||||
|
||||
// Type selector popup
|
||||
connect(editor, &RcxEditor::typeSelectorRequested,
|
||||
this, [this, editor]() {
|
||||
showTypeSelectorPopup(editor);
|
||||
});
|
||||
|
||||
// Inline editing signals
|
||||
connect(editor, &RcxEditor::inlineEditCommitted,
|
||||
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);
|
||||
});
|
||||
|
||||
menu.addAction(icon("symbol-structure.svg"), "Change &Type\tT", [editor, line]() {
|
||||
menu.addAction("Change &Type\tT", [editor, line]() {
|
||||
editor->beginInlineEdit(EditTarget::Type, line);
|
||||
});
|
||||
|
||||
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) {
|
||||
menu.addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() {
|
||||
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]() {
|
||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||
m_suppressRefresh = true;
|
||||
@@ -1450,22 +1445,35 @@ void RcxController::updateCommandRow() {
|
||||
.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;
|
||||
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) {
|
||||
if (m_viewRootId != 0) {
|
||||
int vi = m_doc->tree.indexOfId(m_viewRootId);
|
||||
if (vi >= 0) {
|
||||
const auto& n = m_doc->tree.nodes[vi];
|
||||
QString keyword = n.resolvedClassKeyword();
|
||||
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
||||
row2 = QStringLiteral("%1\u25BE %2 {")
|
||||
.arg(keyword, className);
|
||||
break;
|
||||
.arg(keyword, className.isEmpty() ? QStringLiteral("<no name>") : className);
|
||||
}
|
||||
}
|
||||
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())
|
||||
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) {
|
||||
ed->setCommandRowText(combined);
|
||||
@@ -1473,6 +1481,63 @@ void RcxController::updateCommandRow() {
|
||||
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) {
|
||||
#ifdef _WIN32
|
||||
HANDLE hProc = OpenProcess(
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
#include <QFutureWatcher>
|
||||
#include <memory>
|
||||
|
||||
class QSplitter;
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class RcxController;
|
||||
@@ -80,7 +78,7 @@ public:
|
||||
~RcxController() override;
|
||||
|
||||
RcxEditor* primaryEditor() const;
|
||||
RcxEditor* addSplitEditor(QSplitter* splitter);
|
||||
RcxEditor* addSplitEditor(QWidget* parent = nullptr);
|
||||
void removeSplitEditor(RcxEditor* editor);
|
||||
QList<RcxEditor*> editors() const { return m_editors; }
|
||||
|
||||
@@ -146,6 +144,7 @@ private:
|
||||
void attachToProcess(uint32_t pid, const QString& processName);
|
||||
void switchToSavedSource(int idx);
|
||||
void pushSavedSourcesToEditors();
|
||||
void showTypeSelectorPopup(RcxEditor* editor);
|
||||
|
||||
// ── Auto-refresh methods ──
|
||||
void setupAutoRefresh();
|
||||
|
||||
12
src/core.h
12
src/core.h
@@ -489,7 +489,7 @@ struct ColumnSpan {
|
||||
|
||||
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount,
|
||||
ArrayElementType, ArrayElementCount, PointerTarget,
|
||||
RootClassType, RootClassName };
|
||||
RootClassType, RootClassName, TypeSelector };
|
||||
|
||||
// Column layout constants (shared with format.cpp span computation)
|
||||
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};
|
||||
}
|
||||
|
||||
// ── 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) ──
|
||||
// Line format: " int32_t[10] name {"
|
||||
// arrayElemTypeSpan covers "int32_t", arrayElemCountSpan covers "10"
|
||||
|
||||
@@ -650,6 +650,11 @@ void RcxEditor::applyCommandRowPills() {
|
||||
clearIndicatorLine(IND_HEX_DIM, 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
|
||||
ColumnSpan srcSpan = commandRowSrcSpan(t);
|
||||
if (srcSpan.valid) {
|
||||
@@ -838,10 +843,12 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
||||
// CommandRow: Source / BaseAddress / Root class (type+name) editing
|
||||
if (lm->lineKind == LineKind::CommandRow) {
|
||||
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);
|
||||
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::RootClassType) s = commandRowRootTypeSpan(lineText);
|
||||
else s = commandRowRootNameSpan(lineText);
|
||||
@@ -959,8 +966,10 @@ static bool hitTestTarget(QsciScintilla* sci,
|
||||
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) {
|
||||
ColumnSpan chevron = commandRowChevronSpan(lineText);
|
||||
if (inSpan(chevron)) { outTarget = EditTarget::TypeSelector; outLine = line; return true; }
|
||||
ColumnSpan ss = commandRowSrcSpan(lineText);
|
||||
if (inSpan(ss)) { outTarget = EditTarget::Source; outLine = line; return true; }
|
||||
ColumnSpan as = commandRowAddrSpan(lineText);
|
||||
@@ -1102,11 +1111,15 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
emit marginClicked(0, h.line, me->modifiers());
|
||||
return true;
|
||||
}
|
||||
// CommandRow: try ADDR edit or consume
|
||||
// CommandRow: try chevron/ADDR edit or consume
|
||||
if (h.nodeId == kCommandRowId) {
|
||||
int tLine; EditTarget t;
|
||||
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t))
|
||||
beginInlineEdit(t, tLine);
|
||||
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) {
|
||||
if (t == EditTarget::TypeSelector)
|
||||
emit typeSelectorRequested();
|
||||
else
|
||||
beginInlineEdit(t, tLine);
|
||||
}
|
||||
return true; // consume all CommandRow clicks
|
||||
}
|
||||
if (h.nodeId != 0) {
|
||||
@@ -1369,6 +1382,7 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
||||
// ── Begin inline edit ──
|
||||
|
||||
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;
|
||||
m_hoveredNodeId = 0;
|
||||
m_hoveredLine = -1;
|
||||
@@ -1937,6 +1951,7 @@ void RcxEditor::applyHoverCursor() {
|
||||
case EditTarget::ArrayElementType:
|
||||
case EditTarget::PointerTarget:
|
||||
case EditTarget::RootClassType:
|
||||
case EditTarget::TypeSelector:
|
||||
desired = Qt::PointingHandCursor;
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -61,6 +61,7 @@ signals:
|
||||
void inlineEditCommitted(int nodeIdx, int subLine,
|
||||
EditTarget target, const QString& text);
|
||||
void inlineEditCancelled();
|
||||
void typeSelectorRequested();
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
|
||||
@@ -93,51 +93,61 @@ struct GenContext {
|
||||
// Forward declarations
|
||||
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) {
|
||||
const NodeTree& tree = ctx.tree;
|
||||
QString name = sanitizeIdent(node.name.isEmpty()
|
||||
? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0'))
|
||||
: node.name);
|
||||
QString oc = offsetComment(node.offset);
|
||||
|
||||
switch (node.kind) {
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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: {
|
||||
if (node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
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: {
|
||||
if (node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
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:
|
||||
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;
|
||||
});
|
||||
|
||||
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) {
|
||||
const Node& child = tree.nodes[ci];
|
||||
int cursor = 0;
|
||||
int i = 0;
|
||||
|
||||
while (i < children.size()) {
|
||||
const Node& child = tree.nodes[children[i]];
|
||||
int childSize;
|
||||
if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
|
||||
childSize = tree.structSpan(child.id, &ctx.childMap);
|
||||
@@ -166,28 +187,40 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
childSize = child.byteSize();
|
||||
|
||||
// Gap before this field
|
||||
if (child.offset > cursor) {
|
||||
int gap = child.offset - cursor;
|
||||
ctx.output += QStringLiteral(" %1 %2[0x%3];\n")
|
||||
.arg(ctx.cType(NodeKind::Padding))
|
||||
.arg(ctx.uniquePadName())
|
||||
.arg(QString::number(gap, 16).toUpper());
|
||||
} else if (child.offset < cursor) {
|
||||
// Overlap
|
||||
if (child.offset > cursor)
|
||||
emitPadRun(cursor, child.offset - cursor);
|
||||
else if (child.offset < cursor)
|
||||
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(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
|
||||
if (child.kind == NodeKind::Struct) {
|
||||
// Ensure the nested struct type is emitted first
|
||||
emitStruct(ctx, child.id);
|
||||
QString typeName = ctx.structName(child);
|
||||
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) {
|
||||
// Check if array has struct element children
|
||||
QVector<int> arrayKids = ctx.childMap.value(child.id);
|
||||
bool hasStructChild = false;
|
||||
QString elemTypeName;
|
||||
@@ -203,11 +236,11 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
|
||||
QString fieldName = sanitizeIdent(child.name);
|
||||
if (hasStructChild && !elemTypeName.isEmpty()) {
|
||||
ctx.output += QStringLiteral(" %1 %2[%3];\n")
|
||||
.arg(elemTypeName, fieldName).arg(child.arrayLen);
|
||||
ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
|
||||
.arg(elemTypeName, fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
|
||||
} else {
|
||||
ctx.output += QStringLiteral(" %1 %2[%3];\n")
|
||||
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen);
|
||||
ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
|
||||
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
|
||||
}
|
||||
} else {
|
||||
ctx.output += emitField(ctx, child) + QStringLiteral("\n");
|
||||
@@ -215,16 +248,12 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
|
||||
int childEnd = child.offset + childSize;
|
||||
if (childEnd > cursor) cursor = childEnd;
|
||||
i++;
|
||||
}
|
||||
|
||||
// Tail padding
|
||||
if (cursor < structSize) {
|
||||
int gap = structSize - cursor;
|
||||
ctx.output += QStringLiteral(" %1 %2[0x%3];\n")
|
||||
.arg(ctx.cType(NodeKind::Padding))
|
||||
.arg(ctx.uniquePadName())
|
||||
.arg(QString::number(gap, 16).toUpper());
|
||||
}
|
||||
if (cursor < structSize)
|
||||
emitPadRun(cursor, structSize - cursor);
|
||||
}
|
||||
|
||||
// ── Emit a complete struct definition ──
|
||||
@@ -294,7 +323,6 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
ctx.emittedTypeNames.insert(typeName);
|
||||
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
||||
|
||||
ctx.output += QStringLiteral("#pragma pack(push, 1)\n");
|
||||
QString kw = node.resolvedClassKeyword();
|
||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic
|
||||
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
|
||||
@@ -302,7 +330,6 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
emitStructBody(ctx, structId);
|
||||
|
||||
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")
|
||||
.arg(typeName)
|
||||
.arg(QString::number(structSize, 16).toUpper());
|
||||
@@ -319,22 +346,39 @@ static QHash<uint64_t, QVector<int>> buildChildMap(const NodeTree& tree) {
|
||||
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) {
|
||||
QStringList parts;
|
||||
QSet<uint64_t> seen;
|
||||
uint64_t cur = nodeId;
|
||||
while (cur != 0 && !seen.contains(cur)) {
|
||||
seen.insert(cur);
|
||||
int idx = tree.indexOfId(cur);
|
||||
if (idx < 0) break;
|
||||
const Node& n = tree.nodes[idx];
|
||||
parts << (n.name.isEmpty() ? QStringLiteral("<unnamed>") : n.name);
|
||||
cur = n.parentId;
|
||||
static QString alignComments(const QString& raw) {
|
||||
QStringList lines = raw.split('\n');
|
||||
|
||||
// First pass: find the maximum code width (text before the marker)
|
||||
int maxCode = 0;
|
||||
for (const QString& line : lines) {
|
||||
int pos = line.indexOf(kCommentMarker);
|
||||
if (pos >= 0)
|
||||
maxCode = qMax(maxCode, pos);
|
||||
}
|
||||
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
|
||||
@@ -350,30 +394,19 @@ QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
||||
if (root.kind != NodeKind::Struct) return {};
|
||||
|
||||
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("// 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");
|
||||
ctx.output += QStringLiteral("#pragma once\n\n");
|
||||
|
||||
emitStruct(ctx, rootStructId);
|
||||
|
||||
return ctx.output;
|
||||
return alignComments(ctx.output);
|
||||
}
|
||||
|
||||
QString renderCppAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases) {
|
||||
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
|
||||
|
||||
ctx.output += QStringLiteral("// Generated by ReclassX\n");
|
||||
ctx.output += QStringLiteral("// Full SDK export\n\n");
|
||||
ctx.output += QStringLiteral("#pragma once\n");
|
||||
ctx.output += QStringLiteral("#include <cstdint>\n\n");
|
||||
ctx.output += QStringLiteral("#pragma once\n\n");
|
||||
|
||||
QVector<int> roots = ctx.childMap.value(0);
|
||||
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);
|
||||
}
|
||||
|
||||
return ctx.output;
|
||||
return alignComments(ctx.output);
|
||||
}
|
||||
|
||||
QString renderNull(const NodeTree&, uint64_t) {
|
||||
|
||||
526
src/main.cpp
526
src/main.cpp
@@ -10,7 +10,8 @@
|
||||
#include <QStatusBar>
|
||||
#include <QLabel>
|
||||
#include <QSplitter>
|
||||
#include <QStackedWidget>
|
||||
#include <QTabWidget>
|
||||
#include <QTabBar>
|
||||
#include <QPointer>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
@@ -38,6 +39,7 @@
|
||||
#include <QDialog>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include <Qsci/qscilexercpp.h>
|
||||
#include <QProxyStyle>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -113,6 +115,18 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
||||
}
|
||||
#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 {
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
@@ -122,6 +136,7 @@ public:
|
||||
|
||||
private slots:
|
||||
void newFile();
|
||||
void newDocument();
|
||||
void selfTest();
|
||||
void openFile();
|
||||
void saveFile();
|
||||
@@ -151,21 +166,26 @@ public:
|
||||
void project_close(QMdiSubWindow* sub = nullptr);
|
||||
|
||||
private:
|
||||
enum ViewMode { VM_Reclass, VM_Rendered };
|
||||
enum ViewMode { VM_Reclass, VM_Rendered, VM_Debug };
|
||||
|
||||
QMdiArea* m_mdiArea;
|
||||
QLabel* m_statusLabel;
|
||||
PluginManager m_pluginManager;
|
||||
|
||||
struct SplitPane {
|
||||
QTabWidget* tabWidget = nullptr;
|
||||
RcxEditor* editor = nullptr;
|
||||
QsciScintilla* rendered = nullptr;
|
||||
ViewMode viewMode = VM_Reclass;
|
||||
uint64_t lastRenderedRootId = 0;
|
||||
};
|
||||
|
||||
struct TabState {
|
||||
RcxDocument* doc;
|
||||
RcxController* ctrl;
|
||||
QSplitter* splitter;
|
||||
QStackedWidget* stack = nullptr;
|
||||
QPointer<QsciScintilla> rendered;
|
||||
ViewMode viewMode = VM_Reclass;
|
||||
uint64_t lastRenderedRootId = 0;
|
||||
int lastRenderedFirstLine = 0;
|
||||
RcxDocument* doc;
|
||||
RcxController* ctrl;
|
||||
QSplitter* splitter;
|
||||
QVector<SplitPane> panes;
|
||||
int activePaneIdx = 0;
|
||||
};
|
||||
QMap<QMdiSubWindow*, TabState> m_tabs;
|
||||
|
||||
@@ -183,11 +203,18 @@ private:
|
||||
void updateWindowTitle();
|
||||
|
||||
void setViewMode(ViewMode mode);
|
||||
void updateRenderedView(TabState& tab);
|
||||
void updateRenderedView(TabState& tab, SplitPane& pane);
|
||||
void updateAllRenderedPanes(TabState& tab);
|
||||
void syncRenderMenuState();
|
||||
uint64_t findRootStructForNode(const NodeTree& tree, uint64_t nodeId) const;
|
||||
void setupRenderedSci(QsciScintilla* sci);
|
||||
|
||||
SplitPane createSplitPane(TabState& tab);
|
||||
void applyTabWidgetStyle(QTabWidget* tw);
|
||||
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
||||
SplitPane* findActiveSplitPane();
|
||||
RcxEditor* activePaneEditor();
|
||||
|
||||
// Workspace dock
|
||||
QDockWidget* m_workspaceDock = nullptr;
|
||||
QTreeView* m_workspaceTree = nullptr;
|
||||
@@ -210,6 +237,15 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
createMenus();
|
||||
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
|
||||
m_pluginManager.LoadPlugins();
|
||||
|
||||
@@ -219,31 +255,30 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
syncRenderMenuState();
|
||||
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) {
|
||||
// Render SVG at 14x14 (2px smaller)
|
||||
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);
|
||||
return QIcon(svgPath);
|
||||
}
|
||||
|
||||
void MainWindow::createMenus() {
|
||||
// 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->addSeparator();
|
||||
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-right.svg"), "&Redo", QKeySequence::Redo, this, &MainWindow::redo);
|
||||
edit->addSeparator();
|
||||
edit->addAction(makeIcon(":/vsicons/symbol-structure.svg"), "&Type Aliases...", this, &MainWindow::showTypeAliasesDialog);
|
||||
edit->addAction("&Type Aliases...", this, &MainWindow::showTypeAliasesDialog);
|
||||
|
||||
// View
|
||||
auto* view = menuBar()->addMenu("&View");
|
||||
@@ -311,30 +346,131 @@ void MainWindow::createStatusBar() {
|
||||
m_statusLabel = new QLabel("Ready");
|
||||
statusBar()->addWidget(m_statusLabel, 1);
|
||||
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) {
|
||||
// QStackedWidget wraps [0] splitter (Reclass view) and [1] rendered QsciScintilla
|
||||
auto* stack = new QStackedWidget;
|
||||
auto* splitter = new QSplitter(Qt::Horizontal);
|
||||
auto* ctrl = new RcxController(doc, splitter);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
|
||||
stack->addWidget(splitter); // index 0 = Reclass view
|
||||
|
||||
auto* renderedSci = new QsciScintilla;
|
||||
setupRenderedSci(renderedSci);
|
||||
stack->addWidget(renderedSci); // index 1 = Rendered view
|
||||
stack->setCurrentIndex(0);
|
||||
|
||||
auto* sub = m_mdiArea->addSubWindow(stack);
|
||||
auto* sub = m_mdiArea->addSubWindow(splitter);
|
||||
sub->setWindowTitle(doc->filePath.isEmpty()
|
||||
? "Untitled" : QFileInfo(doc->filePath).fileName());
|
||||
sub->setAttribute(Qt::WA_DeleteOnClose);
|
||||
sub->showMaximized();
|
||||
|
||||
m_tabs[sub] = { doc, ctrl, splitter, stack, renderedSci,
|
||||
VM_Reclass, 0, 0 };
|
||||
m_tabs[sub] = { doc, ctrl, splitter, {}, 0 };
|
||||
auto& tab = m_tabs[sub];
|
||||
|
||||
// Create the initial split pane
|
||||
tab.panes.append(createSplitPane(tab));
|
||||
|
||||
connect(sub, &QObject::destroyed, this, [this, sub]() {
|
||||
auto it = m_tabs.find(sub);
|
||||
@@ -349,8 +485,8 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
this, [this, ctrl, sub](int nodeIdx) {
|
||||
if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) {
|
||||
auto& node = ctrl->document()->tree.nodes[nodeIdx];
|
||||
auto it = m_tabs.find(sub);
|
||||
if (it != m_tabs.end() && it->viewMode == VM_Rendered)
|
||||
auto* ap = findActiveSplitPane();
|
||||
if (ap && ap->viewMode == VM_Rendered)
|
||||
m_statusLabel->setText(
|
||||
QString("Rendered: %1 %2")
|
||||
.arg(kindToString(node.kind))
|
||||
@@ -365,10 +501,10 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
} else {
|
||||
m_statusLabel->setText("Ready");
|
||||
}
|
||||
// Update rendered view on selection change
|
||||
// Update all rendered panes on selection change
|
||||
auto it = m_tabs.find(sub);
|
||||
if (it != m_tabs.end())
|
||||
updateRenderedView(*it);
|
||||
updateAllRenderedPanes(*it);
|
||||
});
|
||||
connect(ctrl, &RcxController::selectionChanged,
|
||||
this, [this](int count) {
|
||||
@@ -378,14 +514,14 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
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,
|
||||
this, [this, sub]() {
|
||||
auto it = m_tabs.find(sub);
|
||||
if (it != m_tabs.end())
|
||||
QTimer::singleShot(0, this, [this, sub]() {
|
||||
auto it2 = m_tabs.find(sub);
|
||||
if (it2 != m_tabs.end()) updateRenderedView(*it2);
|
||||
if (it2 != m_tabs.end()) updateAllRenderedPanes(*it2);
|
||||
rebuildWorkspaceModel();
|
||||
});
|
||||
});
|
||||
@@ -395,7 +531,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
if (it != m_tabs.end())
|
||||
QTimer::singleShot(0, this, [this, 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;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
project_new();
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
QString demoPath = QCoreApplication::applicationDirPath() + "/demo.rcx";
|
||||
if (QFile::exists(demoPath)) {
|
||||
project_open(demoPath);
|
||||
} else {
|
||||
// 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);
|
||||
void MainWindow::newDocument() {
|
||||
auto* tab = activeTab();
|
||||
if (!tab) {
|
||||
project_new();
|
||||
return;
|
||||
}
|
||||
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() {
|
||||
@@ -506,7 +676,7 @@ void MainWindow::addNode() {
|
||||
if (!ctrl) return;
|
||||
|
||||
uint64_t parentId = ctrl->viewRootId(); // default to current view root
|
||||
auto* primary = ctrl->primaryEditor();
|
||||
auto* primary = activePaneEditor();
|
||||
if (primary && primary->isEditing()) return;
|
||||
if (primary) {
|
||||
int ni = primary->currentNodeIndex();
|
||||
@@ -524,7 +694,7 @@ void MainWindow::addNode() {
|
||||
void MainWindow::removeNode() {
|
||||
auto* ctrl = activeController();
|
||||
if (!ctrl) return;
|
||||
auto* primary = ctrl->primaryEditor();
|
||||
auto* primary = activePaneEditor();
|
||||
if (!primary || primary->isEditing()) return;
|
||||
QSet<int> indices = primary->selectedNodeIndices();
|
||||
if (indices.size() > 1) {
|
||||
@@ -537,7 +707,7 @@ void MainWindow::removeNode() {
|
||||
void MainWindow::changeNodeType() {
|
||||
auto* ctrl = activeController();
|
||||
if (!ctrl) return;
|
||||
auto* primary = ctrl->primaryEditor();
|
||||
auto* primary = activePaneEditor();
|
||||
if (!primary) return;
|
||||
primary->beginInlineEdit(EditTarget::Type);
|
||||
}
|
||||
@@ -545,7 +715,7 @@ void MainWindow::changeNodeType() {
|
||||
void MainWindow::renameNodeAction() {
|
||||
auto* ctrl = activeController();
|
||||
if (!ctrl) return;
|
||||
auto* primary = ctrl->primaryEditor();
|
||||
auto* primary = activePaneEditor();
|
||||
if (!primary) return;
|
||||
primary->beginInlineEdit(EditTarget::Name);
|
||||
}
|
||||
@@ -553,7 +723,7 @@ void MainWindow::renameNodeAction() {
|
||||
void MainWindow::duplicateNodeAction() {
|
||||
auto* ctrl = activeController();
|
||||
if (!ctrl) return;
|
||||
auto* primary = ctrl->primaryEditor();
|
||||
auto* primary = activePaneEditor();
|
||||
if (!primary || primary->isEditing()) return;
|
||||
int ni = primary->currentNodeIndex();
|
||||
if (ni >= 0) ctrl->duplicateNode(ni);
|
||||
@@ -562,15 +732,16 @@ void MainWindow::duplicateNodeAction() {
|
||||
void MainWindow::splitView() {
|
||||
auto* tab = activeTab();
|
||||
if (!tab) return;
|
||||
tab->ctrl->addSplitEditor(tab->splitter);
|
||||
tab->panes.append(createSplitPane(*tab));
|
||||
}
|
||||
|
||||
void MainWindow::unsplitView() {
|
||||
auto* tab = activeTab();
|
||||
if (!tab) return;
|
||||
auto editors = tab->ctrl->editors();
|
||||
if (editors.size() > 1)
|
||||
tab->ctrl->removeSplitEditor(editors.last());
|
||||
if (!tab || tab->panes.size() <= 1) return;
|
||||
auto pane = tab->panes.takeLast();
|
||||
tab->ctrl->removeSplitEditor(pane.editor);
|
||||
pane.tabWidget->deleteLater();
|
||||
tab->activePaneIdx = qBound(0, tab->activePaneIdx, tab->panes.size() - 1);
|
||||
}
|
||||
|
||||
void MainWindow::undo() {
|
||||
@@ -598,20 +769,27 @@ void MainWindow::setEditorFont(const QString& fontName) {
|
||||
f.setFixedPitch(true);
|
||||
for (auto& state : m_tabs) {
|
||||
state.ctrl->setEditorFont(fontName);
|
||||
// Also update the rendered view font
|
||||
if (state.rendered) {
|
||||
state.rendered->setFont(f);
|
||||
if (auto* lex = state.rendered->lexer()) {
|
||||
lex->setFont(f);
|
||||
for (int i = 0; i <= 127; i++)
|
||||
lex->setFont(f, i);
|
||||
for (auto& pane : state.panes) {
|
||||
// Update rendered view font
|
||||
if (pane.rendered) {
|
||||
pane.rendered->setFont(f);
|
||||
if (auto* lex = pane.rendered->lexer()) {
|
||||
lex->setFont(f);
|
||||
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
|
||||
if (m_workspaceTree)
|
||||
m_workspaceTree->setFont(f);
|
||||
// Sync status bar font
|
||||
statusBar()->setFont(f);
|
||||
}
|
||||
|
||||
RcxController* MainWindow::activeController() const {
|
||||
@@ -650,14 +828,10 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) {
|
||||
f.setFixedPitch(true);
|
||||
|
||||
sci->setFont(f);
|
||||
sci->setReadOnly(true);
|
||||
sci->setReadOnly(false);
|
||||
sci->setWrapMode(QsciScintilla::WrapNone);
|
||||
sci->setCaretLineVisible(false);
|
||||
sci->setPaper(QColor("#1e1e1e"));
|
||||
sci->setColor(QColor("#d4d4d4"));
|
||||
sci->setTabWidth(4);
|
||||
sci->setIndentationsUseTabs(false);
|
||||
sci->setCaretForegroundColor(QColor("#d4d4d4"));
|
||||
sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2);
|
||||
sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2);
|
||||
|
||||
@@ -672,7 +846,8 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) {
|
||||
sci->setMarginWidth(1, 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);
|
||||
lexer->setFont(f);
|
||||
lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword);
|
||||
@@ -693,28 +868,34 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) {
|
||||
}
|
||||
sci->setLexer(lexer);
|
||||
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 ──
|
||||
|
||||
void MainWindow::setViewMode(ViewMode mode) {
|
||||
auto* tab = activeTab();
|
||||
if (!tab) return;
|
||||
tab->viewMode = mode;
|
||||
if (tab->stack) {
|
||||
tab->stack->setCurrentIndex(mode == VM_Rendered ? 1 : 0);
|
||||
}
|
||||
if (mode == VM_Rendered) {
|
||||
updateRenderedView(*tab);
|
||||
}
|
||||
auto* pane = findActiveSplitPane();
|
||||
if (!pane) return;
|
||||
pane->viewMode = mode;
|
||||
int idx = (mode == VM_Rendered) ? 1 : (mode == VM_Debug) ? 2 : 0;
|
||||
pane->tabWidget->setCurrentIndex(idx);
|
||||
// The QTabWidget::currentChanged signal will handle updating the rendered view
|
||||
syncRenderMenuState();
|
||||
}
|
||||
|
||||
void MainWindow::syncRenderMenuState() {
|
||||
auto* tab = activeTab();
|
||||
bool rendered = tab && tab->viewMode == VM_Rendered;
|
||||
if (m_actViewRendered) m_actViewRendered->setEnabled(!rendered);
|
||||
if (m_actViewReclass) m_actViewReclass->setEnabled(rendered);
|
||||
auto* pane = findActiveSplitPane();
|
||||
ViewMode vm = pane ? pane->viewMode : VM_Reclass;
|
||||
if (m_actViewRendered) m_actViewRendered->setEnabled(vm != VM_Rendered);
|
||||
if (m_actViewReclass) m_actViewReclass->setEnabled(vm != VM_Reclass);
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
}
|
||||
|
||||
// ── Update the rendered view for a tab ──
|
||||
// ── Update the rendered view for a single pane ──
|
||||
|
||||
void MainWindow::updateRenderedView(TabState& tab) {
|
||||
if (tab.viewMode != VM_Rendered) return;
|
||||
if (!tab.rendered) return;
|
||||
void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) {
|
||||
if (pane.viewMode != VM_Rendered) return;
|
||||
if (!pane.rendered) return;
|
||||
|
||||
// Determine which struct to render based on selection
|
||||
uint64_t rootId = 0;
|
||||
@@ -763,26 +944,31 @@ void MainWindow::updateRenderedView(TabState& tab) {
|
||||
|
||||
// Scroll restoration: save if same root, reset if different
|
||||
int restoreLine = 0;
|
||||
if (rootId != 0 && rootId == tab.lastRenderedRootId) {
|
||||
restoreLine = (int)tab.rendered->SendScintilla(
|
||||
if (rootId != 0 && rootId == pane.lastRenderedRootId) {
|
||||
restoreLine = (int)pane.rendered->SendScintilla(
|
||||
QsciScintillaBase::SCI_GETFIRSTVISIBLELINE);
|
||||
}
|
||||
tab.lastRenderedRootId = rootId;
|
||||
pane.lastRenderedRootId = rootId;
|
||||
|
||||
// Set text
|
||||
tab.rendered->setReadOnly(false);
|
||||
tab.rendered->setText(text);
|
||||
tab.rendered->setReadOnly(true);
|
||||
pane.rendered->setText(text);
|
||||
|
||||
// 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');
|
||||
tab.rendered->setMarginWidth(0, marginStr);
|
||||
pane.rendered->setMarginWidth(0, marginStr);
|
||||
|
||||
// Restore scroll
|
||||
if (restoreLine > 0) {
|
||||
tab.rendered->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE,
|
||||
(unsigned long)restoreLine);
|
||||
pane.rendered->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE,
|
||||
(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() {
|
||||
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->tree.baseAddress = 0x00400000;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Entity";
|
||||
root.structTypeName = "Entity";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = doc->tree.addNode(root);
|
||||
uint64_t rootId = doc->tree.nodes[ri].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "health"; n.parentId = rootId; n.offset = 0; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "armor"; n.parentId = rootId; n.offset = 4; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = rootId; n.offset = 8; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "flags"; n.parentId = rootId; n.offset = 12; doc->tree.addNode(n); }
|
||||
// Build Ball + Material demo structs
|
||||
buildBallDemo(doc->tree);
|
||||
|
||||
auto* sub = createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
|
||||
350
src/typeselectorpopup.cpp
Normal file
350
src/typeselectorpopup.cpp
Normal 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
58
src/typeselectorpopup.h
Normal 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
|
||||
@@ -54,18 +54,16 @@ private slots:
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
// Header
|
||||
QVERIFY(result.contains("Generated by ReclassX"));
|
||||
QVERIFY(result.contains("#pragma once"));
|
||||
QVERIFY(result.contains("#include <cstdint>"));
|
||||
QVERIFY(!result.contains("#include <cstdint>"));
|
||||
QVERIFY(!result.contains("#pragma pack"));
|
||||
|
||||
// Struct definition
|
||||
QVERIFY(result.contains("#pragma pack(push, 1)"));
|
||||
QVERIFY(result.contains("struct Player {"));
|
||||
QVERIFY(result.contains("int32_t health;"));
|
||||
QVERIFY(result.contains("float speed;"));
|
||||
QVERIFY(result.contains("uint64_t id;"));
|
||||
QVERIFY(result.contains("};"));
|
||||
QVERIFY(result.contains("#pragma pack(pop)"));
|
||||
|
||||
// static_assert - struct is 16 bytes (0+4 + 4+4 + 8+8 = 16)
|
||||
QVERIFY(result.contains("static_assert(sizeof(Player) == 0x10"));
|
||||
@@ -485,7 +483,6 @@ private slots:
|
||||
|
||||
QString result = rcx::renderCppAll(tree);
|
||||
|
||||
QVERIFY(result.contains("Full SDK export"));
|
||||
QVERIFY(result.contains("struct StructA {"));
|
||||
QVERIFY(result.contains("struct StructB {"));
|
||||
QVERIFY(result.contains("uint32_t valueA;"));
|
||||
|
||||
361
tests/test_rendered_view.cpp
Normal file
361
tests/test_rendered_view.cpp
Normal 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"
|
||||
223
tests/test_type_selector.cpp
Normal file
223
tests/test_type_selector.cpp
Normal 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"
|
||||
Reference in New Issue
Block a user