mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add type selector popup, view root switching, and new type creation
- Type selector chevron [▸] on command row opens searchable popup - Popup lists all root structs with filter, keyboard nav, side-triangle indicator - Selecting a type switches the editor view via setViewRootId - "Create new type" inserts a new root struct with no name - Command row displays the active view root's name - Tests for chevron detection, span compatibility, view switching, undo
This commit is contained in:
350
src/typeselectorpopup.cpp
Normal file
350
src/typeselectorpopup.cpp
Normal file
@@ -0,0 +1,350 @@
|
||||
#include "typeselectorpopup.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QToolButton>
|
||||
#include <QStringListModel>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QPainter>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QIcon>
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Custom delegate: gutter checkmark + icon + text ──
|
||||
|
||||
class TypeSelectorDelegate : public QStyledItemDelegate {
|
||||
public:
|
||||
explicit TypeSelectorDelegate(TypeSelectorPopup* popup, QObject* parent = nullptr)
|
||||
: QStyledItemDelegate(parent), m_popup(popup) {}
|
||||
|
||||
void setFont(const QFont& f) { m_font = f; }
|
||||
void setCurrentTypes(const QVector<TypeEntry>* filtered, uint64_t currentId) {
|
||||
m_filtered = filtered;
|
||||
m_currentId = currentId;
|
||||
}
|
||||
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const override {
|
||||
painter->save();
|
||||
|
||||
// Background
|
||||
if (option.state & QStyle::State_Selected)
|
||||
painter->fillRect(option.rect, option.palette.highlight());
|
||||
else if (option.state & QStyle::State_MouseOver)
|
||||
painter->fillRect(option.rect, QColor(43, 43, 43));
|
||||
|
||||
int x = option.rect.x();
|
||||
int y = option.rect.y();
|
||||
int h = option.rect.height();
|
||||
|
||||
// 18px gutter: side triangle if current
|
||||
int row = index.row();
|
||||
if (m_filtered && row >= 0 && row < m_filtered->size()
|
||||
&& (*m_filtered)[row].id == m_currentId) {
|
||||
painter->setPen(QColor("#4ec9b0"));
|
||||
QFont checkFont = m_font;
|
||||
painter->setFont(checkFont);
|
||||
painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter,
|
||||
QString(QChar(0x25B8)));
|
||||
}
|
||||
x += 18;
|
||||
|
||||
// Icon 16x16
|
||||
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg"));
|
||||
structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16);
|
||||
x += 20;
|
||||
|
||||
// Text
|
||||
painter->setPen(option.state & QStyle::State_Selected
|
||||
? option.palette.color(QPalette::HighlightedText)
|
||||
: option.palette.color(QPalette::Text));
|
||||
painter->setFont(m_font);
|
||||
painter->drawText(QRect(x, y, option.rect.right() - x, h),
|
||||
Qt::AlignVCenter | Qt::AlignLeft,
|
||||
index.data().toString());
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem& /*option*/,
|
||||
const QModelIndex& /*index*/) const override {
|
||||
QFontMetrics fm(m_font);
|
||||
return QSize(200, fm.height() + 8);
|
||||
}
|
||||
|
||||
private:
|
||||
TypeSelectorPopup* m_popup = nullptr;
|
||||
QFont m_font;
|
||||
const QVector<TypeEntry>* m_filtered = nullptr;
|
||||
uint64_t m_currentId = 0;
|
||||
};
|
||||
|
||||
// ── TypeSelectorPopup ──
|
||||
|
||||
TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
: QFrame(parent, Qt::Popup | Qt::FramelessWindowHint)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
|
||||
// Dark palette (no CSS)
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, QColor("#252526"));
|
||||
pal.setColor(QPalette::WindowText, QColor("#d4d4d4"));
|
||||
pal.setColor(QPalette::Base, QColor("#1e1e1e"));
|
||||
pal.setColor(QPalette::AlternateBase, QColor("#2a2d2e"));
|
||||
pal.setColor(QPalette::Text, QColor("#d4d4d4"));
|
||||
pal.setColor(QPalette::Button, QColor("#333333"));
|
||||
pal.setColor(QPalette::ButtonText, QColor("#d4d4d4"));
|
||||
pal.setColor(QPalette::Highlight, QColor("#264f78"));
|
||||
pal.setColor(QPalette::HighlightedText, QColor("#ffffff"));
|
||||
setPalette(pal);
|
||||
setAutoFillBackground(true);
|
||||
|
||||
// Thin border
|
||||
setFrameShape(QFrame::Box);
|
||||
setLineWidth(1);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(6, 6, 6, 6);
|
||||
layout->setSpacing(4);
|
||||
|
||||
// Row 1: title + Esc hint
|
||||
{
|
||||
auto* row = new QHBoxLayout;
|
||||
row->setContentsMargins(0, 0, 0, 0);
|
||||
m_titleLabel = new QLabel(QStringLiteral("View as type"));
|
||||
m_titleLabel->setPalette(pal);
|
||||
QFont bold = m_titleLabel->font();
|
||||
bold.setBold(true);
|
||||
m_titleLabel->setFont(bold);
|
||||
row->addWidget(m_titleLabel);
|
||||
|
||||
row->addStretch();
|
||||
|
||||
m_escLabel = new QLabel(QStringLiteral("Esc"));
|
||||
QPalette dimPal = pal;
|
||||
dimPal.setColor(QPalette::WindowText, QColor("#858585"));
|
||||
m_escLabel->setPalette(dimPal);
|
||||
row->addWidget(m_escLabel);
|
||||
|
||||
layout->addLayout(row);
|
||||
}
|
||||
|
||||
// Row 2: + Create new type button
|
||||
{
|
||||
m_createBtn = new QToolButton;
|
||||
m_createBtn->setText(QStringLiteral("+ Create new type\u2026"));
|
||||
m_createBtn->setIcon(QIcon(QStringLiteral(":/vsicons/add.svg")));
|
||||
m_createBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
m_createBtn->setAutoRaise(true);
|
||||
m_createBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_createBtn->setPalette(pal);
|
||||
connect(m_createBtn, &QToolButton::clicked, this, [this]() {
|
||||
emit createNewTypeRequested();
|
||||
hide();
|
||||
});
|
||||
layout->addWidget(m_createBtn);
|
||||
}
|
||||
|
||||
// Separator
|
||||
{
|
||||
auto* sep = new QFrame;
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setFrameShadow(QFrame::Plain);
|
||||
QPalette sepPal = pal;
|
||||
sepPal.setColor(QPalette::WindowText, QColor("#3c3c3c"));
|
||||
sep->setPalette(sepPal);
|
||||
sep->setFixedHeight(1);
|
||||
layout->addWidget(sep);
|
||||
}
|
||||
|
||||
// Row 3: Filter
|
||||
{
|
||||
m_filterEdit = new QLineEdit;
|
||||
m_filterEdit->setPlaceholderText(QStringLiteral("Filter types\u2026"));
|
||||
m_filterEdit->setClearButtonEnabled(true);
|
||||
m_filterEdit->setPalette(pal);
|
||||
m_filterEdit->installEventFilter(this);
|
||||
connect(m_filterEdit, &QLineEdit::textChanged,
|
||||
this, &TypeSelectorPopup::applyFilter);
|
||||
layout->addWidget(m_filterEdit);
|
||||
}
|
||||
|
||||
// Row 4: List
|
||||
{
|
||||
m_model = new QStringListModel(this);
|
||||
m_listView = new QListView;
|
||||
m_listView->setModel(m_model);
|
||||
m_listView->setPalette(pal);
|
||||
m_listView->setFrameShape(QFrame::NoFrame);
|
||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_listView->setMouseTracking(true);
|
||||
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
|
||||
m_listView->installEventFilter(this);
|
||||
|
||||
auto* delegate = new TypeSelectorDelegate(this, m_listView);
|
||||
m_listView->setItemDelegate(delegate);
|
||||
|
||||
layout->addWidget(m_listView, 1);
|
||||
|
||||
connect(m_listView, &QListView::clicked,
|
||||
this, [this](const QModelIndex& index) {
|
||||
acceptIndex(index.row());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setFont(const QFont& font) {
|
||||
m_font = font;
|
||||
|
||||
m_titleLabel->setFont([&]() {
|
||||
QFont f = font; f.setBold(true); return f;
|
||||
}());
|
||||
m_escLabel->setFont(font);
|
||||
m_createBtn->setFont(font);
|
||||
m_filterEdit->setFont(font);
|
||||
m_listView->setFont(font);
|
||||
|
||||
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
|
||||
if (delegate)
|
||||
delegate->setFont(font);
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, uint64_t currentId) {
|
||||
m_allTypes = types;
|
||||
m_currentId = currentId;
|
||||
m_filterEdit->clear();
|
||||
applyFilter(QString());
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
||||
// Size: width based on longest entry, height based on count
|
||||
QFontMetrics fm(m_font);
|
||||
int maxTextW = fm.horizontalAdvance(QStringLiteral("View as type Esc"));
|
||||
for (const auto& t : m_allTypes) {
|
||||
QString text = t.classKeyword + QStringLiteral(" ") + t.displayName;
|
||||
int w = 18 + 20 + fm.horizontalAdvance(text) + 16; // gutter + icon + text + pad
|
||||
if (w > maxTextW) maxTextW = w;
|
||||
}
|
||||
int popupW = qBound(250, maxTextW + 24, 500); // +margins
|
||||
int rowH = fm.height() + 8;
|
||||
int headerH = rowH * 3 + 30; // title + button + filter + separators/margins
|
||||
int listH = qBound(rowH * 3, rowH * (int)m_allTypes.size(), rowH * 12);
|
||||
int popupH = headerH + listH;
|
||||
|
||||
// Clamp to screen
|
||||
QScreen* screen = QApplication::screenAt(globalPos);
|
||||
if (screen) {
|
||||
QRect avail = screen->availableGeometry();
|
||||
if (globalPos.y() + popupH > avail.bottom())
|
||||
popupH = avail.bottom() - globalPos.y();
|
||||
if (globalPos.x() + popupW > avail.right())
|
||||
popupW = avail.right() - globalPos.x();
|
||||
}
|
||||
|
||||
setFixedSize(popupW, popupH);
|
||||
move(globalPos);
|
||||
show();
|
||||
raise();
|
||||
activateWindow();
|
||||
m_filterEdit->setFocus();
|
||||
|
||||
// Pre-select current type in list
|
||||
for (int i = 0; i < m_filteredTypes.size(); i++) {
|
||||
if (m_filteredTypes[i].id == m_currentId) {
|
||||
m_listView->setCurrentIndex(m_model->index(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
m_filteredTypes.clear();
|
||||
QStringList displayStrings;
|
||||
|
||||
for (const auto& t : m_allTypes) {
|
||||
if (text.isEmpty()
|
||||
|| t.displayName.contains(text, Qt::CaseInsensitive)
|
||||
|| t.classKeyword.contains(text, Qt::CaseInsensitive)) {
|
||||
m_filteredTypes.append(t);
|
||||
displayStrings << (t.classKeyword + QStringLiteral(" ") + t.displayName);
|
||||
}
|
||||
}
|
||||
|
||||
m_model->setStringList(displayStrings);
|
||||
|
||||
// Update delegate data
|
||||
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
|
||||
if (delegate)
|
||||
delegate->setCurrentTypes(&m_filteredTypes, m_currentId);
|
||||
|
||||
// Select first match
|
||||
if (!m_filteredTypes.isEmpty())
|
||||
m_listView->setCurrentIndex(m_model->index(0));
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::acceptCurrent() {
|
||||
QModelIndex idx = m_listView->currentIndex();
|
||||
if (idx.isValid())
|
||||
acceptIndex(idx.row());
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::acceptIndex(int row) {
|
||||
if (row < 0 || row >= m_filteredTypes.size()) return;
|
||||
emit typeSelected(m_filteredTypes[row].id);
|
||||
hide();
|
||||
}
|
||||
|
||||
bool TypeSelectorPopup::eventFilter(QObject* obj, QEvent* event) {
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
auto* ke = static_cast<QKeyEvent*>(event);
|
||||
|
||||
if (ke->key() == Qt::Key_Escape) {
|
||||
hide();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj == m_filterEdit) {
|
||||
if (ke->key() == Qt::Key_Down) {
|
||||
m_listView->setFocus();
|
||||
if (!m_listView->currentIndex().isValid() && m_model->rowCount() > 0)
|
||||
m_listView->setCurrentIndex(m_model->index(0));
|
||||
return true;
|
||||
}
|
||||
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
|
||||
acceptCurrent();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj == m_listView) {
|
||||
if (ke->key() == Qt::Key_Up) {
|
||||
QModelIndex cur = m_listView->currentIndex();
|
||||
if (!cur.isValid() || cur.row() == 0) {
|
||||
m_filterEdit->setFocus();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
|
||||
acceptCurrent();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QFrame::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::hideEvent(QHideEvent* event) {
|
||||
QFrame::hideEvent(event);
|
||||
emit dismissed();
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
Reference in New Issue
Block a user