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 <mitch.curtis@qt.io>
This commit is contained in:
Aleix Pol 2024-04-21 15:40:40 +02:00 committed by Mitch Curtis
parent f9f9480746
commit 7bdeea2c30
8 changed files with 141 additions and 20 deletions

View File

@ -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");

View File

@ -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<QQuickItem *>(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<QQuickItem *>(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<QQuickItem *>(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"

View File

@ -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<QObject *>(); 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<QQuickItem*>(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;

View File

@ -18,6 +18,8 @@
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/qpa/qplatformtheme.h>
#include <QtQuick/private/qquickevents_p_p.h>
#include <QtQuick/private/qquickevents_p_p.h>
#include <QtQuick/private/qquickaccessibleattached_p.h>
#include <QtQml/qqmllist.h>
QT_BEGIN_NAMESPACE
@ -889,6 +891,12 @@ void QQuickAbstractButton::setAction(QQuickAction *action)
setEnabled(action->isEnabled());
}
#if QT_CONFIG(accessibility)
auto attached = qobject_cast<QQuickAccessibleAttached*>(qmlAttachedPropertiesObject<QQuickAccessibleAttached>(this, true));
Q_ASSERT(attached);
attached->setProxying(qobject_cast<QQuickAccessibleAttached*>(qmlAttachedPropertiesObject<QQuickAccessibleAttached>(action, true)));
#endif
d->action = action;
if (oldText != text())

View File

@ -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 },

View File

@ -161,7 +161,7 @@ void tst_QQuickAccessible::quickAttachedProperties()
// Attaching to non-item
{
QObject parent;
QTest::ignoreMessage(QtWarningMsg, "<Unknown File>: QML QtObject: Accessible must be attached to an Item");
QTest::ignoreMessage(QtWarningMsg, "<Unknown File>: QML QtObject: Accessible must be attached to an Item or an Action");
QQuickAccessibleAttached *attachedObj = new QQuickAccessibleAttached(&parent);
QCOMPARE(attachedObj->ignored(), false);

View File

@ -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
}

View File

@ -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<QObject> object(component.create());
QVERIFY2(!object.isNull(), qPrintable(component.errorString()));
QQuickItem *item = qobject_cast<QQuickItem *>(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"