Files
qemby/src/qEmbyApp/components/moderncombobox.cpp
T
dela ab1accc821 perf: smooth UI animations and reduce repaint overhead
Batch of performance and animation work: shared FadeHelper/MotionPrefs
primitives, hover animators, shadow pixmap caching, smooth scrolling,
seek time bubble, and widespread component repaint reductions.
2026-07-08 11:29:38 +08:00

379 lines
9.2 KiB
C++

#include "moderncombobox.h"
#include "../utils/fadehelper.h"
#include "../utils/popupfade.h"
#include <QAbstractItemView>
#include <QApplication>
#include <QEvent>
#include <QFontMetrics>
#include <QFrame>
#include <QKeyEvent>
#include <QListWidget>
#include <QPainter>
#include <QScrollBar>
#include <QShortcut>
#include <QStyleOption>
#include <QStyleOptionComboBox>
#include <QStyleOptionViewItem>
#include <QStylePainter>
#include <QVBoxLayout>
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<int>(&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);
}