From db5d3ae3119ca7bc561e1562ea4f3e7f5ecccad7 Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Wed, 11 Feb 2026 13:39:43 -0700 Subject: [PATCH] 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 --- src/controller.cpp | 319 +++++++++++++---------- src/controller.h | 12 +- src/editor.cpp | 22 +- src/typeselectorpopup.cpp | 491 ++++++++++++++++++++++++++++++----- src/typeselectorpopup.h | 58 ++++- tests/test_type_selector.cpp | 228 +++++++++++++++- 6 files changed, 889 insertions(+), 241 deletions(-) diff --git a/src/controller.cpp b/src/controller.cpp index eaf7952..092a92c 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -203,13 +203,18 @@ void RcxController::connectEditor(RcxEditor* editor) { // Type selector popup (command row chevron) connect(editor, &RcxEditor::typeSelectorRequested, this, [this, editor]() { - showTypeSelectorPopup(editor); + showTypePopup(editor, TypePopupMode::Root, -1, QPoint()); }); // Type picker popup (array element type / pointer target) connect(editor, &RcxEditor::typePickerRequested, this, [this, editor](EditTarget target, int nodeIdx, QPoint globalPos) { - showTypePickerPopup(editor, target, nodeIdx, globalPos); + TypePopupMode mode = TypePopupMode::FieldType; + if (target == EditTarget::ArrayElementType) + mode = TypePopupMode::ArrayElement; + else if (target == EditTarget::PointerTarget) + mode = TypePopupMode::PointerTarget; + showTypePopup(editor, mode, nodeIdx, globalPos); }); // Inline editing signals @@ -1505,20 +1510,118 @@ void RcxController::updateCommandRow() { emit selectionChanged(m_selIds.size()); } -void RcxController::showTypeSelectorPopup(RcxEditor* editor) { - // Collect all root-level struct types - QVector 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); +TypeSelectorPopup* RcxController::ensurePopup(RcxEditor* editor) { + if (!m_cachedPopup) { + m_cachedPopup = new TypeSelectorPopup(editor); + // Pre-warm: force native window creation so first visible show is fast + m_cachedPopup->warmUp(); + } + // Disconnect previous signals so we can reconnect fresh + m_cachedPopup->disconnect(this); + return m_cachedPopup; +} + +void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode, + int nodeIdx, QPoint globalPos) { + const Node* node = nullptr; + if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) + node = &m_doc->tree.nodes[nodeIdx]; + + // ── Build entry list based on mode ── + QVector entries; + TypeEntry currentEntry; + bool hasCurrent = false; + + auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) { + for (const auto& m : kKindMeta) { + if (m.kind == NodeKind::Padding) continue; + if (excludeStructArrayPad && + (m.kind == NodeKind::Struct || m.kind == NodeKind::Array)) + continue; + TypeEntry e; + e.entryKind = TypeEntry::Primitive; + e.primitiveKind = m.kind; + e.displayName = QString::fromLatin1(m.typeName); + e.enabled = enabled; + entries.append(e); } + }; + + auto addComposites = [&](const std::function& isCurrent) { + for (const auto& n : m_doc->tree.nodes) { + if (n.parentId != 0 || n.kind != NodeKind::Struct) continue; + TypeEntry e; + e.entryKind = TypeEntry::Composite; + e.structId = n.id; + e.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName; + e.classKeyword = n.resolvedClassKeyword(); + entries.append(e); + if (!hasCurrent && node && isCurrent(*node, e)) { + currentEntry = e; + hasCurrent = true; + } + } + }; + + switch (mode) { + case TypePopupMode::Root: + addPrimitives(/*enabled=*/false, /*excludeStructArrayPad=*/false); + addComposites([&](const Node&, const TypeEntry& e) { + return e.structId == m_viewRootId; + }); + break; + + case TypePopupMode::FieldType: + addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false); + if (node) { + // Mark current primitive + for (auto& e : entries) { + if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) { + currentEntry = e; + hasCurrent = true; + break; + } + } + } + addComposites([](const Node&, const TypeEntry&) { return false; }); + break; + + case TypePopupMode::ArrayElement: + addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true); + if (node) { + for (auto& e : entries) { + if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) { + currentEntry = e; + hasCurrent = true; + break; + } + } + } + addComposites([](const Node& n, const TypeEntry& e) { + return n.elementKind == NodeKind::Struct && n.refId == e.structId; + }); + break; + + case TypePopupMode::PointerTarget: { + // "void" entry as a primitive with a special display + TypeEntry voidEntry; + voidEntry.entryKind = TypeEntry::Primitive; + voidEntry.primitiveKind = NodeKind::Hex8; // unused, but needs a value + voidEntry.displayName = QStringLiteral("void"); + voidEntry.enabled = true; + entries.append(voidEntry); + if (node && node->refId == 0) { + currentEntry = voidEntry; + hasCurrent = true; + } + addComposites([](const Node& n, const TypeEntry& e) { + return n.refId == e.structId; + }); + break; + } } - // Get font with zoom + // ── Font with zoom ── QSettings settings("ReclassX", "ReclassX"); QString fontName = settings.value("font", "JetBrains Mono").toString(); QFont font(fontName, 12); @@ -1527,26 +1630,45 @@ void RcxController::showTypeSelectorPopup(RcxEditor* editor) { 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)); + // ── Position ── + QPoint pos = globalPos; + if (mode == TypePopupMode::Root) { + // 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); + pos = sci->viewport()->mapToGlobal(QPoint(x, y + lineH)); + } - auto* popup = new TypeSelectorPopup(editor); + // ── Configure and show popup ── + auto* popup = ensurePopup(editor); popup->setFont(font); - popup->setTypes(types, m_viewRootId); + popup->setMode(mode); + + // Pass current node size for same-size sorting + int nodeSize = 0; + if (node) { + if (mode == TypePopupMode::ArrayElement) + nodeSize = sizeForKind(node->elementKind); + else + nodeSize = sizeForKind(node->kind); + } + popup->setCurrentNodeSize(nodeSize); + + static const char* titles[] = { "Change root", "Change type", + "Element type", "Pointer target" }; + popup->setTitle(QString::fromLatin1(titles[(int)mode])); + popup->setTypes(entries, hasCurrent ? ¤tEntry : nullptr); connect(popup, &TypeSelectorPopup::typeSelected, - this, [this](uint64_t structId, const QString&) { - setViewRootId(structId); + this, [this, mode, nodeIdx](const TypeEntry& entry, const QString& fullText) { + applyTypePopupResult(mode, nodeIdx, entry, fullText); }); connect(popup, &TypeSelectorPopup::createNewTypeRequested, - this, [this]() { - // Create a new root struct with no name + this, [this, mode, nodeIdx]() { Node n; n.kind = NodeKind::Struct; n.name = QString(); @@ -1554,144 +1676,57 @@ void RcxController::showTypeSelectorPopup(RcxEditor* editor) { n.offset = 0; n.id = m_doc->tree.reserveId(); m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n})); - setViewRootId(n.id); + TypeEntry newEntry; + newEntry.entryKind = TypeEntry::Composite; + newEntry.structId = n.id; + applyTypePopupResult(mode, nodeIdx, newEntry, QString()); }); - connect(popup, &TypeSelectorPopup::dismissed, - popup, &QObject::deleteLater); popup->popup(pos); } -void RcxController::showTypePickerPopup(RcxEditor* editor, EditTarget target, - int nodeIdx, QPoint globalPos) { - if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; - const Node& node = m_doc->tree.nodes[nodeIdx]; - - QVector entries; - uint64_t currentId = 0; - - // Sentinel range for primitive entries: UINT64_MAX - kind - constexpr uint64_t kPrimBase = UINT64_MAX - 256; - constexpr uint64_t kNoSelection = UINT64_MAX; - currentId = kNoSelection; - - if (target == EditTarget::ArrayElementType) { - // Primitive types (unique synthetic id per kind) - for (const auto& m : kKindMeta) { - if (m.kind == NodeKind::Struct || m.kind == NodeKind::Array - || m.kind == NodeKind::Padding) continue; - TypeEntry e; - e.id = kPrimBase - (uint64_t)m.kind; - e.displayName = QString::fromLatin1(m.typeName); - entries.append(e); - if (m.kind == node.elementKind) - currentId = e.id; - } - // Struct types - for (const auto& n : m_doc->tree.nodes) { - if (n.parentId == 0 && n.kind == NodeKind::Struct) { - TypeEntry e; - e.id = n.id; - e.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName; - e.classKeyword = n.resolvedClassKeyword(); - entries.append(e); - if (node.elementKind == NodeKind::Struct && n.id == node.refId) - currentId = n.id; - } - } - } else if (target == EditTarget::PointerTarget) { - // "void" entry - { - TypeEntry e; - e.id = kPrimBase; // unique sentinel for void - e.displayName = QStringLiteral("void"); - entries.append(e); - if (node.refId == 0) currentId = e.id; - } - // Struct types - for (const auto& n : m_doc->tree.nodes) { - if (n.parentId == 0 && n.kind == NodeKind::Struct) { - TypeEntry e; - e.id = n.id; - e.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName; - e.classKeyword = n.resolvedClassKeyword(); - entries.append(e); - if (n.id == node.refId) currentId = n.id; - } - } +void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx, + const TypeEntry& entry, const QString& fullText) { + if (mode == TypePopupMode::Root) { + if (entry.entryKind == TypeEntry::Composite) + setViewRootId(entry.structId); + return; } - // Font with zoom - QSettings settings("ReclassX", "ReclassX"); - QString fontName = settings.value("font", "JetBrains Mono").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); - - auto* popup = new TypeSelectorPopup(editor); - popup->setFont(font); - popup->setTitle(target == EditTarget::ArrayElementType - ? QStringLiteral("Choose element type") - : QStringLiteral("Choose pointer target")); - popup->setTypes(entries, currentId); - - connect(popup, &TypeSelectorPopup::typeSelected, - this, [this, target, nodeIdx](uint64_t id, const QString& displayName) { - applyTypePickerResult(target, nodeIdx, id, displayName); - }); - connect(popup, &TypeSelectorPopup::createNewTypeRequested, - this, [this, target, nodeIdx]() { - 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})); - applyTypePickerResult(target, nodeIdx, n.id, QString()); - }); - connect(popup, &TypeSelectorPopup::dismissed, - popup, &QObject::deleteLater); - - popup->popup(globalPos); -} - -void RcxController::applyTypePickerResult(EditTarget target, int nodeIdx, - uint64_t selectedId, const QString& displayName) { if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; const Node& node = m_doc->tree.nodes[nodeIdx]; - constexpr uint64_t kPrimBase = UINT64_MAX - 256; + // Parse the full text for modifiers (e.g. "int32_t[10]", "Ball*") + TypeSpec spec = parseTypeSpec(fullText); - if (target == EditTarget::ArrayElementType) { - if (selectedId >= kPrimBase) { - // Primitive type — resolve from displayName - bool ok; - NodeKind elemKind = kindFromTypeName(displayName, &ok); - if (ok && elemKind != node.elementKind) { + if (mode == TypePopupMode::FieldType) { + if (entry.entryKind == TypeEntry::Primitive) { + if (entry.primitiveKind != node.kind) + changeNodeKind(nodeIdx, entry.primitiveKind); + } + } else if (mode == TypePopupMode::ArrayElement) { + if (entry.entryKind == TypeEntry::Primitive) { + if (entry.primitiveKind != node.elementKind) { m_doc->undoStack.push(new RcxCommand(this, cmd::ChangeArrayMeta{node.id, - node.elementKind, elemKind, + node.elementKind, entry.primitiveKind, node.arrayLen, node.arrayLen})); } - } else { - // Struct type — real node id - if (node.elementKind != NodeKind::Struct || node.refId != selectedId) { + } else if (entry.entryKind == TypeEntry::Composite) { + if (node.elementKind != NodeKind::Struct || node.refId != entry.structId) { m_doc->undoStack.push(new RcxCommand(this, cmd::ChangeArrayMeta{node.id, node.elementKind, NodeKind::Struct, node.arrayLen, node.arrayLen})); - if (node.refId != selectedId) { + if (node.refId != entry.structId) { m_doc->undoStack.push(new RcxCommand(this, - cmd::ChangePointerRef{node.id, node.refId, selectedId})); + cmd::ChangePointerRef{node.id, node.refId, entry.structId})); } } } - } else if (target == EditTarget::PointerTarget) { - // Map void sentinel back to refId 0 - uint64_t realRefId = (selectedId >= kPrimBase) ? 0 : selectedId; + } else if (mode == TypePopupMode::PointerTarget) { + // "void" entry → refId 0; composite entry → real structId + uint64_t realRefId = (entry.entryKind == TypeEntry::Composite) ? entry.structId : 0; if (realRefId != node.refId) { m_doc->undoStack.push(new RcxCommand(this, cmd::ChangePointerRef{node.id, node.refId, realRefId})); diff --git a/src/controller.h b/src/controller.h index a9ccc1d..bd44e1e 100644 --- a/src/controller.h +++ b/src/controller.h @@ -12,6 +12,9 @@ namespace rcx { class RcxController; +class TypeSelectorPopup; +struct TypeEntry; +enum class TypePopupMode; // ── Document ── @@ -133,6 +136,9 @@ private: QVector m_savedSources; int m_activeSourceIdx = -1; + // ── Cached type selector popup (avoids ~350ms cold-start on first show) ── + TypeSelectorPopup* m_cachedPopup = nullptr; + // ── Auto-refresh state ── QTimer* m_refreshTimer = nullptr; QFutureWatcher* m_refreshWatcher = nullptr; @@ -149,9 +155,9 @@ private: void performRealignment(uint64_t structId, int targetAlign); void switchToSavedSource(int idx); void pushSavedSourcesToEditors(); - void showTypeSelectorPopup(RcxEditor* editor); - void showTypePickerPopup(RcxEditor* editor, EditTarget target, int nodeIdx, QPoint globalPos); - void applyTypePickerResult(EditTarget target, int nodeIdx, uint64_t selectedId, const QString& displayName); + void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos); + void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText); + TypeSelectorPopup* ensurePopup(RcxEditor* editor); // ── Auto-refresh methods ── void setupAutoRefresh(); diff --git a/src/editor.cpp b/src/editor.cpp index b401b45..c68ec7d 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -97,10 +97,6 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { if (m_updatingComment) return; // Skip queuing during comment update if (m_editState.target == EditTarget::Value) QTimer::singleShot(0, this, &RcxEditor::validateEditLive); - if (m_editState.target == EditTarget::Type || m_editState.target == EditTarget::ArrayElementType) - QTimer::singleShot(0, this, &RcxEditor::updateTypeListFilter); - if (m_editState.target == EditTarget::PointerTarget) - QTimer::singleShot(0, this, &RcxEditor::updatePointerTargetFilter); }); connect(m_sci, &QsciScintilla::selectionChanged, @@ -1473,18 +1469,20 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { if (target == EditTarget::TypeSelector) return false; // handled by popup, not inline edit - // Array element type and pointer target: handled by TypeSelectorPopup, not inline edit - if (target == EditTarget::ArrayElementType || target == EditTarget::PointerTarget) { + // Type, array element type and pointer target: handled by TypeSelectorPopup, not inline edit + if (target == EditTarget::Type || target == EditTarget::ArrayElementType || target == EditTarget::PointerTarget) { if (line < 0) { int c; m_sci->getCursorPosition(&line, &c); } auto* lm = metaForLine(line); if (!lm) return false; - long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line); + // Position popup at the type column start + ColumnSpan ts = typeSpan(*lm); + long typePos = posFromCol(m_sci, line, ts.valid ? ts.start : 0); int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, (unsigned long)line); - int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION, (unsigned long)0, lineStart); - int y = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION, (unsigned long)0, lineStart); + int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION, (unsigned long)0, typePos); + int y = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION, (unsigned long)0, typePos); QPoint pos = m_sci->viewport()->mapToGlobal(QPoint(x, y + lineH)); emit typePickerRequested(target, lm->nodeIdx, pos); return true; @@ -1657,12 +1655,10 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { if (target == EditTarget::Value) setEditComment(QStringLiteral("Enter=Save Esc=Cancel")); - if (target == EditTarget::Type || target == EditTarget::ArrayElementType) - QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete); + // Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup + // and exit early above (never reach here). if (target == EditTarget::Source) QTimer::singleShot(0, this, &RcxEditor::showSourcePicker); - if (target == EditTarget::PointerTarget) - QTimer::singleShot(0, this, &RcxEditor::showPointerTargetPicker); if (target == EditTarget::RootClassType) { QTimer::singleShot(0, this, [this]() { if (!m_editState.active || m_editState.target != EditTarget::RootClassType) return; diff --git a/src/typeselectorpopup.cpp b/src/typeselectorpopup.cpp index cae013b..b2de817 100644 --- a/src/typeselectorpopup.cpp +++ b/src/typeselectorpopup.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -14,11 +15,44 @@ #include #include #include +#include #include "themes/thememanager.h" namespace rcx { -// ── Custom delegate: gutter checkmark + icon + text ── +// ── parseTypeSpec ── + +TypeSpec parseTypeSpec(const QString& text) { + TypeSpec spec; + QString s = text.trimmed(); + if (s.isEmpty()) return spec; + + // Check for pointer suffix: "Ball*" or "Ball**" + if (s.endsWith('*')) { + spec.isPointer = true; + s.chop(1); + if (s.endsWith('*')) s.chop(1); // double pointer + spec.baseName = s.trimmed(); + return spec; + } + + // Check for array suffix: "int32_t[10]" + int bracket = s.indexOf('['); + if (bracket > 0 && s.endsWith(']')) { + spec.baseName = s.left(bracket).trimmed(); + QString countStr = s.mid(bracket + 1, s.size() - bracket - 2); + bool ok; + int count = countStr.toInt(&ok); + if (ok && count > 0) + spec.arrayCount = count; + return spec; + } + + spec.baseName = s; + return spec; +} + +// ── Custom delegate: gutter checkmark + icon + text + sections ── class TypeSelectorDelegate : public QStyledItemDelegate { public: @@ -26,51 +60,115 @@ public: : QStyledItemDelegate(parent), m_popup(popup) {} void setFont(const QFont& f) { m_font = f; } - void setCurrentTypes(const QVector* filtered, uint64_t currentId) { + void setFilteredTypes(const QVector* filtered, const TypeEntry* current, bool hasCurrent) { m_filtered = filtered; - m_currentId = currentId; + m_current = current; + m_hasCurrent = hasCurrent; } void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override { painter->save(); - // Background: themed colors const auto& t = ThemeManager::instance().current(); - if (option.state & QStyle::State_Selected) - painter->fillRect(option.rect, t.selected); - else if (option.state & QStyle::State_MouseOver) - painter->fillRect(option.rect, t.hover); + int row = index.row(); + bool isSection = (m_filtered && row >= 0 && row < m_filtered->size() + && (*m_filtered)[row].entryKind == TypeEntry::Section); + bool isDisabled = (m_filtered && row >= 0 && row < m_filtered->size() + && !(*m_filtered)[row].enabled); + + // Background + if (isSection) { + // No background highlight for sections + } else if (isDisabled) { + // Subtle background on hover only + if (option.state & QStyle::State_MouseOver) + painter->fillRect(option.rect, t.surface); + } else { + if (option.state & QStyle::State_Selected) + painter->fillRect(option.rect, t.selected); + else if (option.state & QStyle::State_MouseOver) + painter->fillRect(option.rect, t.hover); + } int x = option.rect.x(); int y = option.rect.y(); int h = option.rect.height(); + int w = option.rect.width(); + + // Section: centered dim text with horizontal rules + if (isSection) { + painter->setPen(t.textDim); + QFont dimFont = m_font; + dimFont.setPointSize(qMax(7, m_font.pointSize() - 1)); + painter->setFont(dimFont); + QFontMetrics fm(dimFont); + QString text = index.data().toString(); + int textW = fm.horizontalAdvance(text); + int textX = x + (w - textW) / 2; + int lineY = y + h / 2; + + // Left rule + if (textX > x + 8) + painter->drawLine(x + 8, lineY, textX - 6, lineY); + // Text + painter->drawText(QRect(textX, y, textW, h), Qt::AlignVCenter, text); + // Right rule + if (textX + textW + 6 < x + w - 8) + painter->drawLine(textX + textW + 6, lineY, x + w - 8, lineY); + + painter->restore(); + return; + } // 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(t.syntaxType); - QFont checkFont = m_font; - painter->setFont(checkFont); - painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter, - QString(QChar(0x25B8))); + if (m_hasCurrent && m_filtered && row >= 0 && row < m_filtered->size()) { + const TypeEntry& entry = (*m_filtered)[row]; + bool isCurrent = false; + if (m_current->entryKind == TypeEntry::Primitive && entry.entryKind == TypeEntry::Primitive) + isCurrent = (entry.primitiveKind == m_current->primitiveKind); + else if (m_current->entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite) + isCurrent = (entry.structId == m_current->structId); + if (isCurrent) { + painter->setPen(t.syntaxType); + painter->setFont(m_font); + painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter, + QString(QChar(0x25B8))); + } } x += 18; - // Icon 16x16 — only for struct/class/enum entries (non-empty classKeyword) + // Icon 16x16 — only for composite entries bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size() - && !(*m_filtered)[row].classKeyword.isEmpty()); + && (*m_filtered)[row].entryKind == TypeEntry::Composite); if (hasIcon) { static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg")); - structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16); + QPixmap pm = structIcon.pixmap(16, 16); + if (isDisabled) { + // Paint dimmed + QPixmap dimmed(pm.size()); + dimmed.fill(Qt::transparent); + QPainter p(&dimmed); + p.setOpacity(0.35); + p.drawPixmap(0, 0, pm); + p.end(); + painter->drawPixmap(x, y + (h - 16) / 2, dimmed); + } else { + structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16); + } } - x += 20; // reserve space for alignment + x += 20; // Text - painter->setPen(option.state & QStyle::State_Selected - ? option.palette.color(QPalette::HighlightedText) - : option.palette.color(QPalette::Text)); + QColor textColor; + if (isDisabled) + textColor = t.textDim; + else if (option.state & QStyle::State_Selected) + textColor = option.palette.color(QPalette::HighlightedText); + else + textColor = option.palette.color(QPalette::Text); + + painter->setPen(textColor); painter->setFont(m_font); painter->drawText(QRect(x, y, option.rect.right() - x, h), Qt::AlignVCenter | Qt::AlignLeft, @@ -80,16 +178,21 @@ public: } QSize sizeHint(const QStyleOptionViewItem& /*option*/, - const QModelIndex& /*index*/) const override { + const QModelIndex& index) const override { QFontMetrics fm(m_font); - return QSize(200, fm.height() + 8); + int row = index.row(); + bool isSection = (m_filtered && row >= 0 && row < m_filtered->size() + && (*m_filtered)[row].entryKind == TypeEntry::Section); + int h = isSection ? fm.height() + 2 : fm.height() + 8; + return QSize(200, h); } private: TypeSelectorPopup* m_popup = nullptr; QFont m_font; const QVector* m_filtered = nullptr; - uint64_t m_currentId = 0; + const TypeEntry* m_current = nullptr; + bool m_hasCurrent = false; }; // ── TypeSelectorPopup ── @@ -99,7 +202,6 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) { setAttribute(Qt::WA_DeleteOnClose, false); - // Theme palette const auto& theme = ThemeManager::instance().current(); QPalette pal; pal.setColor(QPalette::Window, theme.backgroundAlt); @@ -114,9 +216,8 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) setPalette(pal); setAutoFillBackground(true); - // Thin border - setFrameShape(QFrame::Box); - setLineWidth(1); + setFrameShape(QFrame::NoFrame); + setLineWidth(0); auto* layout = new QVBoxLayout(this); layout->setContentsMargins(6, 6, 6, 6); @@ -126,7 +227,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) { auto* row = new QHBoxLayout; row->setContentsMargins(0, 0, 0, 0); - m_titleLabel = new QLabel(QStringLiteral("Change root")); + m_titleLabel = new QLabel(QStringLiteral("Change type")); m_titleLabel->setPalette(pal); QFont bold = m_titleLabel->font(); bold.setBold(true); @@ -151,14 +252,17 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) layout->addLayout(row); } - // Row 2: + Create new type button + // Row 2: + Create new type button (flat, no gradient) { m_createBtn = new QToolButton; m_createBtn->setText(QStringLiteral("+ Create new type\u2026")); m_createBtn->setToolButtonStyle(Qt::ToolButtonTextOnly); m_createBtn->setAutoRaise(true); m_createBtn->setCursor(Qt::PointingHandCursor); - m_createBtn->setPalette(pal); + m_createBtn->setStyleSheet(QStringLiteral( + "QToolButton { color: %1; border: none; padding: 3px 6px; }" + "QToolButton:hover { color: %2; background: %3; }") + .arg(theme.textMuted.name(), theme.text.name(), theme.hover.name())); connect(m_createBtn, &QToolButton::clicked, this, [this]() { emit createNewTypeRequested(); hide(); @@ -178,7 +282,65 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) layout->addWidget(sep); } - // Row 3: Filter + // Row 3: Modifier toggles [ plain ] [ * ] [ ** ] [ [n] ] + { + m_modRow = new QWidget; + auto* modLayout = new QHBoxLayout(m_modRow); + modLayout->setContentsMargins(0, 0, 0, 0); + modLayout->setSpacing(3); + + m_modGroup = new QButtonGroup(this); + m_modGroup->setExclusive(true); + + QString btnStyle = QStringLiteral( + "QToolButton { color: %1; background: %2; border: 1px solid %3;" + " padding: 2px 8px; border-radius: 3px; }" + "QToolButton:checked { color: %4; background: %5; border-color: %5; }" + "QToolButton:hover:!checked { background: %6; }") + .arg(theme.textDim.name(), theme.background.name(), theme.border.name(), + theme.text.name(), theme.selected.name(), theme.hover.name()); + + auto makeToggle = [&](const QString& label, int id) -> QToolButton* { + auto* btn = new QToolButton; + btn->setText(label); + btn->setCheckable(true); + btn->setCursor(Qt::PointingHandCursor); + btn->setStyleSheet(btnStyle); + m_modGroup->addButton(btn, id); + modLayout->addWidget(btn); + return btn; + }; + + m_btnPlain = makeToggle(QStringLiteral("plain"), 0); + m_btnPtr = makeToggle(QStringLiteral("*"), 1); + m_btnDblPtr = makeToggle(QStringLiteral("**"), 2); + m_btnArray = makeToggle(QStringLiteral("[n]"), 3); + m_btnPlain->setChecked(true); + + // Array count input (shown only when [n] is active) + m_arrayCountEdit = new QLineEdit; + m_arrayCountEdit->setPlaceholderText(QStringLiteral("n")); + m_arrayCountEdit->setValidator(new QIntValidator(1, 99999, m_arrayCountEdit)); + m_arrayCountEdit->setFixedWidth(50); + m_arrayCountEdit->setPalette(pal); + m_arrayCountEdit->hide(); + modLayout->addWidget(m_arrayCountEdit); + + modLayout->addStretch(); + layout->addWidget(m_modRow); + + connect(m_modGroup, &QButtonGroup::idToggled, + this, [this](int id, bool checked) { + if (!checked) return; + m_arrayCountEdit->setVisible(id == 3); + if (id == 3) m_arrayCountEdit->setFocus(); + updateModifierPreview(); + }); + connect(m_arrayCountEdit, &QLineEdit::textChanged, + this, [this]() { updateModifierPreview(); }); + } + + // Row 4: Filter + preview { m_filterEdit = new QLineEdit; m_filterEdit->setPlaceholderText(QStringLiteral("Filter types\u2026")); @@ -188,6 +350,13 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) connect(m_filterEdit, &QLineEdit::textChanged, this, &TypeSelectorPopup::applyFilter); layout->addWidget(m_filterEdit); + + m_previewLabel = new QLabel; + m_previewLabel->setPalette(pal); + m_previewLabel->setStyleSheet(QStringLiteral( + "QLabel { color: %1; padding: 1px 6px; }").arg(theme.syntaxType.name())); + m_previewLabel->hide(); + layout->addWidget(m_previewLabel); } // Row 4: List @@ -214,6 +383,17 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) } } +void TypeSelectorPopup::warmUp() { + TypeEntry dummy; + dummy.entryKind = TypeEntry::Primitive; + dummy.primitiveKind = NodeKind::Hex8; + dummy.displayName = "warmup"; + setTypes({dummy}); + popup(QPoint(-9999, -9999)); + hide(); + QApplication::processEvents(); +} + void TypeSelectorPopup::setFont(const QFont& font) { m_font = font; @@ -224,35 +404,80 @@ void TypeSelectorPopup::setFont(const QFont& font) { m_createBtn->setFont(font); m_filterEdit->setFont(font); m_listView->setFont(font); + m_previewLabel->setFont(font); + + QFont smallFont = font; + smallFont.setPointSize(qMax(7, font.pointSize() - 1)); + m_btnPlain->setFont(smallFont); + m_btnPtr->setFont(smallFont); + m_btnDblPtr->setFont(smallFont); + m_btnArray->setFont(smallFont); + m_arrayCountEdit->setFont(smallFont); auto* delegate = static_cast(m_listView->itemDelegate()); if (delegate) delegate->setFont(font); } -void TypeSelectorPopup::setTypes(const QVector& types, uint64_t currentId) { +void TypeSelectorPopup::setTitle(const QString& title) { + m_titleLabel->setText(title); +} + +void TypeSelectorPopup::setMode(TypePopupMode mode) { + m_mode = mode; + // Show modifier toggles for modes where type modifiers make sense + bool showMods = (mode == TypePopupMode::FieldType + || mode == TypePopupMode::ArrayElement); + m_modRow->setVisible(showMods); + // Reset to plain when showing + if (showMods) { + m_btnPlain->setChecked(true); + m_arrayCountEdit->clear(); + m_arrayCountEdit->hide(); + } +} + +void TypeSelectorPopup::setCurrentNodeSize(int bytes) { + m_currentNodeSize = bytes; +} + +void TypeSelectorPopup::setTypes(const QVector& types, const TypeEntry* current) { m_allTypes = types; - m_currentId = currentId; + if (current) { + m_currentEntry = *current; + m_hasCurrent = true; + } else { + m_currentEntry = TypeEntry{}; + m_hasCurrent = false; + } + // Reset modifier toggles + m_btnPlain->setChecked(true); + m_arrayCountEdit->clear(); + m_arrayCountEdit->hide(); + m_previewLabel->hide(); + 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("Choose element 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 + QString text = t.classKeyword.isEmpty() + ? t.displayName + : (t.classKeyword + QStringLiteral(" ") + t.displayName); + int w = 18 + 20 + fm.horizontalAdvance(text) + 16; if (w > maxTextW) maxTextW = w; } - int popupW = qBound(250, maxTextW + 24, 500); // +margins + int popupW = qBound(280, maxTextW + 24, 500); 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 headerH = rowH * 3 + 30; + if (m_modRow->isVisible()) + headerH += rowH + 4; // extra row for modifier toggles + int listH = qBound(rowH * 3, rowH * (int)m_filteredTypes.size(), rowH * 14); int popupH = headerH + listH; - // Clamp to screen QScreen* screen = QApplication::screenAt(globalPos); if (screen) { QRect avail = screen->availableGeometry(); @@ -270,40 +495,125 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) { 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; + if (m_hasCurrent) { + for (int i = 0; i < m_filteredTypes.size(); i++) { + const auto& entry = m_filteredTypes[i]; + if (entry.entryKind == TypeEntry::Section) continue; + bool match = false; + if (m_currentEntry.entryKind == TypeEntry::Primitive && entry.entryKind == TypeEntry::Primitive) + match = (entry.primitiveKind == m_currentEntry.primitiveKind); + else if (m_currentEntry.entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite) + match = (entry.structId == m_currentEntry.structId); + if (match) { + m_listView->setCurrentIndex(m_model->index(i)); + break; + } } } } +void TypeSelectorPopup::updateModifierPreview() { + int modId = m_modGroup->checkedId(); + if (modId <= 0) { + m_previewLabel->hide(); + return; + } + QString suffix; + if (modId == 1) suffix = QStringLiteral("*"); + else if (modId == 2) suffix = QStringLiteral("**"); + else if (modId == 3) { + QString countText = m_arrayCountEdit->text().trimmed(); + suffix = countText.isEmpty() + ? QStringLiteral("[n]") + : QStringLiteral("[%1]").arg(countText); + } + m_previewLabel->setText(QStringLiteral("\u2192 %1").arg(suffix)); + m_previewLabel->show(); +} + void TypeSelectorPopup::applyFilter(const QString& text) { m_filteredTypes.clear(); QStringList displayStrings; + QString filterBase = text.trimmed(); + + // Separate primitives and composites + QVector primitives, composites; 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); - if (t.classKeyword.isEmpty()) - displayStrings << t.displayName; + if (t.entryKind == TypeEntry::Section) continue; // skip stale sections + bool matchesFilter = filterBase.isEmpty() + || t.displayName.contains(filterBase, Qt::CaseInsensitive) + || t.classKeyword.contains(filterBase, Qt::CaseInsensitive); + if (!matchesFilter) continue; + + if (t.entryKind == TypeEntry::Primitive) + primitives.append(t); + else if (t.entryKind == TypeEntry::Composite) + composites.append(t); + } + + // For non-Root modes, sort primitives: same-size first, then rest + if (m_mode != TypePopupMode::Root && m_currentNodeSize > 0 && !primitives.isEmpty()) { + QVector sameSize, other; + for (const auto& p : primitives) { + if (sizeForKind(p.primitiveKind) == m_currentNodeSize) + sameSize.append(p); else - displayStrings << (t.classKeyword + QStringLiteral(" ") + t.displayName); + other.append(p); } + primitives = sameSize + other; + } + + // Helper lambdas for appending sections + auto appendPrimitives = [&]() { + if (primitives.isEmpty()) return; + TypeEntry sec; + sec.entryKind = TypeEntry::Section; + sec.displayName = QStringLiteral("primitives"); + sec.enabled = false; + m_filteredTypes.append(sec); + displayStrings << sec.displayName; + for (const auto& p : primitives) { + m_filteredTypes.append(p); + displayStrings << p.displayName; + } + }; + auto appendComposites = [&]() { + if (composites.isEmpty()) return; + TypeEntry sec; + sec.entryKind = TypeEntry::Section; + sec.displayName = QStringLiteral("project types"); + sec.enabled = false; + m_filteredTypes.append(sec); + displayStrings << sec.displayName; + for (const auto& c : composites) { + m_filteredTypes.append(c); + QString label = c.classKeyword.isEmpty() + ? c.displayName + : (c.classKeyword + QStringLiteral(" ") + c.displayName); + displayStrings << label; + } + }; + + // Root mode: project types first (composites are the primary selection) + if (m_mode == TypePopupMode::Root) { + appendComposites(); + appendPrimitives(); + } else { + appendPrimitives(); + appendComposites(); } m_model->setStringList(displayStrings); - // Update delegate data auto* delegate = static_cast(m_listView->itemDelegate()); if (delegate) - delegate->setCurrentTypes(&m_filteredTypes, m_currentId); + delegate->setFilteredTypes(&m_filteredTypes, &m_currentEntry, m_hasCurrent); - // Select first match - if (!m_filteredTypes.isEmpty()) - m_listView->setCurrentIndex(m_model->index(0)); + // Select first selectable item + int first = nextSelectableRow(0, 1); + if (first >= 0) + m_listView->setCurrentIndex(m_model->index(first)); } void TypeSelectorPopup::acceptCurrent() { @@ -312,16 +622,40 @@ void TypeSelectorPopup::acceptCurrent() { acceptIndex(idx.row()); } -void TypeSelectorPopup::setTitle(const QString& title) { - m_titleLabel->setText(title); -} - void TypeSelectorPopup::acceptIndex(int row) { if (row < 0 || row >= m_filteredTypes.size()) return; - emit typeSelected(m_filteredTypes[row].id, m_filteredTypes[row].displayName); + const TypeEntry& entry = m_filteredTypes[row]; + if (entry.entryKind == TypeEntry::Section) return; + if (!entry.enabled) return; + + // Build full text with modifier from toggle buttons + int modId = m_modGroup->checkedId(); + QString fullText = entry.displayName; + if (modId == 1) + fullText += QStringLiteral("*"); + else if (modId == 2) + fullText += QStringLiteral("**"); + else if (modId == 3) { + QString countText = m_arrayCountEdit->text().trimmed(); + if (!countText.isEmpty()) + fullText += QStringLiteral("[%1]").arg(countText); + } + + emit typeSelected(entry, fullText); hide(); } +int TypeSelectorPopup::nextSelectableRow(int from, int direction) const { + int i = from; + while (i >= 0 && i < m_filteredTypes.size()) { + const auto& e = m_filteredTypes[i]; + if (e.entryKind != TypeEntry::Section && e.enabled) + return i; + i += direction; + } + return -1; +} + bool TypeSelectorPopup::eventFilter(QObject* obj, QEvent* event) { if (event->type() == QEvent::KeyPress) { auto* ke = static_cast(event); @@ -334,8 +668,11 @@ bool TypeSelectorPopup::eventFilter(QObject* obj, QEvent* event) { 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)); + QModelIndex cur = m_listView->currentIndex(); + int startRow = cur.isValid() ? cur.row() : 0; + int next = nextSelectableRow(startRow, 1); + if (next >= 0) + m_listView->setCurrentIndex(m_model->index(next)); return true; } if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { @@ -351,11 +688,33 @@ bool TypeSelectorPopup::eventFilter(QObject* obj, QEvent* event) { m_filterEdit->setFocus(); return true; } + // Skip sections and disabled entries + int prev = nextSelectableRow(cur.row() - 1, -1); + if (prev < 0) { + m_filterEdit->setFocus(); + return true; + } + m_listView->setCurrentIndex(m_model->index(prev)); + return true; + } + if (ke->key() == Qt::Key_Down) { + QModelIndex cur = m_listView->currentIndex(); + int startRow = cur.isValid() ? cur.row() + 1 : 0; + int next = nextSelectableRow(startRow, 1); + if (next >= 0) + m_listView->setCurrentIndex(m_model->index(next)); + return true; } if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { acceptCurrent(); return true; } + // Forward printable keys to filter edit for type-to-filter + if (!ke->text().isEmpty() && ke->text()[0].isPrint()) { + m_filterEdit->setFocus(); + m_filterEdit->setText(m_filterEdit->text() + ke->text()); + return true; + } } } diff --git a/src/typeselectorpopup.h b/src/typeselectorpopup.h index 32f6555..1de9e94 100644 --- a/src/typeselectorpopup.h +++ b/src/typeselectorpopup.h @@ -4,21 +4,47 @@ #include #include #include +#include "core.h" class QLineEdit; class QListView; class QStringListModel; class QLabel; class QToolButton; +class QButtonGroup; +class QWidget; namespace rcx { +// ── Popup mode ── + +enum class TypePopupMode { Root, FieldType, ArrayElement, PointerTarget }; + +// ── Type entry (explicit discriminant — no sentinel IDs) ── + struct TypeEntry { - uint64_t id = 0; - QString displayName; - QString classKeyword; // "struct", "class", or "enum" + enum Kind { Primitive, Composite, Section }; + + Kind entryKind = Primitive; + NodeKind primitiveKind = NodeKind::Hex8; // valid when entryKind==Primitive + uint64_t structId = 0; // valid when entryKind==Composite + QString displayName; + QString classKeyword; // "struct", "class", "enum" (Composite only) + bool enabled = true; // false = grayed out (visible but not selectable) }; +// ── Parsed type spec (shared between popup filter and inline edit) ── + +struct TypeSpec { + QString baseName; + bool isPointer = false; + int arrayCount = 0; // 0 = not array +}; + +TypeSpec parseTypeSpec(const QString& text); + +// ── Popup widget ── + class TypeSelectorPopup : public QFrame { Q_OBJECT public: @@ -26,11 +52,16 @@ public: void setFont(const QFont& font); void setTitle(const QString& title); - void setTypes(const QVector& types, uint64_t currentId); + void setMode(TypePopupMode mode); + void setCurrentNodeSize(int bytes); + void setTypes(const QVector& types, const TypeEntry* current = nullptr); void popup(const QPoint& globalPos); + /// Force native window creation to avoid cold-start delay. + void warmUp(); + signals: - void typeSelected(uint64_t id, const QString& displayName); + void typeSelected(const TypeEntry& entry, const QString& fullText); void createNewTypeRequested(); void dismissed(); @@ -43,17 +74,32 @@ private: QToolButton* m_escLabel = nullptr; QToolButton* m_createBtn = nullptr; QLineEdit* m_filterEdit = nullptr; + QLabel* m_previewLabel = nullptr; QListView* m_listView = nullptr; QStringListModel* m_model = nullptr; + // Modifier toggles + QWidget* m_modRow = nullptr; + QToolButton* m_btnPlain = nullptr; + QToolButton* m_btnPtr = nullptr; + QToolButton* m_btnDblPtr = nullptr; + QToolButton* m_btnArray = nullptr; + QLineEdit* m_arrayCountEdit = nullptr; + QButtonGroup* m_modGroup = nullptr; + QVector m_allTypes; QVector m_filteredTypes; - uint64_t m_currentId = 0; + TypeEntry m_currentEntry; + bool m_hasCurrent = false; + TypePopupMode m_mode = TypePopupMode::FieldType; + int m_currentNodeSize = 0; QFont m_font; void applyFilter(const QString& text); + void updateModifierPreview(); void acceptCurrent(); void acceptIndex(int row); + int nextSelectableRow(int from, int direction) const; }; } // namespace rcx diff --git a/tests/test_type_selector.cpp b/tests/test_type_selector.cpp index 2b7aa5e..338e278 100644 --- a/tests/test_type_selector.cpp +++ b/tests/test_type_selector.cpp @@ -2,14 +2,19 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #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"); -#endif + qRegisterMetaType("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 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 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 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 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(); + QVERIFY(listView); + QVERIFY(listView->model()->rowCount() > 2); + } }; QTEST_MAIN(TestTypeSelector)