Unified type popup: explicit TypeEntry model, modifier toggles, section headers

- Replace sentinel-id scheme (kPrimBase) with TypeEntry::Kind discriminant
- Merge showTypeSelectorPopup + showTypePickerPopup into single showTypePopup
- Merge applyTypePickerResult into applyTypePopupResult matching on entryKind
- Add modifier toggle buttons (plain, *, **, [n]) with array count input
- Add section headers (primitives / project types) with dim centered styling
- Root mode shows project types first; non-Root sorts same-size primitives first
- Remove popup outside border, flat "Create new type" button
- Add parseTypeSpec parser, update tests with new TypeEntry API
This commit is contained in:
IChooseYou
2026-02-11 13:39:43 -07:00
committed by sysadmin
parent 33a093ae7d
commit db5d3ae311
6 changed files with 889 additions and 241 deletions

View File

@@ -2,14 +2,19 @@
#include <QtTest/QSignalSpy>
#include <QApplication>
#include <QSplitter>
#include <QElapsedTimer>
#include <QVBoxLayout>
#include <QToolButton>
#include <QLineEdit>
#include <QListView>
#include <QStringListModel>
#include <Qsci/qsciscintilla.h>
#include "controller.h"
#include "typeselectorpopup.h"
#include "themes/thememanager.h"
#include "core.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
Q_DECLARE_METATYPE(uint64_t)
#endif
Q_DECLARE_METATYPE(rcx::TypeEntry)
using namespace rcx;
@@ -49,9 +54,7 @@ class TestTypeSelector : public QObject {
private slots:
void initTestCase() {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qRegisterMetaType<uint64_t>("uint64_t");
#endif
qRegisterMetaType<TypeEntry>("TypeEntry");
}
// ── Chevron span detection ──
@@ -89,6 +92,112 @@ private slots:
QCOMPARE(text.mid(rootName.start, rootName.end - rootName.start).trimmed(), QString("Alpha"));
}
// ── Benchmark: warmUp() + cached reuse vs cold new/delete ──
void benchmarkPopupOpen() {
auto makeComposite = [](uint64_t id, const QString& name, const QString& kw) {
TypeEntry e;
e.entryKind = TypeEntry::Composite;
e.structId = id;
e.displayName = name;
e.classKeyword = kw;
return e;
};
QVector<TypeEntry> types;
types.append(makeComposite(1, "Alpha", "struct"));
types.append(makeComposite(2, "Bravo", "struct"));
types.append(makeComposite(3, "Charlie", "struct"));
types.append(makeComposite(4, "Delta", "class"));
TypeEntry cur1 = makeComposite(1, "Alpha", "struct");
TypeEntry cur2 = makeComposite(2, "Bravo", "struct");
QFont font("Consolas", 12);
font.setFixedPitch(true);
auto ms = [](qint64 ns) { return QString::number(ns / 1000000.0, 'f', 2); };
// --- Measure cold path: new popup, first show ever ---
{
QElapsedTimer total;
total.start();
auto* popup = new TypeSelectorPopup();
popup->setFont(font);
popup->setTypes(types, &cur1);
popup->popup(QPoint(100, 100));
QApplication::processEvents();
qint64 tCold = total.nsecsElapsed();
popup->hide();
QApplication::processEvents();
qDebug() << "";
qDebug().noquote() << QString("=== COLD (new popup, no warmUp) ===");
qDebug().noquote() << QString(" Total: %1 ms").arg(ms(tCold));
// --- Measure cached reuse of same instance ---
{
QElapsedTimer t2;
t2.start();
popup->setTypes(types, &cur2);
popup->popup(QPoint(100, 100));
QApplication::processEvents();
qint64 tReuse = t2.nsecsElapsed();
popup->hide();
QApplication::processEvents();
qDebug() << "";
qDebug().noquote() << QString("=== WARM (reuse same popup) ===");
qDebug().noquote() << QString(" Total: %1 ms").arg(ms(tReuse));
}
delete popup;
}
// --- Measure warmUp() approach ---
{
QElapsedTimer tWarmup;
tWarmup.start();
auto* popup2 = new TypeSelectorPopup();
popup2->warmUp();
qint64 tWarmMs = tWarmup.nsecsElapsed();
qDebug() << "";
qDebug().noquote() << QString("=== warmUp() cost (constructor + hidden show/hide) ===");
qDebug().noquote() << QString(" Total: %1 ms").arg(ms(tWarmMs));
// First user-visible show after warmUp
QElapsedTimer t3;
t3.start();
popup2->setFont(font);
popup2->setTypes(types, &cur1);
popup2->popup(QPoint(100, 100));
QApplication::processEvents();
qint64 tFirst = t3.nsecsElapsed();
popup2->hide();
QApplication::processEvents();
qDebug() << "";
qDebug().noquote() << QString("=== FIRST visible show after warmUp() ===");
qDebug().noquote() << QString(" Total: %1 ms").arg(ms(tFirst));
// Second show (fully warm)
QElapsedTimer t4;
t4.start();
popup2->setTypes(types, &cur2);
popup2->popup(QPoint(100, 100));
QApplication::processEvents();
qint64 tSecond = t4.nsecsElapsed();
popup2->hide();
QApplication::processEvents();
qDebug() << "";
qDebug().noquote() << QString("=== SECOND visible show after warmUp() ===");
qDebug().noquote() << QString(" Total: %1 ms").arg(ms(tSecond));
delete popup2;
}
}
// ── Popup data model ──
void testPopupListsRootStructs() {
@@ -98,8 +207,12 @@ private slots:
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()});
TypeEntry e;
e.entryKind = TypeEntry::Composite;
e.structId = n.id;
e.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
e.classKeyword = n.resolvedClassKeyword();
types.append(e);
}
}
@@ -112,14 +225,28 @@ private slots:
void testPopupSignals() {
TypeSelectorPopup popup;
popup.setTypes({{1, "A", "struct"}, {2, "B", "struct"}}, 1);
TypeEntry eA;
eA.entryKind = TypeEntry::Composite;
eA.structId = 1;
eA.displayName = "A";
eA.classKeyword = "struct";
TypeEntry eB;
eB.entryKind = TypeEntry::Composite;
eB.structId = 2;
eB.displayName = "B";
eB.classKeyword = "struct";
QVector<TypeEntry> types;
types.append(eA);
types.append(eB);
popup.setTypes(types, &eA);
QSignalSpy typeSpy(&popup, &TypeSelectorPopup::typeSelected);
QSignalSpy createSpy(&popup, &TypeSelectorPopup::createNewTypeRequested);
emit popup.typeSelected(2, QStringLiteral("B"));
emit popup.typeSelected(eB, QStringLiteral("B"));
QCOMPARE(typeSpy.count(), 1);
QCOMPARE(typeSpy.at(0).at(0).toULongLong(), (uint64_t)2);
// Verify the entry came through — check the fullText (second arg)
QCOMPARE(typeSpy.at(0).at(1).toString(), QStringLiteral("B"));
emit popup.createNewTypeRequested();
@@ -227,6 +354,85 @@ private slots:
delete splitter;
delete doc;
}
// ── parseTypeSpec tests ──
void testParseTypeSpecPlain() {
TypeSpec spec = parseTypeSpec("int32_t");
QCOMPARE(spec.baseName, QString("int32_t"));
QVERIFY(!spec.isPointer);
QCOMPARE(spec.arrayCount, 0);
}
void testParseTypeSpecArray() {
TypeSpec spec = parseTypeSpec("int32_t[10]");
QCOMPARE(spec.baseName, QString("int32_t"));
QVERIFY(!spec.isPointer);
QCOMPARE(spec.arrayCount, 10);
}
void testParseTypeSpecPointer() {
TypeSpec spec = parseTypeSpec("Ball*");
QCOMPARE(spec.baseName, QString("Ball"));
QVERIFY(spec.isPointer);
QCOMPARE(spec.arrayCount, 0);
}
void testParseTypeSpecDoublePointer() {
TypeSpec spec = parseTypeSpec("Ball**");
QCOMPARE(spec.baseName, QString("Ball"));
QVERIFY(spec.isPointer);
}
void testParseTypeSpecEmpty() {
TypeSpec spec = parseTypeSpec("");
QVERIFY(spec.baseName.isEmpty());
QVERIFY(!spec.isPointer);
QCOMPARE(spec.arrayCount, 0);
}
void testParseTypeSpecWhitespace() {
TypeSpec spec = parseTypeSpec(" Ball * ");
// trimmed → "Ball *", ends with '*'
QCOMPARE(spec.baseName, QString("Ball"));
QVERIFY(spec.isPointer);
}
void testParseTypeSpecArrayZero() {
// [0] parses baseName but arrayCount stays 0 (invalid count)
TypeSpec spec = parseTypeSpec("int32_t[0]");
QCOMPARE(spec.baseName, QString("int32_t"));
QCOMPARE(spec.arrayCount, 0);
}
// ── Section headers in filtered list ──
void testSectionHeadersPresent() {
TypeSelectorPopup popup;
// Build entries with both primitives and composites
QVector<TypeEntry> types;
TypeEntry prim;
prim.entryKind = TypeEntry::Primitive;
prim.primitiveKind = NodeKind::Int32;
prim.displayName = "int32_t";
types.append(prim);
TypeEntry comp;
comp.entryKind = TypeEntry::Composite;
comp.structId = 42;
comp.displayName = "MyStruct";
comp.classKeyword = "struct";
types.append(comp);
popup.setTypes(types);
// After setTypes, the internal filtered list should have section headers
// We can verify this indirectly by checking the model row count
// (should be > 2 due to section headers)
auto* listView = popup.findChild<QListView*>();
QVERIFY(listView);
QVERIFY(listView->model()->rowCount() > 2);
}
};
QTEST_MAIN(TestTypeSelector)