QDialogButtonBox - Update focus chain when buttons show or hide
Hiding a button in a QDialogButtonBox doesn't remove its default and focus behavior. Hiding the button shown in the first position, breaks the focus chain. Tabbing between the button is no longer possible. This patch implements listening to the buttons' HideToParent and ShowToParent events. Hidden buttons are removed from the button box and kept in a separate hash. That ensures focus chain consistency. When they are shown again, they are added to the button logic and their default/focus behavior is restored. An autotest is added in tst_QDialogButtonBox. Fixes: QTBUG-114377 Change-Id: Id10c4675f43d6007206e41c694688c4f0a34ee52 Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> (cherry picked from commit bbb71e7e80f292c2e69faef81b1624832981147e) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
parent
2e70ca4847
commit
63428b1f1f
@ -112,7 +112,6 @@ QT_BEGIN_NAMESPACE
|
|||||||
|
|
||||||
\sa QMessageBox, QPushButton, QDialog
|
\sa QMessageBox, QPushButton, QDialog
|
||||||
*/
|
*/
|
||||||
|
|
||||||
QDialogButtonBoxPrivate::QDialogButtonBoxPrivate(Qt::Orientation orient)
|
QDialogButtonBoxPrivate::QDialogButtonBoxPrivate(Qt::Orientation orient)
|
||||||
: orientation(orient), buttonLayout(nullptr), center(false)
|
: orientation(orient), buttonLayout(nullptr), center(false)
|
||||||
{
|
{
|
||||||
@ -172,6 +171,7 @@ void QDialogButtonBoxPrivate::layoutButtons()
|
|||||||
Q_Q(QDialogButtonBox);
|
Q_Q(QDialogButtonBox);
|
||||||
const int MacGap = 36 - 8; // 8 is the default gap between a widget and a spacer item
|
const int MacGap = 36 - 8; // 8 is the default gap between a widget and a spacer item
|
||||||
|
|
||||||
|
QBoolBlocker blocker(byPassEventFilter);
|
||||||
for (int i = buttonLayout->count() - 1; i >= 0; --i) {
|
for (int i = buttonLayout->count() - 1; i >= 0; --i) {
|
||||||
QLayoutItem *item = buttonLayout->takeAt(i);
|
QLayoutItem *item = buttonLayout->takeAt(i);
|
||||||
if (QWidget *widget = item->widget())
|
if (QWidget *widget = item->widget())
|
||||||
@ -367,11 +367,22 @@ QPushButton *QDialogButtonBoxPrivate::createButton(QDialogButtonBox::StandardBut
|
|||||||
}
|
}
|
||||||
|
|
||||||
void QDialogButtonBoxPrivate::addButton(QAbstractButton *button, QDialogButtonBox::ButtonRole role,
|
void QDialogButtonBoxPrivate::addButton(QAbstractButton *button, QDialogButtonBox::ButtonRole role,
|
||||||
LayoutRule layoutRule)
|
LayoutRule layoutRule, AddRule addRule)
|
||||||
{
|
{
|
||||||
QObjectPrivate::connect(button, &QAbstractButton::clicked, this, &QDialogButtonBoxPrivate::handleButtonClicked);
|
Q_Q(QDialogButtonBox);
|
||||||
QObjectPrivate::connect(button, &QAbstractButton::destroyed, this, &QDialogButtonBoxPrivate::handleButtonDestroyed);
|
|
||||||
buttonLists[role].append(button);
|
buttonLists[role].append(button);
|
||||||
|
switch (addRule) {
|
||||||
|
case AddRule::Connect:
|
||||||
|
QObjectPrivate::connect(button, &QAbstractButton::clicked,
|
||||||
|
this, &QDialogButtonBoxPrivate::handleButtonClicked);
|
||||||
|
QObjectPrivate::connect(button, &QAbstractButton::destroyed,
|
||||||
|
this, &QDialogButtonBoxPrivate::handleButtonDestroyed);
|
||||||
|
button->installEventFilter(q);
|
||||||
|
break;
|
||||||
|
case AddRule::SkipConnect:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
switch (layoutRule) {
|
switch (layoutRule) {
|
||||||
case LayoutRule::DoLayout:
|
case LayoutRule::DoLayout:
|
||||||
layoutButtons();
|
layoutButtons();
|
||||||
@ -453,10 +464,13 @@ QDialogButtonBox::QDialogButtonBox(StandardButtons buttons, Qt::Orientation orie
|
|||||||
*/
|
*/
|
||||||
QDialogButtonBox::~QDialogButtonBox()
|
QDialogButtonBox::~QDialogButtonBox()
|
||||||
{
|
{
|
||||||
|
Q_D(QDialogButtonBox);
|
||||||
|
|
||||||
|
d->byPassEventFilter = true;
|
||||||
|
|
||||||
// QObjectPrivate::connect requires explicit disconnect in destructor
|
// QObjectPrivate::connect requires explicit disconnect in destructor
|
||||||
// otherwise the connection may kick in on child destruction and reach
|
// otherwise the connection may kick in on child destruction and reach
|
||||||
// the parent's destroyed private object
|
// the parent's destroyed private object
|
||||||
Q_D(QDialogButtonBox);
|
|
||||||
d->disconnectAll();
|
d->disconnectAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -622,22 +636,32 @@ void QDialogButtonBox::clear()
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
Returns a list of all the buttons that have been added to the button box.
|
Returns a list of all buttons that have been added to the button box.
|
||||||
|
|
||||||
\sa buttonRole(), addButton(), removeButton()
|
\sa buttonRole(), addButton(), removeButton()
|
||||||
*/
|
*/
|
||||||
QList<QAbstractButton *> QDialogButtonBox::buttons() const
|
QList<QAbstractButton *> QDialogButtonBox::buttons() const
|
||||||
{
|
{
|
||||||
Q_D(const QDialogButtonBox);
|
Q_D(const QDialogButtonBox);
|
||||||
|
return d->allButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QAbstractButton *> QDialogButtonBoxPrivate::visibleButtons() const
|
||||||
|
{
|
||||||
QList<QAbstractButton *> finalList;
|
QList<QAbstractButton *> finalList;
|
||||||
for (int i = 0; i < NRoles; ++i) {
|
for (int i = 0; i < QDialogButtonBox::NRoles; ++i) {
|
||||||
const QList<QAbstractButton *> &list = d->buttonLists[i];
|
const QList<QAbstractButton *> &list = buttonLists[i];
|
||||||
for (int j = 0; j < list.size(); ++j)
|
for (int j = 0; j < list.size(); ++j)
|
||||||
finalList.append(list.at(j));
|
finalList.append(list.at(j));
|
||||||
}
|
}
|
||||||
return finalList;
|
return finalList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<QAbstractButton *> QDialogButtonBoxPrivate::allButtons() const
|
||||||
|
{
|
||||||
|
return visibleButtons() << hiddenButtons.keys();
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
Returns the button role for the specified \a button. This function returns
|
Returns the button role for the specified \a button. This function returns
|
||||||
\l InvalidRole if \a button is \nullptr or has not been added to the button box.
|
\l InvalidRole if \a button is \nullptr or has not been added to the button box.
|
||||||
@ -654,7 +678,7 @@ QDialogButtonBox::ButtonRole QDialogButtonBox::buttonRole(QAbstractButton *butto
|
|||||||
return ButtonRole(i);
|
return ButtonRole(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return InvalidRole;
|
return d->hiddenButtons.value(button, InvalidRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@ -670,9 +694,13 @@ void QDialogButtonBox::removeButton(QAbstractButton *button)
|
|||||||
|
|
||||||
void QDialogButtonBoxPrivate::removeButton(QAbstractButton *button, RemoveRule rule)
|
void QDialogButtonBoxPrivate::removeButton(QAbstractButton *button, RemoveRule rule)
|
||||||
{
|
{
|
||||||
|
Q_Q(QDialogButtonBox);
|
||||||
if (!button)
|
if (!button)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Remove it from hidden buttons
|
||||||
|
hiddenButtons.remove(button);
|
||||||
|
|
||||||
// Remove it from the standard button hash first and then from the roles
|
// Remove it from the standard button hash first and then from the roles
|
||||||
standardButtonHash.remove(reinterpret_cast<QPushButton *>(button));
|
standardButtonHash.remove(reinterpret_cast<QPushButton *>(button));
|
||||||
for (int i = 0; i < QDialogButtonBox::NRoles; ++i)
|
for (int i = 0; i < QDialogButtonBox::NRoles; ++i)
|
||||||
@ -685,6 +713,7 @@ void QDialogButtonBoxPrivate::removeButton(QAbstractButton *button, RemoveRule r
|
|||||||
this, &QDialogButtonBoxPrivate::handleButtonClicked);
|
this, &QDialogButtonBoxPrivate::handleButtonClicked);
|
||||||
QObjectPrivate::disconnect(button, &QAbstractButton::destroyed,
|
QObjectPrivate::disconnect(button, &QAbstractButton::destroyed,
|
||||||
this, &QDialogButtonBoxPrivate::handleButtonDestroyed);
|
this, &QDialogButtonBoxPrivate::handleButtonDestroyed);
|
||||||
|
button->removeEventFilter(q);
|
||||||
break;
|
break;
|
||||||
case RemoveRule::KeepConnections:
|
case RemoveRule::KeepConnections:
|
||||||
break;
|
break;
|
||||||
@ -841,6 +870,54 @@ void QDialogButtonBoxPrivate::handleButtonDestroyed()
|
|||||||
removeButton(reinterpret_cast<QAbstractButton *>(object), RemoveRule::KeepConnections);
|
removeButton(reinterpret_cast<QAbstractButton *>(object), RemoveRule::KeepConnections);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool QDialogButtonBox::eventFilter(QObject *object, QEvent *event)
|
||||||
|
{
|
||||||
|
Q_D(QDialogButtonBox);
|
||||||
|
if (d->byPassEventFilter)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
QAbstractButton *button = qobject_cast<QAbstractButton *>(object);
|
||||||
|
if (!button)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
|
||||||
|
const QEvent::Type type = event->type();
|
||||||
|
if (type == QEvent::HideToParent || type == QEvent::ShowToParent)
|
||||||
|
return d->handleButtonShowAndHide(button, event);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QDialogButtonBoxPrivate::handleButtonShowAndHide(QAbstractButton *button, QEvent *event)
|
||||||
|
{
|
||||||
|
Q_Q(QDialogButtonBox);
|
||||||
|
|
||||||
|
const QEvent::Type type = event->type();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case QEvent::HideToParent: {
|
||||||
|
const QDialogButtonBox::ButtonRole role = q->buttonRole(button);
|
||||||
|
if (role != QDialogButtonBox::ButtonRole::InvalidRole) {
|
||||||
|
removeButton(button, RemoveRule::KeepConnections);
|
||||||
|
hiddenButtons.insert(button, role);
|
||||||
|
layoutButtons();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case QEvent::ShowToParent:
|
||||||
|
if (hiddenButtons.contains(button)) {
|
||||||
|
const auto role = hiddenButtons.take(button);
|
||||||
|
addButton(button, role, LayoutRule::DoLayout, AddRule::SkipConnect);
|
||||||
|
if (role == QDialogButtonBox::AcceptRole)
|
||||||
|
ensureFirstAcceptIsDefault();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
\property QDialogButtonBox::centerButtons
|
\property QDialogButtonBox::centerButtons
|
||||||
\brief whether the buttons in the button box are centered
|
\brief whether the buttons in the button box are centered
|
||||||
|
@ -107,6 +107,8 @@ public:
|
|||||||
void setCenterButtons(bool center);
|
void setCenterButtons(bool center);
|
||||||
bool centerButtons() const;
|
bool centerButtons() const;
|
||||||
|
|
||||||
|
bool eventFilter(QObject *object, QEvent *event) override;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void clicked(QAbstractButton *button);
|
void clicked(QAbstractButton *button);
|
||||||
void accepted();
|
void accepted();
|
||||||
|
@ -32,6 +32,10 @@ public:
|
|||||||
DoLayout,
|
DoLayout,
|
||||||
SkipLayout,
|
SkipLayout,
|
||||||
};
|
};
|
||||||
|
enum class AddRule {
|
||||||
|
Connect,
|
||||||
|
SkipConnect,
|
||||||
|
};
|
||||||
|
|
||||||
QDialogButtonBoxPrivate(Qt::Orientation orient);
|
QDialogButtonBoxPrivate(Qt::Orientation orient);
|
||||||
|
|
||||||
@ -54,7 +58,8 @@ public:
|
|||||||
QPushButton *createButton(QDialogButtonBox::StandardButton button,
|
QPushButton *createButton(QDialogButtonBox::StandardButton button,
|
||||||
LayoutRule layoutRule = LayoutRule::DoLayout);
|
LayoutRule layoutRule = LayoutRule::DoLayout);
|
||||||
void addButton(QAbstractButton *button, QDialogButtonBox::ButtonRole role,
|
void addButton(QAbstractButton *button, QDialogButtonBox::ButtonRole role,
|
||||||
LayoutRule layoutRule = LayoutRule::DoLayout);
|
LayoutRule layoutRule = LayoutRule::DoLayout,
|
||||||
|
AddRule addRule = AddRule::Connect);
|
||||||
void handleButtonDestroyed();
|
void handleButtonDestroyed();
|
||||||
void handleButtonClicked();
|
void handleButtonClicked();
|
||||||
bool handleButtonShowAndHide(QAbstractButton *button, QEvent *event);
|
bool handleButtonShowAndHide(QAbstractButton *button, QEvent *event);
|
||||||
|
@ -11,4 +11,5 @@ qt_internal_add_test(tst_qdialogbuttonbox
|
|||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
Qt::Widgets
|
Qt::Widgets
|
||||||
|
Qt::WidgetsPrivate
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
#include <QtWidgets/QDialog>
|
#include <QtWidgets/QDialog>
|
||||||
#include <QtGui/QAction>
|
#include <QtGui/QAction>
|
||||||
#include <qdialogbuttonbox.h>
|
#include <qdialogbuttonbox.h>
|
||||||
|
#include <QtWidgets/private/qdialogbuttonbox_p.h>
|
||||||
|
#include <QtWidgets/private/qabstractbutton_p.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(QDialogButtonBox::ButtonRole)
|
Q_DECLARE_METATYPE(QDialogButtonBox::ButtonRole)
|
||||||
@ -49,6 +51,9 @@ private slots:
|
|||||||
void clear();
|
void clear();
|
||||||
void removeButton_data();
|
void removeButton_data();
|
||||||
void removeButton();
|
void removeButton();
|
||||||
|
#ifdef QT_BUILD_INTERNAL
|
||||||
|
void hideAndShowButton();
|
||||||
|
#endif
|
||||||
void buttonRole_data();
|
void buttonRole_data();
|
||||||
void buttonRole();
|
void buttonRole();
|
||||||
void setStandardButtons_data();
|
void setStandardButtons_data();
|
||||||
@ -372,6 +377,51 @@ void tst_QDialogButtonBox::removeButton()
|
|||||||
delete button;
|
delete button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef QT_BUILD_INTERNAL
|
||||||
|
void tst_QDialogButtonBox::hideAndShowButton()
|
||||||
|
{
|
||||||
|
QDialogButtonBox buttonBox;
|
||||||
|
QDialogButtonBoxPrivate *d = static_cast<QDialogButtonBoxPrivate *>(QObjectPrivate::get(&buttonBox));
|
||||||
|
auto *apply = buttonBox.addButton( "Apply", QDialogButtonBox::ApplyRole );
|
||||||
|
auto *accept = buttonBox.addButton( "Accept", QDialogButtonBox::AcceptRole );
|
||||||
|
buttonBox.addButton( "Reject", QDialogButtonBox::RejectRole );
|
||||||
|
auto *widget = new QWidget();
|
||||||
|
auto *layout = new QHBoxLayout(widget);
|
||||||
|
layout->addWidget(&buttonBox);
|
||||||
|
|
||||||
|
// apply button shows first on macOS. accept button on all other OSes.
|
||||||
|
QAbstractButton *hideButton;
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
hideButton = apply;
|
||||||
|
Q_UNUSED(accept);
|
||||||
|
#else
|
||||||
|
hideButton = accept;
|
||||||
|
Q_UNUSED(apply);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
hideButton->hide();
|
||||||
|
widget->show();
|
||||||
|
QVERIFY(QTest::qWaitForWindowExposed(widget));
|
||||||
|
QVERIFY(buttonBox.focusWidget()); // QTBUG-114377: Without fixing, focusWidget() == nullptr
|
||||||
|
QCOMPARE(d->visibleButtons().count(), 2);
|
||||||
|
QCOMPARE(d->hiddenButtons.count(), 1);
|
||||||
|
QVERIFY(d->hiddenButtons.contains(hideButton));
|
||||||
|
QCOMPARE(buttonBox.buttons().count(), 3);
|
||||||
|
QSignalSpy spy(qApp, &QApplication::focusChanged);
|
||||||
|
QTest::sendKeyEvent(QTest::KeyAction::Click, &buttonBox, Qt::Key_Tab, QString(), Qt::KeyboardModifiers());
|
||||||
|
QCOMPARE(spy.count(), 1); // QTBUG-114377: Without fixing, tabbing wouldn't work.
|
||||||
|
hideButton->show();
|
||||||
|
QCOMPARE_GE(spy.count(), 1);
|
||||||
|
QTRY_COMPARE(QApplication::focusWidget(), hideButton);
|
||||||
|
QCOMPARE(d->visibleButtons().count(), 3);
|
||||||
|
QCOMPARE(d->hiddenButtons.count(), 0);
|
||||||
|
QCOMPARE(buttonBox.buttons().count(), 3);
|
||||||
|
spy.clear();
|
||||||
|
QTest::sendKeyEvent(QTest::KeyAction::Click, &buttonBox, Qt::Key_Backtab, QString(), Qt::KeyboardModifiers());
|
||||||
|
QCOMPARE(spy.count(), 1);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void tst_QDialogButtonBox::testDelete()
|
void tst_QDialogButtonBox::testDelete()
|
||||||
{
|
{
|
||||||
QDialogButtonBox buttonBox;
|
QDialogButtonBox buttonBox;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user