#include "moderncombobox.h" #include "../utils/fadehelper.h" #include "../utils/popupfade.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int kArrowWidth = 32; constexpr int kModernComboMinItemHeight = 36; constexpr auto kPopupEdgeFixMarker = "/* modern-combobox-popup-edge-fix */"; constexpr auto kPopupEdgeFixQss = R"( /* modern-combobox-popup-edge-fix */ QAbstractItemView QScrollBar:vertical { margin: 0px; } )"; } void CenterAlignDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); opt.displayAlignment = Qt::AlignCenter; QStyledItemDelegate::paint(painter, opt, index); } QSize CenterAlignDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QSize base = QStyledItemDelegate::sizeHint(option, index); base.setHeight(qMax(base.height(), kModernComboMinItemHeight)); return base; } ModernComboBox::ModernComboBox(QWidget *parent) : QComboBox(parent) { setItemDelegate(new CenterAlignDelegate(this)); setSizeAdjustPolicy(QComboBox::AdjustToContents); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); connect(this, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) { if (m_maxTextWidth > 0 && index >= 0) { setToolTip(itemText(index)); } else { setToolTip({}); } }); polishPopupView(); } void ModernComboBox::setMaxTextWidth(int pixels) { m_maxTextWidth = pixels; updateGeometry(); } QSize ModernComboBox::sizeHint() const { QFontMetrics fm(font()); int maxW = 60; for (int i = 0; i < count(); ++i) { maxW = qMax(maxW, fm.horizontalAdvance(itemText(i))); } if (m_maxTextWidth > 0) { maxW = qMin(maxW, m_maxTextWidth); } int w = maxW + 16 + 16 + 32; int h = QComboBox::sizeHint().height(); return QSize(w, h); } QSize ModernComboBox::minimumSizeHint() const { return sizeHint(); } void ModernComboBox::showPopup() { if (window() && window()->isFullScreen()) { showEmbeddedPopup(); return; } polishPopupView(); QComboBox::showPopup(); auto *popupView = view(); QWidget *popupContainer = popupView ? popupView->window() : nullptr; if (!popupView || !popupContainer) { return; } PopupFade::run(popupContainer); auto *vScrollBar = popupView->verticalScrollBar(); if (vScrollBar && vScrollBar->isVisible()) { const int targetWidth = qMax(popupContainer->width(), width() + vScrollBar->sizeHint().width()); if (targetWidth > popupContainer->width()) { popupContainer->resize(targetWidth, popupContainer->height()); } } } void ModernComboBox::hidePopup() { closeEmbeddedPopup(); QComboBox::hidePopup(); } bool ModernComboBox::eventFilter(QObject *watched, QEvent *event) { if (!event) { return QComboBox::eventFilter(watched, event); } if (m_embeddedPopup && watched == m_embeddedPopup.data()) { if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick) { closeEmbeddedPopup(); return true; } } return QComboBox::eventFilter(watched, event); } void ModernComboBox::showEmbeddedPopup() { closeEmbeddedPopup(); QWidget *host = window(); if (!host || count() == 0) { return; } const bool flushRightScrollBar = property("flush-right-scrollbar").toBool(); auto *overlay = new QWidget(host); overlay->setAttribute(Qt::WA_DeleteOnClose); overlay->setObjectName(QStringLiteral("modernComboEmbeddedOverlay")); overlay->setGeometry(host->rect()); overlay->installEventFilter(this); auto *listFrame = new QFrame(overlay); listFrame->setObjectName(QStringLiteral("modernComboEmbeddedFrame")); listFrame->setFrameStyle(QFrame::NoFrame); listFrame->setProperty("flush-right-scrollbar", flushRightScrollBar); auto *frameLayout = new QVBoxLayout(listFrame); frameLayout->setContentsMargins(0, 0, 0, 0); frameLayout->setSpacing(0); auto *listWidget = new QListWidget(listFrame); listWidget->setObjectName(QStringLiteral("modernComboEmbeddedList")); listWidget->setProperty("flush-right-scrollbar", flushRightScrollBar); listWidget->setItemDelegate(new CenterAlignDelegate(listWidget)); listWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); listWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); listWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); listWidget->setFocusPolicy(Qt::StrongFocus); listWidget->setFrameStyle(QFrame::NoFrame); listWidget->setContentsMargins(0, 0, 0, 0); for (int i = 0; i < count(); ++i) { auto *item = new QListWidgetItem(itemText(i)); item->setData(Qt::UserRole, itemData(i)); item->setTextAlignment(Qt::AlignCenter); listWidget->addItem(item); } if (currentIndex() >= 0 && currentIndex() < listWidget->count()) { listWidget->setCurrentRow(currentIndex()); } frameLayout->addWidget(listWidget); constexpr int kMaxVisibleItems = 12; constexpr int kItemPadding = 12; const QFontMetrics fm(listWidget->font()); const int itemH = qMax(fm.height() + kItemPadding, kModernComboMinItemHeight); const int visibleItems = qMin(count(), kMaxVisibleItems); int popupH = itemH * visibleItems + 6; int popupW = qMax(width(), 240); popupH = qMin(popupH, host->height() / 2); const QPoint comboBottomLeft = mapTo(host, QPoint(0, height())); const QPoint comboTopLeft = mapTo(host, QPoint(0, 0)); int popupX = comboBottomLeft.x(); int popupY = comboBottomLeft.y() + 4; if (popupY + popupH > host->height() - 10) { popupY = comboTopLeft.y() - popupH - 4; } if (popupX + popupW > host->width() - 10) { popupX = host->width() - popupW - 10; } popupX = qMax(10, popupX); popupY = qMax(10, popupY); listFrame->setGeometry(popupX, popupY, popupW, popupH); overlay->show(); overlay->raise(); listFrame->show(); // Child overlay (not a top-level window) — FadeHelper, not PopupFade. FadeHelper::fadeIn(listFrame, AnimationTokens::DurationMs::kQuick); listWidget->setFocus(); if (currentIndex() >= 0 && currentIndex() < listWidget->count()) { listWidget->scrollToItem(listWidget->item(currentIndex()), QAbstractItemView::PositionAtCenter); } m_embeddedPopup = overlay; connect(listWidget, &QListWidget::itemClicked, this, [this, listWidget](QListWidgetItem *) { const int row = listWidget->currentRow(); if (row >= 0 && row < count()) { setCurrentIndex(row); } closeEmbeddedPopup(); }); auto *escShortcut = new QShortcut(QKeySequence(Qt::Key_Escape), overlay, nullptr, nullptr, Qt::WidgetWithChildrenShortcut); connect(escShortcut, &QShortcut::activated, this, [this]() { closeEmbeddedPopup(); }); auto *enterShortcut = new QShortcut(QKeySequence(Qt::Key_Return), listWidget, nullptr, nullptr, Qt::WidgetWithChildrenShortcut); connect(enterShortcut, &QShortcut::activated, this, [this, listWidget]() { const int row = listWidget->currentRow(); if (row >= 0 && row < count()) { setCurrentIndex(row); } closeEmbeddedPopup(); }); } void ModernComboBox::closeEmbeddedPopup() { if (m_embeddedPopup) { m_embeddedPopup->removeEventFilter(this); m_embeddedPopup->close(); m_embeddedPopup = nullptr; } } void ModernComboBox::polishPopupView() { auto *popupView = view(); if (!popupView) { return; } popupView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); popupView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); QString popupStyleSheet = popupView->styleSheet(); if (!popupStyleSheet.contains(QLatin1String(kPopupEdgeFixMarker))) { if (!popupStyleSheet.isEmpty() && !popupStyleSheet.endsWith(QLatin1Char('\n'))) { popupStyleSheet += QLatin1Char('\n'); } popupStyleSheet += QLatin1String(kPopupEdgeFixQss); popupView->setStyleSheet(popupStyleSheet); } } void ModernComboBox::paintEvent(QPaintEvent *) { QStylePainter painter(this); QStyleOptionComboBox opt; initStyleOption(&opt); QString text = opt.currentText; opt.currentText = ""; painter.drawComplexControl(QStyle::CC_ComboBox, opt); QRect textRect = rect().adjusted(0, 0, -kArrowWidth, 0); if (m_maxTextWidth > 0) { QFontMetrics fm(this->font()); text = fm.elidedText(text, Qt::ElideRight, textRect.width() - 8); } painter.setPen(opt.palette.color(QPalette::ButtonText)); painter.setFont(this->font()); painter.drawText(textRect, Qt::AlignCenter | Qt::TextSingleLine, text); }