From a921d69d3a0da773db8a693d6b14550b0e7019ae Mon Sep 17 00:00:00 2001 From: Mitch Curtis Date: Mon, 21 Sep 2026 13:48:00 +0800 Subject: QQuickAbstractButton: fix mnemonics not being stripped on macOS 474a84312a06c427dee4e248bb85ac2c460a3a71 intended to disable mnemonics for macOS. However, it defaulted QQuickMnemonicLabel::m_mnemonicEnabled (and QQuickIconLabel::m_mnemonicEnabled, which just forwards it to QQuickMnemonicLabel) to QPlatformTheme::MnemonicsEnabled, which is false for macOS. The property's value controls whether the & is removed or not: if true, it is removed, but whether it's underlined is still controlled by QPlatformTheme::UnderlineShortcut. If false, the & is kept. This patch fixes the issue by simply defaulting m_mnemonicEnabled to true, and leaving it up to ItemDelegate and derived types to set it to false. Fix the test so that it actually tests this, as before it was relying on the platform theme hint. Task-number: QTBUG-73087 Change-Id: I176d167116958296fb5d8e6777048d6203018a4e Reviewed-by: Richard Moe Gustavsen --- src/quickcontrolsimpl/qquickiconlabel.cpp | 9 --- src/quickcontrolsimpl/qquickiconlabel_p_p.h | 5 +- src/quickcontrolsimpl/qquickmnemoniclabel.cpp | 10 +--- src/quickcontrolsimpl/qquickmnemoniclabel_p.h | 3 + .../tst_qquickabstractbutton.cpp | 65 +++++++++++----------- 5 files changed, 43 insertions(+), 49 deletions(-) diff --git a/src/quickcontrolsimpl/qquickiconlabel.cpp b/src/quickcontrolsimpl/qquickiconlabel.cpp index 1c0c56c098..030399364f 100644 --- a/src/quickcontrolsimpl/qquickiconlabel.cpp +++ b/src/quickcontrolsimpl/qquickiconlabel.cpp @@ -8,7 +8,6 @@ #include "qquickmnemoniclabel_p.h" #include -#include #include #include #include @@ -28,12 +27,6 @@ void QQuickIconLabelPrivate::completeComponent(QQuickItem *item) parserStatus->componentComplete(); } -void QQuickIconLabelPrivate::init() -{ - mnemonicEnabled = QGuiApplicationPrivate::platformTheme()->themeHint( - QPlatformTheme::MnemonicsEnabled).toBool(); -} - QQuickIconLabelPrivate::~QQuickIconLabelPrivate() = default; bool QQuickIconLabelPrivate::hasIcon() const @@ -366,13 +359,11 @@ void QQuickIconLabelPrivate::displayChange() QQuickIconLabel::QQuickIconLabel(QQuickItem *parent) : QQuickItem(*(new QQuickIconLabelPrivate), parent) { - d_func()->init(); } QQuickIconLabel::QQuickIconLabel(QQuickIconLabelPrivate &dd, QQuickItem *parent) : QQuickItem(dd, parent) { - d_func()->init(); } QQuickIconLabel::~QQuickIconLabel() diff --git a/src/quickcontrolsimpl/qquickiconlabel_p_p.h b/src/quickcontrolsimpl/qquickiconlabel_p_p.h index 15aa9fb42d..fba5132c7f 100644 --- a/src/quickcontrolsimpl/qquickiconlabel_p_p.h +++ b/src/quickcontrolsimpl/qquickiconlabel_p_p.h @@ -31,7 +31,6 @@ class Q_AUTOTEST_EXPORT QQuickIconLabelPrivate : public QQuickItemPrivate, Q_DECLARE_PUBLIC(QQuickIconLabel) public: - void init(); ~QQuickIconLabelPrivate() override; bool hasIcon() const; @@ -71,6 +70,10 @@ public: const QRectF &rectangle); bool mirrored = false; + // See QQuickMnemonicLabel::m_mnemonicEnabled for why this is true by default. + // We just act as an intermediary between types that require an Icon + Label, like Button, + // and the QQuickMnemonicLabel itself. Other types like CheckBox use QQuickCheckLabel directly, + // which derives from QQuickMnemonicLabel. bool mnemonicEnabled = true; QQuickIconLabel::Display display = QQuickIconLabel::TextBesideIcon; Qt::Alignment alignment = Qt::AlignCenter; diff --git a/src/quickcontrolsimpl/qquickmnemoniclabel.cpp b/src/quickcontrolsimpl/qquickmnemoniclabel.cpp index 471272ad45..01bb277b67 100644 --- a/src/quickcontrolsimpl/qquickmnemoniclabel.cpp +++ b/src/quickcontrolsimpl/qquickmnemoniclabel.cpp @@ -13,8 +13,6 @@ QT_BEGIN_NAMESPACE QQuickMnemonicLabel::QQuickMnemonicLabel(QQuickItem *parent) : QQuickText(parent) { - m_mnemonicEnabled = QGuiApplicationPrivate::platformTheme()->themeHint( - QPlatformTheme::MnemonicsEnabled).toBool(); } QString QQuickMnemonicLabel::text() const @@ -53,8 +51,7 @@ void QQuickMnemonicLabel::setText(const QString &text) \li \c false: the displayed text is \c "M&nemonic" (unchanged). \endlist - The default value is \c true if the platform supports mnemonics - (\c QPlatformTheme::MnemonicsEnabled), otherwise \c false. + The default value is \c true. */ bool QQuickMnemonicLabel::isMnemonicEnabled() const { @@ -103,7 +100,7 @@ void QQuickMnemonicLabel::updateMnemonic() while (len) { if (m_fullText.at(pos) == QLatin1Char('&') && (len == 1 || m_fullText.at(pos + 1) != QLatin1Char('&'))) { // A plain mnemonic marker, e.g. "M&nemonic": drop the "&" and - // underline the character that follows it. + // underline the character that follows it (if the platform supports it). if (showUnderline && (pos == 0 || m_fullText.at(pos - 1) != QLatin1Char('&'))) formats += underlineRange(pos); ++pos; @@ -116,8 +113,7 @@ void QQuickMnemonicLabel::updateMnemonic() m_fullText.at(pos + 3) == QLatin1Char(')')) { // A mnemonic with format "\s*(&X)", used when the label itself has // no natural character to underline (e.g. non-Latin scripts). Keep - // "X" in the text, and underline it if the platform draws - // underlines for shortcuts. + // "X" in the text, and underline it if the platform supports it. if (showUnderline) formats += underlineRange(pos + 1); } diff --git a/src/quickcontrolsimpl/qquickmnemoniclabel_p.h b/src/quickcontrolsimpl/qquickmnemoniclabel_p.h index b92a69ec42..71fd75ced8 100644 --- a/src/quickcontrolsimpl/qquickmnemoniclabel_p.h +++ b/src/quickcontrolsimpl/qquickmnemoniclabel_p.h @@ -41,6 +41,9 @@ public: private: void updateMnemonic(); + // True by default because it's the default for most types like Button: + // only ItemDelegate and its derived types set it to false. Note that underlining + // is controlled separately; see the docs for this property. bool m_mnemonicEnabled = true; QString m_fullText; }; diff --git a/tests/auto/quickcontrols/qquickabstractbutton/tst_qquickabstractbutton.cpp b/tests/auto/quickcontrols/qquickabstractbutton/tst_qquickabstractbutton.cpp index 0eec2ea57b..25e819b17a 100644 --- a/tests/auto/quickcontrols/qquickabstractbutton/tst_qquickabstractbutton.cpp +++ b/tests/auto/quickcontrols/qquickabstractbutton/tst_qquickabstractbutton.cpp @@ -90,28 +90,33 @@ void tst_QQuickAbstractButton::cleanupTestCase() void tst_QQuickAbstractButton::mnemonics_data() { - QTest::addColumn("expectedEnabledByDefault"); + QTest::addColumn("expectEnabledByDefault"); + QTest::addColumn("expectMnemonicStripped"); // The data tag is used as the QML type to instantiate; see mnemonics(). - QTest::newRow("Button") << platformSupportsMnemonics; - QTest::newRow("CheckBox") << platformSupportsMnemonics; - QTest::newRow("MenuItem") << platformSupportsMnemonics; - QTest::newRow("MenuBarItem") << platformSupportsMnemonics; - QTest::newRow("RadioButton") << platformSupportsMnemonics; - QTest::newRow("Switch") << platformSupportsMnemonics; - QTest::newRow("ToolButton") << platformSupportsMnemonics; + // The "&" should always be stripped from the displayed text for Button and its derived types, + // regardless of whether the platform supports mnemonics. For example, on Linux, the & is + // stripped and the next character underlined. On macOS, the & is just stripped. + QTest::newRow("Button") << platformSupportsMnemonics << true; + QTest::newRow("CheckBox") << platformSupportsMnemonics << true; + QTest::newRow("MenuItem") << platformSupportsMnemonics << true; + QTest::newRow("MenuBarItem") << platformSupportsMnemonics << true; + QTest::newRow("RadioButton") << platformSupportsMnemonics << true; + QTest::newRow("Switch") << platformSupportsMnemonics << true; + QTest::newRow("ToolButton") << platformSupportsMnemonics << true; // These types explicitly disable mnemonics regardless of platform. - QTest::newRow("ItemDelegate") << false; - QTest::newRow("CheckDelegate") << false; - QTest::newRow("RadioDelegate") << false; - QTest::newRow("SwipeDelegate") << false; - QTest::newRow("SwitchDelegate") << false; - QTest::newRow("DelayButton") << false; + QTest::newRow("ItemDelegate") << false << false; + QTest::newRow("CheckDelegate") << false << false; + QTest::newRow("RadioDelegate") << false << false; + QTest::newRow("SwipeDelegate") << false << false; + QTest::newRow("SwitchDelegate") << false << false; + QTest::newRow("DelayButton") << false << false; } void tst_QQuickAbstractButton::mnemonics() { - QFETCH(bool, expectedEnabledByDefault); + QFETCH(bool, expectEnabledByDefault); + QFETCH(bool, expectMnemonicStripped); const QString qmlType = QString::fromUtf8(QTest::currentDataTag()); ensureViewVisible(); @@ -145,19 +150,15 @@ void tst_QQuickAbstractButton::mnemonics() QVERIFY(textItem); auto *textItemPrivate = QQuickTextPrivate::get(textItem); - // First, check the defaults. - const bool usesMnemonicLabel = qobject_cast(textItem); - const bool mnemonicStripped = usesMnemonicLabel && expectedEnabledByDefault; - // Whether the mnemonic character is actually underlined additionally depends on - // QPlatformTheme::UnderlineShortcut (which is false for e.g. macOS). - const bool mnemonicUnderlined = mnemonicStripped && platformUnderlinesShortcuts; - // The "&" should always be stripped from the displayed text when mnemonics are enabled, - // even if the platform doesn't underline the shortcut. - QCOMPARE(textItemPrivate->text, mnemonicStripped ? "Mnemonic" : "M&nemonic"); + // "&" is stripped for non-delegate types on every platform; whether the following + // character is underlined depends on QPlatformTheme::UnderlineShortcut. + const bool expectMnemonicUnderlined = expectMnemonicStripped && platformUnderlinesShortcuts; + + QCOMPARE(textItemPrivate->text, expectMnemonicStripped ? "Mnemonic" : "M&nemonic"); // There should only be one QTextLayout::FormatRange applied to the text: the one for the // underline. - QCOMPARE(textItemPrivate->layout.formats().size(), mnemonicUnderlined ? 1 : 0); - if (mnemonicUnderlined) { + QCOMPARE(textItemPrivate->layout.formats().size(), expectMnemonicUnderlined ? 1 : 0); + if (expectMnemonicUnderlined) { const QTextLayout::FormatRange underlineFormatRange = textItemPrivate->layout.formats().constFirst(); QCOMPARE(underlineFormatRange.start, 1); @@ -168,14 +169,14 @@ void tst_QQuickAbstractButton::mnemonics() // Set the text to something else to ensure that it picks up changes. control->setText(QStringLiteral("&Hello")); QCOMPARE(control->text(), QStringLiteral("&Hello")); - QCOMPARE(textItemPrivate->text, mnemonicStripped ? "Hello" : "&Hello"); + QCOMPARE(textItemPrivate->text, expectMnemonicStripped ? "Hello" : "&Hello"); const QSignalSpy clickSpy(control, SIGNAL(clicked())); QVERIFY(clickSpy.isValid()); int expectedClickedCount = 0; // Pressing the shortcut while visible should click the button. - if (expectedEnabledByDefault) + if (expectEnabledByDefault) ++expectedClickedCount; QTest::keyClick(view.get(), Qt::Key_H, Qt::AltModifier); QCOMPARE(clickSpy.count(), expectedClickedCount); @@ -186,7 +187,7 @@ void tst_QQuickAbstractButton::mnemonics() QCOMPARE(clickSpy.count(), expectedClickedCount); // Pressing the shortcut after showing should click the button. - if (expectedEnabledByDefault) + if (expectEnabledByDefault) ++expectedClickedCount; control->setVisible(true); QTest::keyClick(view.get(), Qt::Key_H, Qt::AltModifier); @@ -201,7 +202,7 @@ void tst_QQuickAbstractButton::mnemonics() QCOMPARE(clickSpy.count(), expectedClickedCount); // The new shortcut should. - if (expectedEnabledByDefault) + if (expectEnabledByDefault) ++expectedClickedCount; QTest::keyClick(view.get(), Qt::Key_S, Qt::AltModifier); QCOMPARE(clickSpy.count(), expectedClickedCount); @@ -213,7 +214,7 @@ void tst_QQuickAbstractButton::mnemonics() QCOMPARE(clickSpy.count(), expectedClickedCount); // Restore visibility; should click. - if (expectedEnabledByDefault) + if (expectEnabledByDefault) ++expectedClickedCount; control->setVisible(true); QTest::keyClick(view.get(), Qt::Key_H, Qt::AltModifier); @@ -231,7 +232,7 @@ void tst_QQuickAbstractButton::mnemonics() // Mnemonics in Action text should behave the same as the button's text. int expectedActionTriggeredCount = 0; - if (expectedEnabledByDefault) { + if (expectEnabledByDefault) { ++expectedClickedCount; ++expectedActionTriggeredCount; } -- cgit v1.2.3