Add Identifier role to QAccessible and use it in OS interfaces

* Unify the default identifier creation for QAccessibleInterface on
  all platforms to be the same as the previous identifier on Linux.
  This may change some identifiers on Windows.

[ChangeLog][QAccessible][QAccessibleInterface] Add possibility to
add unique identifier to QAccessibleInterface to give a11y elements
consistent identifiers.

Task-number: QTBUG-123361
Change-Id: I8c42956a4c497e71909d71dcb27bc87433937b69
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
bb10
Jens Trillmann 2024-03-13 14:00:38 +01:00
parent 3c93dedc06
commit 9ec1de2528
11 changed files with 74 additions and 50 deletions

View File

@ -1352,6 +1352,7 @@ void AtSpiAdaptor::notify(QAccessibleEvent *event)
case QAccessible::HelpChanged:
case QAccessible::DefaultActionChanged:
case QAccessible::AcceleratorChanged:
case QAccessible::IdentifierChanged:
case QAccessible::InvalidEvent:
break;
}
@ -1555,26 +1556,6 @@ void AtSpiAdaptor::registerApplication()
delete registry;
}
namespace {
QString accessibleIdForAccessible(QAccessibleInterface *accessible)
{
QString result;
while (accessible) {
if (!result.isEmpty())
result.prepend(u'.');
if (auto obj = accessible->object()) {
const QString name = obj->objectName();
if (!name.isEmpty())
result.prepend(name);
else
result.prepend(QString::fromUtf8(obj->metaObject()->className()));
}
accessible = accessible->parent();
}
return result;
}
} // namespace
// Accessible
bool AtSpiAdaptor::accessibleInterface(QAccessibleInterface *interface, const QString &function, const QDBusMessage &message, const QDBusConnection &connection)
{
@ -1665,7 +1646,7 @@ bool AtSpiAdaptor::accessibleInterface(QAccessibleInterface *interface, const QS
connection.send(message.createReply(QVariant::fromValue(children)));
} else if (function == "GetAccessibleId"_L1) {
sendReply(connection, message,
QVariant::fromValue(QDBusVariant(accessibleIdForAccessible(interface))));
QVariant::fromValue(QDBusVariant(QAccessibleBridgeUtils::accessibleId(interface))));
} else {
qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::accessibleInterface does not implement" << function << message.path();
return false;

View File

@ -206,6 +206,7 @@ Q_LOGGING_CATEGORY(lcAccessibilityCore, "qt.accessibility.core");
clicked or via a key press.
\value HypertextLinkSelected A hypertext link has been selected.
\value HypertextNLinksChanged
\value [since 6.8] IdentifierChanged The identifier of an object has changed.
\value LocationChanged An object's location on the screen has changed.
\value MenuCommand A menu item is triggered.
\value MenuEnd A menu has been closed (Qt uses PopupMenuEnd for all
@ -389,14 +390,15 @@ Q_LOGGING_CATEGORY(lcAccessibilityCore, "qt.accessibility.core");
This enum specifies string information that an accessible object
returns.
\value Name The name of the object. This can be used both
as an identifier or a short description by
accessible clients.
\value Description A short text describing the object.
\value Value The value of the object.
\value Help A longer text giving information about how to use the object.
\value Accelerator The keyboard shortcut that executes the object's default action.
\value UserText The first value to be used for user defined text.
\value Name The name of the object. This can be used both
as an identifier or a short description by
accessible clients.
\value Description A short text describing the object.
\value Value The value of the object.
\value Help A longer text giving information about how to use the object.
\value Accelerator The keyboard shortcut that executes the object's default action.
\value UserText The first value to be used for user defined text.
\value [since 6.8] Identifier An identifier for the object for e.g. UI tests.
\omitvalue DebugDescription
*/
@ -1249,6 +1251,13 @@ QAccessibleInterface *QAccessibleInterface::focusChild() const
tool buttons also have shortcut keys and usually display them in
their tooltip.
The \l QAccessible::Identifier can be explicitly set to provide an
ID to assistive technologies. This can be especially useful for UI tests.
If no identifier has been explicitly set, the identifier is set by the
respective interface to an ID based on \l QObject::objectName or its
class name and \l QObject::objectName or class name of the parents
in its parents chain.
All objects provide a string for \l QAccessible::Name.
\sa role(), state()

View File

@ -102,6 +102,7 @@ public:
DefaultActionChanged = 0x80B0,
AcceleratorChanged = 0x80C0,
Announcement = 0x80D0,
IdentifierChanged = 0x80E0,
InvalidEvent
};
@ -324,6 +325,7 @@ public:
Help,
Accelerator,
DebugDescription,
Identifier,
UserText = 0x0000ffff
};

View File

@ -72,6 +72,28 @@ bool performEffectiveAction(QAccessibleInterface *iface, const QString &actionNa
return true;
}
QString accessibleId(QAccessibleInterface *accessible) {
QString result;
if (!accessible)
return result;
result = accessible->text(QAccessible::Identifier);
if (!result.isEmpty())
return result;
while (accessible) {
if (!result.isEmpty())
result.prepend(u'.');
if (auto obj = accessible->object()) {
const QString name = obj->objectName();
if (!name.isEmpty())
result.prepend(name);
else
result.prepend(QString::fromUtf8(obj->metaObject()->className()));
}
accessible = accessible->parent();
}
return result;
}
} //namespace
QT_END_NAMESPACE

