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.
379 lines
9.2 KiB
C++
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);
|
|
}
|