Support checkable QComboBox items with styles using a popup dropdown

The dropdown of a combobox is rendered using menu items when the style
request it to do so via the SH_ComboBox_Popup style hint.

In that case, checkable items were not supported; the QComboBox didn't
pass the checked state correctly to the style, and the delegate used
for rendering the items into the list view did not implement modifying
the checked state of the item.

However, the QStyleOptionMenuItem's checked state and checkType members
were set anyway, as on e.g. macOS style we use a checkmark to show
which item is currently selected in the combobox.

The QStyle::State enum defines State_On and State_Off for toggleable
things, so in addition to setting QStyleOptionMenuItem::checked, we are
now also adding State_On or State_Off if the model provides a valid
checked/unchecked state. Otherwise, we only set the checked state if
the item is currently selected.

In addition, we implement the delegate to support toggling of checkable
model data with mouse and keyboard, using a simplified version of the
QItemDelegate implementation. To avoid spurious item toggles when the
popup is opened, we only handle mouse releases when the press was on
the same row.

In the fusion style, we ignore the workaround to let QtQuickControls
render comboboxes if State_On or State_Off are set.

[ChangeLog][QtWidgets][QComboBox] Support checkable items in styles
that use a popup for the dropdown.

Change-Id: Ia01519694b0419d777dc66b1ef683482fb01754c
Fixes: QTBUG-60310
Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
Reviewed-by: Christian Ehrlicher <ch.ehrlicher@gmx.de>
This commit is contained in:
Volker Hilsheimer 2019-12-02 18:54:14 +01:00
parent 5a26bf9d65
commit 3e9895f3de
3 changed files with 69 additions and 5 deletions

View File

@ -1590,7 +1590,7 @@ void QFusionStyle::drawControl(ControlElement element, const QStyleOption *optio
(option->styleObject && option->styleObject->property("_q_isComboBoxPopupItem").toBool()))
ignoreCheckMark = true; //ignore the checkmarks provided by the QComboMenuDelegate
if (!ignoreCheckMark) {
if (!ignoreCheckMark || menuItem->state & (State_On | State_Off)) {
// Check, using qreal and QRectF to avoid error accumulation
const qreal boxMargin = dpiScaled(3.5, option);
const qreal boxWidth = checkcol - 2 * boxMargin;
@ -1601,7 +1601,7 @@ void QFusionStyle::drawControl(ControlElement element, const QStyleOption *optio
if (checkable) {
if (menuItem->checkType & QStyleOptionMenuItem::Exclusive) {
// Radio button
if (checked || sunken) {
if (menuItem->state & State_On || checked || sunken) {
painter->setRenderHint(QPainter::Antialiasing);
painter->setPen(Qt::NoPen);
@ -1617,8 +1617,10 @@ void QFusionStyle::drawControl(ControlElement element, const QStyleOption *optio
QStyleOptionButton box;
box.QStyleOption::operator=(*option);
box.rect = checkRect;
if (checked)
if (checked || menuItem->state & State_On)
box.state |= State_On;
else
box.state |= State_Off;
proxy()->drawPrimitive(PE_IndicatorCheckBox, &box, painter, widget);
}
}

View File

@ -129,7 +129,15 @@ QStyleOptionMenuItem QComboMenuDelegate::getStyleOption(const QStyleOptionViewIt
if (option.state & QStyle::State_Selected)
menuOption.state |= QStyle::State_Selected;
menuOption.checkType = QStyleOptionMenuItem::NonExclusive;
menuOption.checked = mCombo->currentIndex() == index.row();
// a valid checkstate means that the model has checkable items
const QVariant checkState = index.data(Qt::CheckStateRole);
if (!checkState.isValid()) {
menuOption.checked = mCombo->currentIndex() == index.row();
} else {
menuOption.checked = qvariant_cast<int>(checkState) == Qt::Checked;
menuOption.state |= qvariant_cast<int>(checkState) == Qt::Checked
? QStyle::State_On : QStyle::State_Off;
}
if (QComboBoxDelegate::isSeparator(index))
menuOption.menuItemType = QStyleOptionMenuItem::Separator;
else
@ -179,6 +187,55 @@ QStyleOptionMenuItem QComboMenuDelegate::getStyleOption(const QStyleOptionViewIt
return menuOption;
}
bool QComboMenuDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option, const QModelIndex &index)
{
Q_ASSERT(event);
Q_ASSERT(model);
// make sure that the item is checkable
Qt::ItemFlags flags = model->flags(index);
if (!(flags & Qt::ItemIsUserCheckable) || !(option.state & QStyle::State_Enabled)
|| !(flags & Qt::ItemIsEnabled))
return false;
// make sure that we have a check state
const QVariant checkState = index.data(Qt::CheckStateRole);
if (!checkState.isValid())
return false;
// make sure that we have the right event type
if ((event->type() == QEvent::MouseButtonRelease)
|| (event->type() == QEvent::MouseButtonDblClick)
|| (event->type() == QEvent::MouseButtonPress)) {
QMouseEvent *me = static_cast<QMouseEvent*>(event);
if (me->button() != Qt::LeftButton)
return false;
if ((event->type() == QEvent::MouseButtonPress)
|| (event->type() == QEvent::MouseButtonDblClick)) {
pressedIndex = index.row();
return false;
}
if (index.row() != pressedIndex)
return false;
pressedIndex = -1;
} else if (event->type() == QEvent::KeyPress) {
if (static_cast<QKeyEvent*>(event)->key() != Qt::Key_Space
&& static_cast<QKeyEvent*>(event)->key() != Qt::Key_Select)
return false;
} else {
return false;
}
// we don't support user-tristate items in QComboBox (not implemented in any style)
Qt::CheckState newState = (static_cast<Qt::CheckState>(checkState.toInt()) == Qt::Checked)
? Qt::Unchecked : Qt::Checked;
return model->setData(index, newState, Qt::CheckStateRole);
}
#if QT_CONFIG(completer)
void QComboBoxPrivate::_q_completerActivated(const QModelIndex &index)
{

View File

@ -266,7 +266,9 @@ class Q_AUTOTEST_EXPORT QComboMenuDelegate : public QAbstractItemDelegate
{
Q_OBJECT
public:
QComboMenuDelegate(QObject *parent, QComboBox *cmb) : QAbstractItemDelegate(parent), mCombo(cmb) {}
QComboMenuDelegate(QObject *parent, QComboBox *cmb)
: QAbstractItemDelegate(parent), mCombo(cmb), pressedIndex(-1)
{}
protected:
void paint(QPainter *painter,
@ -282,11 +284,14 @@ protected:
return mCombo->style()->sizeFromContents(
QStyle::CT_MenuItem, &opt, option.rect.size(), mCombo);
}
bool editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option, const QModelIndex &index) override;
private:
QStyleOptionMenuItem getStyleOption(const QStyleOptionViewItem &option,
const QModelIndex &index) const;
QComboBox *mCombo;
int pressedIndex;
};
// ### Qt6: QStyledItemDelegate ?