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

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

View File

@@ -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;"));

View File

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

View File

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