From 7bdeea2c309150c8b49558b135232926d6a89c50 Mon Sep 17 00:00:00 2001 From: Aleix Pol Date: Sun, 21 Apr 2024 15:40:40 +0200 Subject: [PATCH] QQuickAbstractButton: setAction will share its Accessible object In cases where a button acts as a view for an action, provide the Action's accessibility information into the visual component so that it can convey the action's information into the accessibility infrastructure. This is done by creating a Accessibility proxy object in the button that will communicate the information from the action's accessibility as part of the button. If the properties of the Accessibility object in the button are modified, these values will prevail. Fixes: QTBUG-123123 Change-Id: Ibeddcc918616d717bb1704177d482ff88bfe99ef Reviewed-by: Mitch Curtis --- src/plugins/qmllint/quick/quicklintplugin.cpp | 2 +- src/quick/items/qquickaccessibleattached.cpp | 87 ++++++++++++++++--- src/quick/items/qquickaccessibleattached_p.h | 26 +++++- src/quicktemplates/qquickabstractbutton.cpp | 8 ++ tests/auto/qml/qmllint/tst_qmllint.cpp | 2 +- .../qquickaccessible/tst_qquickaccessible.cpp | 2 +- .../data/actionAccessibility/button.qml | 12 +++ .../accessibility/tst_accessibility.cpp | 22 +++++ 8 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 tests/auto/quickcontrols/accessibility/data/actionAccessibility/button.qml diff --git a/src/plugins/qmllint/quick/quicklintplugin.cpp b/src/plugins/qmllint/quick/quicklintplugin.cpp index 15b947a10c..52d4d759e3 100644 --- a/src/plugins/qmllint/quick/quicklintplugin.cpp +++ b/src/plugins/qmllint/quick/quicklintplugin.cpp @@ -675,7 +675,7 @@ void QmlLintQuickPlugin::registerPasses(QQmlSA::PassManager *manager, { { "columnWidthProvider", { "", "function" } }, { "rowHeightProvider", { "", "function" } } }); addAttachedWarning({ "QtQuick", "Accessible" }, { { "QtQuick", "Item" } }, - "Accessible must be attached to an Item"); + "Accessible must be attached to an Item or an Action"); addAttachedWarning({ "QtQuick", "LayoutMirroring" }, { { "QtQuick", "Item" }, { "QtQuick", "Window" } }, "LayoutDirection attached property only works with Items and Windows"); diff --git a/src/quick/items/qquickaccessibleattached.cpp b/src/quick/items/qquickaccessibleattached.cpp index 865fb8bf11..547ac72e73 100644 --- a/src/quick/items/qquickaccessibleattached.cpp +++ b/src/quick/items/qquickaccessibleattached.cpp @@ -301,15 +301,19 @@ QQuickAccessibleAttached::QQuickAccessibleAttached(QObject *parent) : QObject(parent), m_role(QAccessible::NoRole) { Q_ASSERT(parent); - if (!item()) { - qmlWarning(parent) << "Accessible must be attached to an Item"; - return; - } - // Enable accessibility for items with accessible content. This also - // enables accessibility for the ancestors of souch items. - item()->d_func()->setAccessible(); - QAccessibleEvent ev(item(), QAccessible::ObjectCreated); + // enables accessibility for the ancestors of such items. + auto item = qobject_cast(parent); + if (item) { + item->d_func()->setAccessible(); + } else { + const QLatin1StringView className(QQmlData::ensurePropertyCache(parent)->firstCppMetaObject()->className()); + if (className != QLatin1StringView("QQuickAction")) { + qmlWarning(parent) << "Accessible must be attached to an Item or an Action"; + return; + } + } + QAccessibleEvent ev(parent, QAccessible::ObjectCreated); QAccessible::updateAccessibility(&ev); if (const QMetaObject *pmo = parent->metaObject()) { @@ -423,13 +427,15 @@ QQuickAccessibleAttached *QQuickAccessibleAttached::qmlAttachedProperties(QObjec bool QQuickAccessibleAttached::ignored() const { - return item() ? !item()->d_func()->isAccessible : false; + auto item = qobject_cast(parent()); + return item ? !item->d_func()->isAccessible : false; } void QQuickAccessibleAttached::setIgnored(bool ignored) { - if (this->ignored() != ignored && item()) { - item()->d_func()->isAccessible = !ignored; + auto item = qobject_cast(parent()); + if (item && this->ignored() != ignored) { + item->d_func()->isAccessible = !ignored; emit ignoredChanged(); } } @@ -457,8 +463,13 @@ bool QQuickAccessibleAttached::doAction(const QString &actionName) sig = &sigPreviousPage; else if (actionName == QAccessibleActionInterface::nextPageAction()) sig = &sigNextPage; - if (sig && isSignalConnected(*sig)) - return sig->invoke(this); + if (sig && isSignalConnected(*sig)) { + bool ret = false; + if (m_proxying) + ret = sig->invoke(m_proxying); + ret |= sig->invoke(this); + return ret; + } return false; } @@ -497,6 +508,56 @@ QString QQuickAccessibleAttached::stripHtml(const QString &html) #endif } +void QQuickAccessibleAttached::setProxying(QQuickAccessibleAttached *proxying) +{ + if (proxying == m_proxying) + return; + + const QMetaObject &mo = staticMetaObject; + if (m_proxying) { + // We disconnect all signals from the proxy into this object + auto mo = m_proxying->metaObject(); + auto propertyCache = QQmlData::ensurePropertyCache(m_proxying); + for (int signalIndex = propertyCache->signalOffset(); + signalIndex < propertyCache->signalCount(); ++signalIndex) { + const QMetaMethod m = mo->method(propertyCache->signal(signalIndex)->coreIndex()); + Q_ASSERT(m.methodType() == QMetaMethod::Signal); + if (m.methodType() != QMetaMethod::Signal) + continue; + + disconnect(m_proxying, m, this, m); + } + } + + m_proxying = proxying; + + if (m_proxying) { + // We connect all signals from the proxy into this object + auto propertyCache = QQmlData::ensurePropertyCache(m_proxying); + auto mo = m_proxying->metaObject(); + for (int signalIndex = propertyCache->signalOffset(); + signalIndex < propertyCache->signalCount(); ++signalIndex) { + const QMetaMethod m = mo->method(propertyCache->signal(signalIndex)->coreIndex()); + Q_ASSERT(m.methodType() == QMetaMethod::Signal); + connect(proxying, m, this, m); + } + } + + // We check all properties + for (int prop = mo.propertyOffset(); prop < mo.propertyCount(); ++prop) { + const QMetaProperty p = mo.property(prop); + if (!p.hasNotifySignal()) { + continue; + } + + const QMetaMethod signal = p.notifySignal(); + if (signal.parameterCount() == 0) + signal.invoke(this); + else + signal.invoke(this, Q_ARG(bool, p.read(this).toBool())); + } +} + QT_END_NAMESPACE #include "moc_qquickaccessibleattached_p.cpp" diff --git a/src/quick/items/qquickaccessibleattached_p.h b/src/quick/items/qquickaccessibleattached_p.h index 92d1307a9a..45c5612854 100644 --- a/src/quick/items/qquickaccessibleattached_p.h +++ b/src/quick/items/qquickaccessibleattached_p.h @@ -30,9 +30,11 @@ QT_BEGIN_NAMESPACE #define STATE_PROPERTY(P) \ Q_PROPERTY(bool P READ P WRITE set_ ## P NOTIFY P ## Changed FINAL) \ - bool P() const { return m_state.P ; } \ + bool P() const { return m_proxying && !m_stateExplicitlySet.P ? m_proxying->P() : m_state.P ; } \ void set_ ## P(bool arg) \ { \ + if (m_proxying) \ + m_proxying->set_##P(arg);\ m_stateExplicitlySet.P = true; \ if (m_state.P == arg) \ return; \ @@ -84,6 +86,8 @@ public: QString name() const { if (m_state.passwordEdit) return QString(); + if (m_proxying) + return m_proxying->name(); return m_name; } @@ -99,9 +103,15 @@ public: } void setNameImplicitly(const QString &name); - QString description() const { return m_description; } + QString description() const { + return !m_descriptionExplicitlySet && m_proxying ? m_proxying->description() : m_description; + } void setDescription(const QString &description) { + if (!m_descriptionExplicitlySet && m_proxying) { + disconnect(m_proxying, &QQuickAccessibleAttached::descriptionChanged, this, &QQuickAccessibleAttached::descriptionChanged); + } + m_descriptionExplicitlySet = true; if (m_description != description) { m_description = description; Q_EMIT descriptionChanged(); @@ -143,6 +153,13 @@ public: if (att && (role == QAccessible::NoRole || att->role() == role)) { break; } + if (auto action = object->property("action").value(); action) { + QQuickAccessibleAttached *att = QQuickAccessibleAttached::attachedProperties(action); + if (att && (role == QAccessible::NoRole || att->role() == role)) { + object = action; + break; + } + } object = object->parent(); } return object; @@ -154,6 +171,7 @@ public: void availableActions(QStringList *actions) const; Q_REVISION(6, 2) Q_INVOKABLE static QString stripHtml(const QString &html); + void setProxying(QQuickAccessibleAttached *proxying); public Q_SLOTS: void valueChanged() { @@ -184,14 +202,14 @@ Q_SIGNALS: void nextPageAction(); private: - QQuickItem *item() const { return qobject_cast(parent()); } - QAccessible::Role m_role; QAccessible::State m_state; QAccessible::State m_stateExplicitlySet; QString m_name; bool m_nameExplicitlySet = false; QString m_description; + bool m_descriptionExplicitlySet = false; + QQuickAccessibleAttached* m_proxying = nullptr; static QMetaMethod sigPress; static QMetaMethod sigToggle; diff --git a/src/quicktemplates/qquickabstractbutton.cpp b/src/quicktemplates/qquickabstractbutton.cpp index 0c1af73e55..d244685011 100644 --- a/src/quicktemplates/qquickabstractbutton.cpp +++ b/src/quicktemplates/qquickabstractbutton.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include QT_BEGIN_NAMESPACE @@ -889,6 +891,12 @@ void QQuickAbstractButton::setAction(QQuickAction *action) setEnabled(action->isEnabled()); } +#if QT_CONFIG(accessibility) + auto attached = qobject_cast(qmlAttachedPropertiesObject(this, true)); + Q_ASSERT(attached); + attached->setProxying(qobject_cast(qmlAttachedPropertiesObject(action, true))); +#endif + d->action = action; if (oldText != text()) diff --git a/tests/auto/qml/qmllint/tst_qmllint.cpp b/tests/auto/qml/qmllint/tst_qmllint.cpp index 1ae90d62c7..6bd4d6c90a 100644 --- a/tests/auto/qml/qmllint/tst_qmllint.cpp +++ b/tests/auto/qml/qmllint/tst_qmllint.cpp @@ -2124,7 +2124,7 @@ void TestQmllint::quickPlugin() Message { u"SplitView attached property only works with Items"_s }, Message { u"ScrollIndicator must be attached to a Flickable"_s }, Message { u"ScrollBar must be attached to a Flickable or ScrollView"_s }, - Message { u"Accessible must be attached to an Item"_s }, + Message { u"Accessible must be attached to an Item or an Action"_s }, Message { u"EnterKey attached property only works with Items"_s }, Message { u"LayoutDirection attached property only works with Items and Windows"_s }, diff --git a/tests/auto/quick/qquickaccessible/tst_qquickaccessible.cpp b/tests/auto/quick/qquickaccessible/tst_qquickaccessible.cpp index e164d89217..6a282d4b41 100644 --- a/tests/auto/quick/qquickaccessible/tst_qquickaccessible.cpp +++ b/tests/auto/quick/qquickaccessible/tst_qquickaccessible.cpp @@ -161,7 +161,7 @@ void tst_QQuickAccessible::quickAttachedProperties() // Attaching to non-item { QObject parent; - QTest::ignoreMessage(QtWarningMsg, ": QML QtObject: Accessible must be attached to an Item"); + QTest::ignoreMessage(QtWarningMsg, ": QML QtObject: Accessible must be attached to an Item or an Action"); QQuickAccessibleAttached *attachedObj = new QQuickAccessibleAttached(&parent); QCOMPARE(attachedObj->ignored(), false); diff --git a/tests/auto/quickcontrols/accessibility/data/actionAccessibility/button.qml b/tests/auto/quickcontrols/accessibility/data/actionAccessibility/button.qml new file mode 100644 index 0000000000..7e392e9cc3 --- /dev/null +++ b/tests/auto/quickcontrols/accessibility/data/actionAccessibility/button.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Controls + +Button { + action: Action { + id: anAction + text: "Peaches" + Accessible.name: "Peach" + Accessible.description: "Show peaches some love" + } + text: Accessible.description +} diff --git a/tests/auto/quickcontrols/accessibility/tst_accessibility.cpp b/tests/auto/quickcontrols/accessibility/tst_accessibility.cpp index 9774bf4e07..8bdd9453c8 100644 --- a/tests/auto/quickcontrols/accessibility/tst_accessibility.cpp +++ b/tests/auto/quickcontrols/accessibility/tst_accessibility.cpp @@ -31,6 +31,8 @@ private slots: void override(); void ordering(); + + void actionAccessibility(); private: QQmlEngine engine; }; @@ -274,6 +276,26 @@ void tst_accessibility::ordering() #endif } +void tst_accessibility::actionAccessibility() +{ +#if QT_CONFIG(accessibility) + QQmlComponent component(&engine); + component.loadUrl(testFileUrl("actionAccessibility/button.qml")); + + QScopedPointer object(component.create()); + QVERIFY2(!object.isNull(), qPrintable(component.errorString())); + + QQuickItem *item = qobject_cast(object.data()); + QVERIFY(item); + const QString description = "Show peaches some love"; + QCOMPARE(item->property("text"), description); + QAccessibleInterface *iface = QAccessible::queryAccessibleInterface(item); + QVERIFY(iface); + QCOMPARE(iface->text(QAccessible::Name), "Peach"); + QCOMPARE(iface->text(QAccessible::Description), description); +#endif +} + QTEST_MAIN(tst_accessibility) #include "tst_accessibility.moc"