View File

@ -17,6 +17,7 @@
#include <QtGui/private/qtguiglobal_p.h>
#include <QtCore/qstring.h>
#include <QtCore/qstringlist.h>
#include <QtGui/qaccessible.h>
@ -27,6 +28,7 @@ QT_BEGIN_NAMESPACE
namespace QAccessibleBridgeUtils {
Q_GUI_EXPORT QStringList effectiveActionNames(QAccessibleInterface *iface);
Q_GUI_EXPORT bool performEffectiveAction(QAccessibleInterface *iface, const QString &actionName);
Q_GUI_EXPORT QString accessibleId(QAccessibleInterface *accessible);
}
QT_END_NAMESPACE

View File

@ -470,6 +470,7 @@ namespace QtAndroidAccessibility
QAccessible::Role role;
QStringList actions;
QString description;
QString identifier;
bool hasTextSelection = false;
int selectionStart = 0;
int selectionEnd = 0;
@ -485,6 +486,7 @@ namespace QtAndroidAccessibility
info.role = iface->role();
info.actions = QAccessibleBridgeUtils::effectiveActionNames(iface);
info.description = descriptionForInterface(iface);
info.identifier = QAccessibleBridgeUtils::accessibleId(iface);
QAccessibleTextInterface *textIface = iface->textInterface();
if (textIface && (textIface->selectionCount() > 0)) {
info.hasTextSelection = true;
@ -550,6 +552,8 @@ namespace QtAndroidAccessibility
//CALL_METHOD(node, "setText", "(Ljava/lang/CharSequence;)V", jdesc)
env->CallVoidMethod(node, m_setContentDescriptionMethodID, jdesc);
QJniObject(node).callMethod<void>("setViewIdResourceName", info.identifier);
return true;
}

View File

@ -520,6 +520,12 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of
return nil;
}
- (NSString*) accessibilityIdentifier {
if (QAccessibleInterface *iface = self.qtInterface)
return QAccessibleBridgeUtils::accessibleId(iface).toNSString();
return nil;
}
- (BOOL) isAccessibilityEnabled {
if (QAccessibleInterface *iface = self.qtInterface)
return !iface->state().disabled;

View File

@ -10,6 +10,8 @@
#include "uistrings_p.h"
#include "qioswindow.h"
#include <QtGui/private/qaccessiblebridgeutils_p.h>
QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAccessibilityElement);
@implementation QMacAccessibilityElement
@ -70,6 +72,17 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAccessibilityElement);
return iface->text(QAccessible::Name).toNSString();
}
- (NSString*)accessibilityIdentifier
{
QAccessibleInterface *iface = QAccessible::accessibleInterface(self.axid);
if (!iface) {
qWarning() << "invalid accessible interface for: " << self.axid;
return @"";
}
return QAccessibleBridgeUtils::accessibleId(iface).toNSString();
}
- (NSString*)accessibilityHint
{
QAccessibleInterface *iface = QAccessible::accessibleInterface(self.axid);

View File

@ -284,6 +284,10 @@ emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *ifac
element = document.call<emscripten::val>("createElement", std::string("div"));
}
QString id = QAccessibleBridgeUtils::accessibleId(iface);
if (iface->role() != QAccessible::PageTabList)
element.call<void>("setAttribute", std::string("id"), id.toStdString());
return element;
}();

View File

@ -23,6 +23,7 @@
#include "qwindowsuiaprovidercache.h"
#include <QtCore/qloggingcategory.h>
#include <QtGui/private/qaccessiblebridgeutils_p.h>
#include <QtGui/qaccessible.h>
#include <QtGui/qguiapplication.h>
#include <QtGui/qwindow.h>
@ -503,7 +504,7 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR
break;
case UIA_AutomationIdPropertyId:
// Automation ID, which can be used by tools to select a specific control in the UI.
setVariantString(automationIdForAccessible(accessible), pRetVal);
setVariantString(QAccessibleBridgeUtils::accessibleId(accessible), pRetVal);
break;
case UIA_ClassNamePropertyId:
// Class name.
@ -610,25 +611,6 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR
return S_OK;
}
// Generates an ID based on the name of the controls and their parents.
QString QWindowsUiaMainProvider::automationIdForAccessible(const QAccessibleInterface *accessible)
{
QString result;
if (accessible) {
QObject *obj = accessible->object();
while (obj) {
QString name = obj->objectName();
if (name.isEmpty())
return result;
if (!result.isEmpty())
result.prepend(u'.');
result.prepend(name);
obj = obj->parent();
}
}
return result;
}
HRESULT QWindowsUiaMainProvider::get_HostRawElementProvider(IRawElementProviderSimple **pRetVal)
{
qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this;

View File

@ -59,7 +59,6 @@ public:
HRESULT STDMETHODCALLTYPE GetFocus(IRawElementProviderFragment **pRetVal) override;
private:
QString automationIdForAccessible(const QAccessibleInterface *accessible);
static void fillVariantArrayForRelation(QAccessibleInterface *accessible, QAccessible::Relation relation, VARIANT *pRetVal);
static void setAriaProperties(QAccessibleInterface *accessible, VARIANT *pRetVal);
static void setStyle(QAccessibleInterface *accessible, VARIANT *pRetVal);