diff options
| author | MohammadHossein Qanbari <mohammad.qanbari@qt.io> | 2024-08-15 12:49:34 +0200 |
|---|---|---|
| committer | Volker Hilsheimer <volker.hilsheimer@qt.io> | 2024-09-09 12:15:59 +0000 |
| commit | e1ad73a0beea17ea71e216cb58a4ead997fb6f95 (patch) | |
| tree | 6703ff5004dd15b5b2a56368ddf7658a42a309f3 | |
| parent | 409df2c9f244204c00313a115305d1515a3ce4a8 (diff) | |
Menu: Activate Menu Item When Mnemonic Key is Pressed, Same as Widgets
When a menu is opened, its menu items aren't triggered by pressing their
mnemonic key using the Alt key. Also, aren't triggered even when their
mnemonic key is pressed without the Alt key. This behavior is different
from the widgets menu behavior. When the widgets menu is opened its
actions can be activated by pressing their mnemonic key without holding
the Alt key.
When a key is pressed on QQuickMenu and no modifier is held, it loops
over its items to find the item whose mnemonic key matches the key. If
the item is found, the click() method will be called.
The test case opens the menu and sends the press and release events with
the mnemonic key of each menu item, and checks that they are triggered.
When the item is checkable, the test verifies that the checkedChanged()
signal is also emitted.
Conflict at 6.7 resolved by simulating the click functionality as it is
introduced in 6.8.
Task-number: QTBUG-96630
Pick-to: 6.5 6.7.3
Change-Id: I99f27c6306e14375b45d0630e4f9047e33fa64fb
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
(cherry picked from commit 520c8ab1492259c354dd6541a7cd2de1afbd3368)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
(cherry picked from commit 6f4370b5183bb7acb97be4dc3479c0c7bdc0764f)
| -rw-r--r-- | src/quicktemplates/qquickmenu.cpp | 22 | ||||
| -rw-r--r-- | tests/auto/quickcontrols/qquickmenu/data/mnemonics.qml | 17 | ||||
| -rw-r--r-- | tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp | 202 |
3 files changed, 241 insertions, 0 deletions
diff --git a/src/quicktemplates/qquickmenu.cpp b/src/quicktemplates/qquickmenu.cpp index c9f79a261b..453890c7a9 100644 --- a/src/quicktemplates/qquickmenu.cpp +++ b/src/quicktemplates/qquickmenu.cpp @@ -1543,6 +1543,28 @@ void QQuickMenu::keyPressEvent(QKeyEvent *event) default: break; } + +#if QT_CONFIG(shortcut) + if (event->modifiers() == Qt::NoModifier) { + for (int i = 0; i < count(); ++i) { + QQuickAbstractButton *item = qobject_cast<QQuickAbstractButton*>(d->itemAt(i)); + if (!item) + continue; + const QKeySequence keySequence = QKeySequence::mnemonic(item->text()); + if (keySequence.isEmpty()) + continue; + // Have to simulate click on the item since + // QQuickAbstractButton::click() is introduced in Qt-6.8 + if (keySequence[0].key() == event->key() && item->isEnabled()) { + auto *p = QQuickAbstractButtonPrivate::get(item); + const QPointF eventPos(p->width / 2, p->height / 2); + p->handlePress(eventPos, 0); + p->handleRelease(eventPos, 0); + break; + } + } + } +#endif } void QQuickMenu::timerEvent(QTimerEvent *event) diff --git a/tests/auto/quickcontrols/qquickmenu/data/mnemonics.qml b/tests/auto/quickcontrols/qquickmenu/data/mnemonics.qml index 3e072e130f..bcb1434572 100644 --- a/tests/auto/quickcontrols/qquickmenu/data/mnemonics.qml +++ b/tests/auto/quickcontrols/qquickmenu/data/mnemonics.qml @@ -5,14 +5,18 @@ import QtQuick import QtQuick.Controls ApplicationWindow { + id: root width: 400 height: 400 + property bool enabled: true + property bool checkable: false property alias menu: menu property alias action: action property alias menuItem: menuItem property alias subMenu: subMenu property alias subMenuItem: subMenuItem + property alias subMenuAction: subMenuAction Menu { id: menu @@ -20,20 +24,33 @@ ApplicationWindow { Action { id: action text: "&Action" + checkable: root.checkable + enabled: root.enabled } MenuItem { id: menuItem text: "Menu &Item" + checkable: root.checkable + enabled: root.enabled } Menu { id: subMenu title: "Sub &Menu" + Action { + id: subMenuAction + text: "S&ub Menu Action" + checkable: root.checkable + enabled: root.enabled + } + MenuItem { id: subMenuItem text: "&Sub Menu Item" + checkable: root.checkable + enabled: root.enabled } } } diff --git a/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp b/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp index d101f50a7e..0d6da8f54c 100644 --- a/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp +++ b/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp @@ -47,6 +47,10 @@ private slots: void contextMenuKeyboard(); void disabledMenuItemKeyNavigation(); void mnemonics(); +#if QT_CONFIG(shortcut) + void checkableMnemonics_data(); + void checkableMnemonics(); +#endif void menuButton(); void addItem(); void menuSeparator(); @@ -579,6 +583,204 @@ void tst_QQuickMenu::mnemonics() QCOMPARE(subMenuItemSpy.size(), 1); } +#if QT_CONFIG(shortcut) +namespace CheckableMnemonics { +using MnemonicKey = std::pair<Qt::Key, QString>; + +enum class MenuItemType { + Action, + MenuItem +}; + +enum class SignalName { + CheckedChanged = 0x01, + Triggered = 0x02, +}; +Q_DECLARE_FLAGS(SignalNames, SignalName); + +class ItemSignalSpy +{ +public: + ItemSignalSpy(MenuItemType type, QObject *item) : item(item) + { + switch (type) { + case MenuItemType::Action: + initSignals<QQuickAction>(qobject_cast<QQuickAction *>(item)); + break; + case MenuItemType::MenuItem: + initSignals<QQuickMenuItem>(qobject_cast<QQuickMenuItem *>(item)); + break; + } + } + + [[nodiscard]] bool isValid() const + { + return ((checkedChangedSpy && checkedChangedSpy->isValid()) && + (triggeredSpy && triggeredSpy->isValid())); + } + + [[nodiscard]] int signalSize(SignalName signal) const + { + constexpr int INVALID_SIZE = -1; // makes the test fail even when the signal is not expected + switch (signal) { + case SignalName::CheckedChanged: + return checkedChangedSpy ? checkedChangedSpy->size() : INVALID_SIZE; + case SignalName::Triggered: + return triggeredSpy ? triggeredSpy->size() : INVALID_SIZE; + } + Q_UNREACHABLE_RETURN(INVALID_SIZE); + } + +private: + template<typename Item> + void initSignals(Item *item) + { + checkedChangedSpy = std::make_unique<QSignalSpy>(item, &Item::checkedChanged); + triggeredSpy = std::make_unique<QSignalSpy>(item, &Item::triggered); + } + +private: + QPointer<QObject> item; + std::unique_ptr<QSignalSpy> checkedChangedSpy = nullptr; + std::unique_ptr<QSignalSpy> triggeredSpy = nullptr; +}; + +} + +void tst_QQuickMenu::checkableMnemonics_data() +{ + if (QKeySequence::mnemonic("&A").isEmpty()) + QSKIP("Mnemonics are not enabled"); + + using namespace CheckableMnemonics; + + QTest::addColumn<bool>("checkable"); + QTest::addColumn<bool>("enabled"); + QTest::addColumn<bool>("isSubMenu"); + QTest::addColumn<MenuItemType>("itemType"); + QTest::addColumn<MnemonicKey>("mnemonicKey"); + QTest::addColumn<SignalNames>("expectedSignals"); + + QTest::addRow("checkable_enabled_action") + << true << true << false << MenuItemType::Action << MnemonicKey{Qt::Key_A, "A"} + << SignalNames{SignalName::Triggered, SignalName::CheckedChanged}; + QTest::addRow("checkable_disabled_action") + << true << false << false << MenuItemType::Action << MnemonicKey{Qt::Key_A, "A"} + << SignalNames{}; + QTest::addRow("uncheckable_enabled_action") + << false << true << false << MenuItemType::Action << MnemonicKey{Qt::Key_A, "A"} + << SignalNames{SignalName::Triggered}; + QTest::addRow("uncheckable_disabled_action") + << false << false << false << MenuItemType::Action << MnemonicKey{Qt::Key_A, "A"} + << SignalNames{}; + + QTest::addRow("checkable_enabled_menuItem") + << true << true << false << MenuItemType::MenuItem << MnemonicKey{Qt::Key_I, "I"} + << SignalNames{SignalName::Triggered, SignalName::CheckedChanged}; + QTest::addRow("checkable_disabled_menuItem") + << true << false << false << MenuItemType::MenuItem << MnemonicKey{Qt::Key_I, "I"} + << SignalNames{}; + QTest::addRow("uncheckable_enabled_menuItem") + << false << true << false << MenuItemType::MenuItem << MnemonicKey{Qt::Key_I, "I"} + << SignalNames{SignalName::Triggered}; + QTest::addRow("uncheckable_disabled_menuItem") + << false << false << false << MenuItemType::MenuItem << MnemonicKey{Qt::Key_I, "I"} + << SignalNames{}; + + QTest::addRow("checkable_enabled_subMenuItem") + << true << true << true << MenuItemType::MenuItem << MnemonicKey{Qt::Key_S, "S"} + << SignalNames{SignalName::Triggered, SignalName::CheckedChanged}; + QTest::addRow("checkable_disabled_subMenuItem") + << true << false << true << MenuItemType::MenuItem << MnemonicKey{Qt::Key_S, "S"} + << SignalNames{}; + QTest::addRow("uncheckable_enabled_subMenuItem") + << false << true << true << MenuItemType::MenuItem << MnemonicKey{Qt::Key_S, "S"} + << SignalNames{SignalName::Triggered}; + QTest::addRow("uncheckable_disabled_subMenuItem") + << false << false << true << MenuItemType::MenuItem << MnemonicKey{Qt::Key_S, "S"} + << SignalNames{}; + + QTest::addRow("checkable_enabled_subMenuAction") + << true << true << true << MenuItemType::Action << MnemonicKey{Qt::Key_U, "U"} + << SignalNames{SignalName::Triggered, SignalName::CheckedChanged}; + QTest::addRow("checkable_disabled_subMenuAction") + << true << false << true << MenuItemType::Action << MnemonicKey{Qt::Key_U, "U"} + << SignalNames{}; + QTest::addRow("uncheckable_enabled_subMenuAction") + << false << true << true << MenuItemType::Action << MnemonicKey{Qt::Key_U, "U"} + << SignalNames{SignalName::Triggered}; + QTest::addRow("uncheckable_disabled_subMenuAction") + << false << false << true << MenuItemType::Action << MnemonicKey{Qt::Key_U, "U"} + << SignalNames{}; +} + +// QTBUG-96630 +void tst_QQuickMenu::checkableMnemonics() +{ + using namespace CheckableMnemonics; + + QFETCH(bool, checkable); + QFETCH(bool, enabled); + QFETCH(bool, isSubMenu); + QFETCH(MenuItemType, itemType); + QFETCH(MnemonicKey, mnemonicKey); + QFETCH(SignalNames, expectedSignals); + + QQuickControlsApplicationHelper helper(this, QLatin1String("mnemonics.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickWindow *window = helper.window; + window->show(); + window->requestActivate(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + window->setProperty("checkable", checkable); + window->setProperty("enabled", enabled); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QVERIFY(menu); + + auto clickKey = [window](const MnemonicKey &mnemonic) mutable { + QTest::simulateEvent(window, true, mnemonic.first, Qt::NoModifier, mnemonic.second, false); + QTest::simulateEvent(window, false, mnemonic.first, Qt::NoModifier, mnemonic.second, false); + }; + + constexpr auto EMPTY_ITEM_NAME = ""; + const char *itemName = EMPTY_ITEM_NAME; + switch (itemType) { + case MenuItemType::Action: + itemName = isSubMenu ? "subMenuAction" : "action"; + break; + case MenuItemType::MenuItem: + itemName = isSubMenu ? "subMenuItem" : "menuItem"; + break; + } + QCOMPARE_NE(itemName, EMPTY_ITEM_NAME); + + QObject *menuItem = window->property(itemName).value<QObject*>(); + QVERIFY(menuItem); + + menu->open(); + QTRY_VERIFY(menu->isOpened()); + + if (isSubMenu) { + QQuickMenu *subMenu = window->property("subMenu").value<QQuickMenu *>(); + QVERIFY(subMenu); + clickKey(MnemonicKey{Qt::Key_M, "M"}); // "Sub &Menu" + QTRY_VERIFY(subMenu->isOpened()); + } + + const ItemSignalSpy itemSignalSpy(itemType, menuItem); + QVERIFY(itemSignalSpy.isValid()); + + clickKey(mnemonicKey); + QCOMPARE(itemSignalSpy.signalSize(SignalName::CheckedChanged), + expectedSignals & SignalName::CheckedChanged ? 1 : 0); + QCOMPARE(itemSignalSpy.signalSize(SignalName::Triggered), + expectedSignals & SignalName::Triggered ? 1 : 0); +} +#endif + void tst_QQuickMenu::menuButton() { SKIP_IF_NO_WINDOW_ACTIVATION |
