Files
archived-Reclass/src/typeselectorpopup.cpp

1228 lines
46 KiB
C++

#include "typeselectorpopup.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListView>
#include <QToolButton>
#include <QButtonGroup>
#include <QStringListModel>
#include <QStyledItemDelegate>
#include <QPainter>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QIcon>
#include <QApplication>
#include <QScreen>
#include <QIntValidator>
#include <QElapsedTimer>
#include <QToolTip>
#include <QHelpEvent>
#include "themes/thememanager.h"
namespace rcx {
// ── 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);
spec.ptrDepth = 1;
if (s.endsWith('*')) { s.chop(1); spec.ptrDepth = 2; }
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;
}
// ── Fuzzy scorer: subsequence match with word-boundary bonuses ──
static int fuzzyScore(const QString& pattern, const QString& text,
QVector<int>* outPositions = nullptr) {
int pLen = pattern.size(), tLen = text.size();
if (pLen == 0) return 1;
if (pLen > tLen) return 0;
// Quick subsequence reject
{ int pi = 0;
for (int ti = 0; ti < tLen && pi < pLen; ti++)
if (pattern[pi].toLower() == text[ti].toLower()) pi++;
if (pi < pLen) return 0;
}
// Recursive best-match (bounded: max 4 branches per pattern char)
QVector<int> bestPos;
int best = 0;
auto solve = [&](auto& self, int pi, int ti, QVector<int>& cur, int score) -> void {
if (pi == pLen) {
if (score > best) { best = score; bestPos = cur; }
return;
}
int maxTi = tLen - (pLen - pi);
int branches = 0;
for (int i = ti; i <= maxTi && branches < 4; i++) {
if (pattern[pi].toLower() != text[i].toLower()) continue;
int bonus = 1;
if (i == 0) bonus = 10;
else if (text[i - 1] == '_' || text[i - 1] == ' ') bonus = 8;
else if (text[i].isUpper() && text[i - 1].isLower()) bonus = 8;
if (!cur.isEmpty() && i == cur.last() + 1) bonus += 5;
cur.append(i);
self(self, pi + 1, i + 1, cur, score + bonus);
cur.removeLast();
branches++;
}
};
QVector<int> cur;
solve(solve, 0, 0, cur, 0);
if (best > 0) {
best += qMax(0, 20 - (tLen - pLen)); // tightness bonus
if (pLen == tLen) best += 20; // exact match bonus
if (outPositions) *outPositions = bestPos;
}
return best;
}
// ── Custom delegate: gutter checkmark + icon + text + sections ──
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 setLoading(bool v) { m_isLoading = v; }
void setFilteredTypes(const QVector<TypeEntry>* filtered) {
m_filtered = filtered;
}
void setMatchPositions(const QVector<QVector<int>>* mp) {
m_matchPositions = mp;
}
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const override {
painter->save();
const auto& t = ThemeManager::instance().current();
int row = index.row();
// Skeleton placeholder bars while loading
if (m_isLoading) {
QFontMetrics fm(m_font);
int barH = fm.height() - 2;
int x0 = option.rect.x() + fm.height() + 8;
int y0 = option.rect.y() + (option.rect.height() - barH) / 2;
int barW = 50 + ((row * 73 + 29) % 110);
painter->setPen(Qt::NoPen);
painter->setBrush(t.surface);
painter->drawRoundedRect(x0, y0, barW, barH, 3, 3);
// Size column placeholder
painter->drawRoundedRect(option.rect.right() - 46, y0, 30, barH, 3, 3);
painter->restore();
return;
}
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();
// Scale metrics from font height
QFontMetrics fmMain(m_font);
int iconSz = fmMain.height(); // icon matches text height
int iconColW = iconSz + 4;
// 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;
}
// Icon (scaled to font height)
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
&& (*m_filtered)[row].entryKind != TypeEntry::Section);
if (hasIcon) {
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-class.svg"));
static QIcon enumIcon(QStringLiteral(":/vsicons/symbol-enum.svg"));
static QIcon primIcon(QStringLiteral(":/vsicons/symbol-variable.svg"));
const auto& entry = (*m_filtered)[row];
QIcon& icon = (entry.entryKind == TypeEntry::Composite)
? (entry.category == TypeEntry::CatEnum ? enumIcon : structIcon)
: primIcon;
QPixmap pm = icon.pixmap(iconSz, iconSz);
if (isDisabled) {
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 - iconSz) / 2, dimmed);
} else {
icon.paint(painter, x, y + (h - iconSz) / 2, iconSz, iconSz);
}
}
x += iconColW;
// Text: type name in normal color, size suffix dimmed
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->setFont(m_font);
QString fullText = index.data().toString();
int dashIdx = fullText.lastIndexOf(QStringLiteral(" - "));
int rightPad = 6;
// Fuzzy-match highlight flags for the name portion
QVector<bool> hlFlags;
if (m_matchPositions && row >= 0 && row < m_matchPositions->size()
&& !(*m_matchPositions)[row].isEmpty()) {
int nameLen = dashIdx > 0 ? dashIdx : fullText.size();
hlFlags.resize(nameLen, false);
for (int p : (*m_matchPositions)[row])
if (p < nameLen) hlFlags[p] = true;
}
// Lambda: draw text with optional highlight runs
int sizeColW = 55;
auto drawHL = [&](const QString& text, int x0, int maxW) {
if (hlFlags.isEmpty()) {
painter->setPen(textColor);
painter->drawText(QRect(x0, y, maxW, h),
Qt::AlignVCenter | Qt::AlignLeft, text);
return;
}
QFontMetrics fm(m_font);
int xp = x0;
int i = 0;
while (i < text.size()) {
bool hl = i < hlFlags.size() && hlFlags[i];
int s = i;
while (i < text.size() && (i < hlFlags.size() && hlFlags[i]) == hl) i++;
QString seg = text.mid(s, i - s);
int segW = fm.horizontalAdvance(seg);
painter->setPen(hl ? t.indHoverSpan : textColor);
painter->drawText(QRect(xp, y, segW, h), Qt::AlignVCenter, seg);
xp += segW;
}
};
if (dashIdx > 0) {
int nameW = option.rect.right() - x - sizeColW - rightPad;
drawHL(fullText.left(dashIdx), x, nameW);
painter->setPen(t.textDim);
painter->drawText(QRect(option.rect.right() - sizeColW - rightPad, y, sizeColW, h),
Qt::AlignVCenter | Qt::AlignRight, fullText.mid(dashIdx + 3));
} else {
drawHL(fullText, x, option.rect.right() - x - rightPad);
}
painter->restore();
}
QSize sizeHint(const QStyleOptionViewItem& /*option*/,
const QModelIndex& index) const override {
QFontMetrics fm(m_font);
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);
}
bool helpEvent(QHelpEvent* event, QAbstractItemView* view,
const QStyleOptionViewItem& option,
const QModelIndex& index) override {
if (event->type() == QEvent::ToolTip && m_filtered) {
int row = index.row();
if (row >= 0 && row < m_filtered->size()) {
const auto& e = (*m_filtered)[row];
if (e.entryKind == TypeEntry::Composite && !e.fieldSummary.isEmpty()) {
QString tip = QStringLiteral("%1 (%2 B, %3 fields)\n")
.arg(e.displayName).arg(e.sizeBytes).arg(e.fieldCount);
tip += e.fieldSummary.join(QChar('\n'));
if (e.fieldCount > e.fieldSummary.size())
tip += QStringLiteral("\n...");
QToolTip::showText(event->globalPos(), tip, view);
return true;
}
}
QToolTip::hideText();
return true;
}
return QStyledItemDelegate::helpEvent(event, view, option, index);
}
private:
TypeSelectorPopup* m_popup = nullptr;
QFont m_font;
bool m_isLoading = false;
const QVector<TypeEntry>* m_filtered = nullptr;
const QVector<QVector<int>>* m_matchPositions = nullptr;
};
// ── TypeSelectorPopup ──
TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
: QFrame(parent, Qt::Popup | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_DeleteOnClose, false);
const auto& theme = ThemeManager::instance().current();
QPalette pal;
pal.setColor(QPalette::Window, theme.backgroundAlt);
pal.setColor(QPalette::WindowText, theme.text);
pal.setColor(QPalette::Base, theme.background);
pal.setColor(QPalette::AlternateBase, theme.surface);
pal.setColor(QPalette::Text, theme.text);
pal.setColor(QPalette::Button, theme.button);
pal.setColor(QPalette::ButtonText, theme.text);
pal.setColor(QPalette::Highlight, theme.hover);
pal.setColor(QPalette::HighlightedText, theme.text);
setPalette(pal);
setAutoFillBackground(true);
setFrameShape(QFrame::NoFrame);
setLineWidth(0);
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(8, 6, 8, 6);
layout->setSpacing(4);
// ── Top: Filter + close ──
{
auto* row = new QHBoxLayout;
row->setContentsMargins(0, 0, 0, 0);
m_filterEdit = new QLineEdit;
m_filterEdit->setPlaceholderText(QStringLiteral("Filter types.. (Ctrl+F)"));
m_filterEdit->setClearButtonEnabled(true);
m_filterEdit->setPalette(pal);
m_filterEdit->setStyleSheet(QStringLiteral(
"QLineEdit { border: 1px solid %1; padding: 2px 4px; border-radius: 3px; }")
.arg(theme.border.name()));
m_filterEdit->setAccessibleName(QStringLiteral("Filter types"));
m_filterEdit->installEventFilter(this);
connect(m_filterEdit, &QLineEdit::textChanged,
this, &TypeSelectorPopup::applyFilter);
row->addWidget(m_filterEdit);
m_escLabel = new QToolButton;
m_escLabel->setText(QStringLiteral("\u2715"));
m_escLabel->setAutoRaise(true);
m_escLabel->setCursor(Qt::PointingHandCursor);
m_escLabel->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 2px 4px; }"
"QToolButton:hover { color: %2; }")
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
connect(m_escLabel, &QToolButton::clicked, this, [this]() { hide(); });
row->addWidget(m_escLabel);
layout->addLayout(row);
}
// ── Category chips ──
{
m_chipRow = new QWidget;
auto* chipLayout = new QHBoxLayout(m_chipRow);
chipLayout->setContentsMargins(0, 0, 0, 2);
chipLayout->setSpacing(6);
QString chipStyle = QStringLiteral(
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
" padding: 2px 8px; border-radius: 10px; }"
"QToolButton:checked { color: %4; background: %5; border-color: %5; }"
"QToolButton:hover:!checked { background: %6; }"
"QToolButton:pressed { background: %7; }")
.arg(theme.textDim.name(), theme.background.name(), theme.border.name(),
theme.text.name(), theme.selected.name(), theme.hover.name(),
theme.surface.name());
auto makeChip = [&](const QString& label) -> QToolButton* {
auto* btn = new QToolButton;
btn->setText(label);
btn->setCheckable(true);
btn->setChecked(true);
btn->setCursor(Qt::PointingHandCursor);
btn->setStyleSheet(chipStyle);
chipLayout->addWidget(btn);
return btn;
};
m_chipPrim = makeChip(QStringLiteral("Built-in"));
m_chipTypes = makeChip(QStringLiteral("Types"));
m_chipEnums = makeChip(QStringLiteral("Enum"));
m_chipPrim->setAccessibleName(QStringLiteral("Show primitives"));
m_chipTypes->setAccessibleName(QStringLiteral("Show composites"));
m_chipEnums->setAccessibleName(QStringLiteral("Show enums"));
chipLayout->addStretch();
m_statusLabel = new QLabel;
m_statusLabel->setStyleSheet(QStringLiteral(
"QLabel { color: %1; padding: 1px 4px; }").arg(theme.textDim.name()));
chipLayout->addWidget(m_statusLabel);
layout->addWidget(m_chipRow);
auto refilter = [this]() { applyFilter(m_filterEdit->text()); };
connect(m_chipPrim, &QToolButton::toggled, this, refilter);
connect(m_chipTypes, &QToolButton::toggled, this, refilter);
connect(m_chipEnums, &QToolButton::toggled, this, refilter);
}
// ── List view (main content) ──
{
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->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
m_listView->setAccessibleName(QStringLiteral("Type list"));
m_listView->installEventFilter(this);
auto* delegate = new TypeSelectorDelegate(this, m_listView);
m_listView->setItemDelegate(delegate);
layout->addWidget(m_listView, 1);
connect(m_listView, &QListView::doubleClicked,
this, [this](const QModelIndex& index) {
acceptIndex(index.row());
});
connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged,
this, [this]() { updateModifierPreview(); });
}
// ── Action row: "Apply as: ..." + modifiers + "+ New" ──
{
auto* row = new QHBoxLayout;
row->setContentsMargins(0, 0, 0, 0);
row->setSpacing(6);
m_titleLabel = new QLabel;
m_titleLabel->setPalette(pal);
m_titleLabel->setAlignment(Qt::AlignVCenter);
m_titleLabel->setTextFormat(Qt::RichText);
QFont bold = m_titleLabel->font();
bold.setBold(true);
m_titleLabel->setFont(bold);
row->addWidget(m_titleLabel);
row->addStretch();
// Modifier toggles: [*] [**] [[] count]
{
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(false);
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; }"
"QToolButton:pressed { background: %7; }")
.arg(theme.textDim.name(), theme.background.name(), theme.border.name(),
theme.text.name(), theme.selected.name(), theme.hover.name(),
theme.surface.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_btnPtr = makeToggle(QStringLiteral("*"), 1);
m_btnDblPtr = makeToggle(QStringLiteral("**"), 2);
m_btnArray = makeToggle(QStringLiteral("[]"), 3);
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);
row->addWidget(m_modRow);
connect(m_modGroup, &QButtonGroup::idClicked,
this, [this](int id) {
QAbstractButton* btn = m_modGroup->button(id);
if (btn->isChecked()) {
for (auto* b : m_modGroup->buttons())
if (b != btn) b->setChecked(false);
}
// If unchecked (toggled off), all buttons stay unchecked = plain
m_arrayCountEdit->setVisible(m_btnArray->isChecked());
if (m_btnArray->isChecked()) {
if (m_arrayCountEdit->text().trimmed().isEmpty())
m_arrayCountEdit->setText(QStringLiteral("1"));
m_arrayCountEdit->setFocus();
m_arrayCountEdit->selectAll();
}
updateModifierPreview();
});
connect(m_arrayCountEdit, &QLineEdit::textChanged,
this, [this]() { updateModifierPreview(); });
}
m_createBtn = new QToolButton;
m_createBtn->setText(QStringLiteral("+ New"));
m_createBtn->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_createBtn->setAutoRaise(true);
m_createBtn->setCursor(Qt::PointingHandCursor);
m_createBtn->setAccessibleName(QStringLiteral("Create new type"));
m_createBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
" padding: 2px 10px; border-radius: 3px; }"
"QToolButton:hover { color: %4; background: %5; border-color: %5; }"
"QToolButton:pressed { background: %6; }")
.arg(theme.text.name(), theme.background.name(), theme.border.name(),
theme.text.name(), theme.selected.name(), theme.surface.name()));
connect(m_createBtn, &QToolButton::clicked, this, [this]() {
emit createNewTypeRequested();
hide();
});
row->addWidget(m_createBtn);
m_saveBtn = new QToolButton;
m_saveBtn->setText(QStringLiteral("Save"));
m_saveBtn->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_saveBtn->setAutoRaise(true);
m_saveBtn->setCursor(Qt::PointingHandCursor);
m_saveBtn->setAccessibleName(QStringLiteral("Save"));
m_saveBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
" padding: 2px 10px; border-radius: 3px; }"
"QToolButton:hover { color: %4; background: %5; border-color: %5; }"
"QToolButton:pressed { background: %6; }")
.arg(theme.text.name(), theme.background.name(), theme.border.name(),
theme.text.name(), theme.selected.name(), theme.surface.name()));
connect(m_saveBtn, &QToolButton::clicked, this, [this]() {
acceptCurrent();
});
row->addWidget(m_saveBtn);
layout->addLayout(row);
}
}
void TypeSelectorPopup::warmUp() {
// One-time per-process cost (~170ms): Qt lazily initializes the style/font/DLL
// subsystem the first time a popup with complex children is shown. Pre-pay it
// by briefly showing a throwaway dummy popup with a QListView, then show+hide
// ourselves.
{
auto* primer = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint);
primer->resize(300, 400);
auto* lay = new QVBoxLayout(primer);
lay->addWidget(new QLabel(QStringLiteral("x")));
lay->addWidget(new QLineEdit);
auto* model = new QStringListModel(primer);
QStringList items; for (int i = 0; i < 10; i++) items << QStringLiteral("x");
model->setStringList(items);
auto* lv = new QListView;
lv->setModel(model);
lay->addWidget(lv);
primer->show();
QApplication::processEvents();
primer->hide();
QApplication::processEvents();
delete primer;
}
TypeEntry dummy;
dummy.entryKind = TypeEntry::Primitive;
dummy.primitiveKind = NodeKind::Hex8;
dummy.displayName = QStringLiteral("warmup");
setTypes({dummy});
popup(QPoint(-9999, -9999));
hide();
QApplication::processEvents();
}
void TypeSelectorPopup::popupLoading(const QPoint& globalPos) {
m_loading = true;
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
if (delegate) delegate->setLoading(true);
// Clear stale selection from previous use
m_listView->selectionModel()->clearSelection();
m_listView->selectionModel()->clearCurrentIndex();
// Fill model with dummy rows for skeleton bars
QStringList dummy;
for (int i = 0; i < 12; i++) dummy << QString();
m_model->setStringList(dummy);
// Reset UI to empty state
m_titleLabel->clear();
if (m_statusLabel) m_statusLabel->clear();
// Default popup size (compact — 66% of old width)
QFontMetrics fm(m_font);
int rowH = fm.height() + 8;
int popupW = 560;
int popupH = qMax(400, rowH * 14 + rowH * 2 + 20);
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();
}
void TypeSelectorPopup::setFont(const QFont& font) {
m_font = font;
m_titleLabel->setFont([&]() {
QFont f = font;
f.setPointSize(qMax(7, font.pointSize() * 3 / 4));
f.setBold(true);
return f;
}());
m_escLabel->setFont(font);
m_filterEdit->setFont(font);
m_listView->setFont(font);
QFont smallFont = font;
smallFont.setPointSize(qMax(7, font.pointSize() - 1));
m_btnPtr->setFont(smallFont);
m_btnDblPtr->setFont(smallFont);
m_btnArray->setFont(smallFont);
m_arrayCountEdit->setFont(smallFont);
m_createBtn->setFont(smallFont);
m_saveBtn->setFont(smallFont);
QFont chipFont = font;
chipFont.setPointSize(qMax(7, (int)(font.pointSize() * 0.75)));
m_chipPrim->setFont(chipFont);
m_chipTypes->setFont(chipFont);
m_chipEnums->setFont(chipFont);
if (m_statusLabel) m_statusLabel->setFont(chipFont);
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
if (delegate)
delegate->setFont(font);
}
void TypeSelectorPopup::applyTheme(const Theme& theme) {
QPalette pal;
pal.setColor(QPalette::Window, theme.backgroundAlt);
pal.setColor(QPalette::WindowText, theme.text);
pal.setColor(QPalette::Base, theme.background);
pal.setColor(QPalette::AlternateBase, theme.surface);
pal.setColor(QPalette::Text, theme.text);
pal.setColor(QPalette::Button, theme.button);
pal.setColor(QPalette::ButtonText, theme.text);
pal.setColor(QPalette::Highlight, theme.hover);
pal.setColor(QPalette::HighlightedText, theme.text);
setPalette(pal);
m_titleLabel->setPalette(pal);
m_filterEdit->setPalette(pal);
m_listView->setPalette(pal);
m_arrayCountEdit->setPalette(pal);
// Esc button (snapped to corner)
m_escLabel->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 2px 4px; }"
"QToolButton:hover { color: %2; }")
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
// Create button
m_createBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
" padding: 2px 10px; border-radius: 3px; }"
"QToolButton:hover { color: %4; background: %5; border-color: %5; }"
"QToolButton:pressed { background: %6; }")
.arg(theme.text.name(), theme.background.name(), theme.border.name(),
theme.text.name(), theme.selected.name(), theme.surface.name()));
// Save button
m_saveBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
" padding: 2px 10px; border-radius: 3px; }"
"QToolButton:hover { color: %4; background: %5; border-color: %5; }"
"QToolButton:pressed { background: %6; }")
.arg(theme.text.name(), theme.background.name(), theme.border.name(),
theme.text.name(), theme.selected.name(), theme.surface.name()));
// Filter (no focus accent)
m_filterEdit->setStyleSheet(QStringLiteral(
"QLineEdit { border: 1px solid %1; padding: 2px 4px; border-radius: 3px; }")
.arg(theme.border.name()));
// Modifier toggle buttons
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; }"
"QToolButton:pressed { background: %7; }")
.arg(theme.textDim.name(), theme.background.name(), theme.border.name(),
theme.text.name(), theme.selected.name(), theme.hover.name(),
theme.surface.name());
m_btnPtr->setStyleSheet(btnStyle);
m_btnDblPtr->setStyleSheet(btnStyle);
m_btnArray->setStyleSheet(btnStyle);
// Category chip buttons
{
QString chipStyle = QStringLiteral(
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
" padding: 2px 8px; border-radius: 10px; }"
"QToolButton:checked { color: %4; background: %5; border-color: %5; }"
"QToolButton:hover:!checked { background: %6; }"
"QToolButton:pressed { background: %7; }")
.arg(theme.textDim.name(), theme.background.name(), theme.border.name(),
theme.text.name(), theme.selected.name(), theme.hover.name(),
theme.surface.name());
m_chipPrim->setStyleSheet(chipStyle);
m_chipTypes->setStyleSheet(chipStyle);
m_chipEnums->setStyleSheet(chipStyle);
}
// Status label
if (m_statusLabel) {
m_statusLabel->setStyleSheet(QStringLiteral(
"QLabel { color: %1; padding: 1px 2px; }").arg(theme.textDim.name()));
}
}
void TypeSelectorPopup::setTitle(const QString& /*title*/) {
// Title is now dynamic — set by updateModifierPreview()
}
void TypeSelectorPopup::setMode(TypePopupMode mode) {
m_mode = mode;
bool showMods = (mode == TypePopupMode::FieldType
|| mode == TypePopupMode::ArrayElement);
m_modRow->setVisible(showMods);
// Reset all modifiers — no modifier = plain
for (auto* b : m_modGroup->buttons())
b->setChecked(false);
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
}
void TypeSelectorPopup::setCurrentNodeSize(int bytes) {
m_currentNodeSize = bytes;
}
void TypeSelectorPopup::setPointerSize(int bytes) {
m_pointerSize = bytes;
}
void TypeSelectorPopup::setModifier(int modId, int arrayCount) {
for (auto* b : m_modGroup->buttons())
b->setChecked(false);
if (modId == 1) m_btnPtr->setChecked(true);
else if (modId == 2) m_btnDblPtr->setChecked(true);
else if (modId == 3) {
m_btnArray->setChecked(true);
m_arrayCountEdit->setText(QString::number(arrayCount));
m_arrayCountEdit->show();
}
// else: all unchecked = plain (no modifier)
}
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntry* current) {
m_loading = false;
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
if (delegate) delegate->setLoading(false);
m_allTypes = types;
if (current) {
m_currentEntry = *current;
m_hasCurrent = true;
} else {
m_currentEntry = TypeEntry{};
m_hasCurrent = false;
}
// Don't reset modifier buttons here — setMode() already resets to plain,
// and setModifier() may have preselected a button between setMode/setTypes.
m_filterEdit->clear();
applyFilter(QString());
// Pre-select current type in list
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::popup(const QPoint& globalPos) {
QFontMetrics fm(m_font);
int maxTextW = fm.horizontalAdvance(QStringLiteral("Choose element type "));
for (const auto& t : m_allTypes) {
int iconColW = fm.height() + 4;
int w = iconColW + fm.horizontalAdvance(t.displayName) + 16;
if (w > maxTextW) maxTextW = w;
}
int popupW = qBound(480, maxTextW + 24, 560);
int rowH = fm.height() + 8;
int headerH = rowH * 2 + 10; // filter + chips + separator
int footerH = rowH + 6; // separator + action row
int listH = qBound(rowH * 3, rowH * (int)m_filteredTypes.size(), rowH * 14);
int popupH = qMax(400, headerH + listH + footerH);
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();
}
void TypeSelectorPopup::updateModifierPreview() {
const auto& t = ThemeManager::instance().current();
QModelIndex idx = m_listView->currentIndex();
int row = idx.isValid() ? idx.row() : -1;
if (row < 0 || row >= m_filteredTypes.size()
|| m_filteredTypes[row].entryKind == TypeEntry::Section) {
m_titleLabel->setText(QStringLiteral("<span style='color:%1'>Select a type</span>")
.arg(t.textDim.name()));
return;
}
const TypeEntry& entry = m_filteredTypes[row];
// Disabled entry
if (!entry.enabled) {
m_titleLabel->setText(QStringLiteral("<span style='color:%1'>Not selectable</span>")
.arg(t.textDim.name()));
return;
}
int modId = m_modGroup->checkedId();
// Build modifier suffix
QString suffix;
if (modId == 1) suffix = QStringLiteral("*");
else if (modId == 2) suffix = QStringLiteral("**");
else if (modId == 3) {
QString c = m_arrayCountEdit->text().trimmed();
suffix = c.isEmpty() ? QStringLiteral("[n]") : QStringLiteral("[%1]").arg(c);
}
// Compute resulting size
int newSize = entry.sizeBytes;
if (modId == 1 || modId == 2)
newSize = m_pointerSize;
else if (modId == 3 && newSize > 0) {
QString c = m_arrayCountEdit->text().trimmed();
bool ok; int count = c.toInt(&ok);
if (ok && count > 0) newSize *= count;
}
// Format: "type+modifier → size (+diff)"
QString label = QStringLiteral("<span style='color:%1'>%2%3</span>")
.arg(t.text.name(), entry.displayName, suffix);
if (newSize > 0) {
label += QStringLiteral("<span style='color:%1'> \u2192 %2</span>")
.arg(t.textDim.name()).arg(newSize);
if (m_currentNodeSize > 0 && newSize != m_currentNodeSize) {
int diff = newSize - m_currentNodeSize;
label += QStringLiteral("<span style='color:%1'> (%2%3)</span>")
.arg(t.textDim.name(),
diff > 0 ? QStringLiteral("+") : QString(),
QString::number(diff));
}
}
m_titleLabel->setText(label);
}
void TypeSelectorPopup::applyFilter(const QString& text) {
m_filteredTypes.clear();
m_matchPositions.clear();
QStringList displayStrings;
QString filterBase = text.trimmed();
bool showPrim = m_chipPrim && m_chipPrim->isChecked();
bool showTypes = m_chipTypes && m_chipTypes->isChecked();
bool showEnums = m_chipEnums && m_chipEnums->isChecked();
auto catAllowed = [&](const TypeEntry& t) {
if (t.entryKind == TypeEntry::Primitive) return showPrim;
return (t.category == TypeEntry::CatEnum) ? showEnums : showTypes;
};
auto makeLabel = [](const TypeEntry& e) {
QString label = e.displayName;
if (e.sizeBytes > 0) label += QStringLiteral(" - %1").arg(e.sizeBytes);
return label;
};
int primCount = 0, typeCount = 0, enumCount = 0;
if (!filterBase.isEmpty()) {
// ── Fuzzy search: flat ranked list, no section headers ──
struct Scored { TypeEntry entry; int score; QVector<int> pos; };
QVector<Scored> scored;
for (const auto& t : m_allTypes) {
if (t.entryKind == TypeEntry::Section) continue;
QVector<int> pos;
int sc = fuzzyScore(filterBase, t.displayName, &pos);
if (sc <= 0) continue;
if (t.entryKind == TypeEntry::Primitive) primCount++;
else if (t.category == TypeEntry::CatEnum) enumCount++;
else typeCount++;
if (catAllowed(t))
scored.append({t, sc, pos});
}
std::sort(scored.begin(), scored.end(),
[](const Scored& a, const Scored& b) { return a.score > b.score; });
for (const auto& s : scored) {
m_filteredTypes.append(s.entry);
m_matchPositions.append(s.pos);
displayStrings << makeLabel(s.entry);
}
} else {
// ── No filter: grouped sections, alphabetical ──
QVector<TypeEntry> primitives, types, enums;
for (const auto& t : m_allTypes) {
if (t.entryKind == TypeEntry::Section) continue;
if (t.entryKind == TypeEntry::Primitive) {
primCount++;
if (showPrim) primitives.append(t);
} else if (t.category == TypeEntry::CatEnum) {
enumCount++;
if (showEnums) enums.append(t);
} else {
typeCount++;
if (showTypes) types.append(t);
}
}
auto alphabetical = [](const TypeEntry& a, const TypeEntry& b) {
return a.displayName.compare(b.displayName, Qt::CaseInsensitive) < 0;
};
if (m_mode != TypePopupMode::Root && m_currentNodeSize > 0 && !primitives.isEmpty()) {
QVector<TypeEntry> sameSize, other;
for (const auto& p : primitives) {
if (sizeForKind(p.primitiveKind) == m_currentNodeSize) sameSize.append(p);
else other.append(p);
}
std::sort(sameSize.begin(), sameSize.end(), alphabetical);
std::sort(other.begin(), other.end(), alphabetical);
primitives = sameSize + other;
} else {
std::sort(primitives.begin(), primitives.end(), alphabetical);
}
std::sort(types.begin(), types.end(), alphabetical);
std::sort(enums.begin(), enums.end(), alphabetical);
auto appendSection = [&](const QString& title, const QVector<TypeEntry>& items) {
if (items.isEmpty()) return;
TypeEntry sec;
sec.entryKind = TypeEntry::Section;
sec.displayName = title;
sec.enabled = false;
m_filteredTypes.append(sec);
m_matchPositions.append(QVector<int>());
displayStrings << sec.displayName;
for (const auto& c : items) {
m_filteredTypes.append(c);
m_matchPositions.append(QVector<int>());
displayStrings << makeLabel(c);
}
};
if (m_mode == TypePopupMode::Root) {
appendSection(QStringLiteral("types"), types);
appendSection(QStringLiteral("enums"), enums);
appendSection(QStringLiteral("primitives"), primitives);
} else {
appendSection(QStringLiteral("primitives"), primitives);
appendSection(QStringLiteral("types"), types);
appendSection(QStringLiteral("enums"), enums);
}
}
// Empty state
int resultCount = 0;
for (const auto& f : m_filteredTypes)
if (f.entryKind != TypeEntry::Section) resultCount++;
if (resultCount == 0) {
TypeEntry empty;
empty.entryKind = TypeEntry::Section;
empty.displayName = QStringLiteral("No matching types");
empty.enabled = false;
m_filteredTypes.append(empty);
m_matchPositions.append(QVector<int>());
displayStrings << empty.displayName;
}
m_model->setStringList(displayStrings);
auto updateChipLabel = [](QToolButton* btn, const QString& abbrev, int count) {
btn->setText(QStringLiteral("%1 (%2)").arg(abbrev).arg(count));
};
if (m_chipPrim) updateChipLabel(m_chipPrim, QStringLiteral("Built-in"), primCount);
if (m_chipTypes) updateChipLabel(m_chipTypes, QStringLiteral("Types"), typeCount);
if (m_chipEnums) updateChipLabel(m_chipEnums, QStringLiteral("Enum"), enumCount);
if (m_statusLabel)
m_statusLabel->setText(QStringLiteral("%1 results").arg(resultCount));
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
if (delegate) {
delegate->setFilteredTypes(&m_filteredTypes);
delegate->setMatchPositions(&m_matchPositions);
}
int first = nextSelectableRow(0, 1);
if (first >= 0)
m_listView->setCurrentIndex(m_model->index(first));
}
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;
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<QKeyEvent*>(event);
if (ke->key() == Qt::Key_Escape) {
hide();
return true;
}
// Ctrl+F focuses the filter from anywhere
if (ke->key() == Qt::Key_F && (ke->modifiers() & Qt::ControlModifier)) {
m_filterEdit->setFocus();
m_filterEdit->selectAll();
return true;
}
if (obj == m_filterEdit) {
if (ke->key() == Qt::Key_Down) {
m_listView->setFocus();
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) {
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;
}
// 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;
}
// Backspace in list removes last filter char
if (ke->key() == Qt::Key_Backspace) {
QString txt = m_filterEdit->text();
if (!txt.isEmpty()) {
m_filterEdit->setText(txt.left(txt.size() - 1));
m_filterEdit->setFocus();
}
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;
}
}
}
return QFrame::eventFilter(obj, event);
}
void TypeSelectorPopup::hideEvent(QHideEvent* event) {
QFrame::hideEvent(event);
emit dismissed();
}
} // namespace rcx