aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohammadHossein Qanbari <mohammad.qanbari@qt.io>2024-08-15 12:49:34 +0200
committerVolker Hilsheimer <volker.hilsheimer@qt.io>2024-09-09 12:15:59 +0000
commite1ad73a0beea17ea71e216cb58a4ead997fb6f95 (patch)
tree6703ff5004dd15b5b2a56368ddf7658a42a309f3
parent409df2c9f244204c00313a115305d1515a3ce4a8 (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.cpp22
-rw-r--r--tests/auto/quickcontrols/qquickmenu/data/mnemonics.qml17
-rw-r--r--tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp202
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