// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "atspiadaptor_p.h" #include "qspiaccessiblebridge_p.h" #include #include #include #include #include #include #include #if QT_CONFIG(accessibility) #include "socket_interface.h" #include "qspi_constant_mappings_p.h" #include #include #include "qspiapplicationadaptor_p.h" /*! \class AtSpiAdaptor \internal \brief AtSpiAdaptor is the main class to forward between QAccessibleInterface and AT-SPI DBus AtSpiAdaptor implements the functions specified in all at-spi interfaces. It sends notifications coming from Qt via dbus and listens to incoming dbus requests. */ // ATSPI_COORD_TYPE_PARENT was added in at-spi 2.30, define here for older versions #if ATSPI_COORD_TYPE_COUNT < 3 #define ATSPI_COORD_TYPE_PARENT 2 #endif // ATSPI_*_VERSION defines were added in libatspi 2.50, // as was the AtspiLive enum; define values here for older versions #if !defined(ATSPI_MAJOR_VERSION) || !defined(ATSPI_MINOR_VERSION) || ATSPI_MAJOR_VERSION < 2 || ATSPI_MINOR_VERSION < 50 #define ATSPI_LIVE_POLITE 1 #define ATSPI_LIVE_ASSERTIVE 2 #endif QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; Q_LOGGING_CATEGORY(lcAccessibilityAtspi, "qt.accessibility.atspi") Q_LOGGING_CATEGORY(lcAccessibilityAtspiCreation, "qt.accessibility.atspi.creation") AtSpiAdaptor::AtSpiAdaptor(DBusConnection *connection, QObject *parent) : QDBusVirtualObject(parent), m_dbus(connection) , sendFocus(0) , sendObject(0) , sendObject_active_descendant_changed(0) , sendObject_announcement(0) , sendObject_attributes_changed(0) , sendObject_bounds_changed(0) , sendObject_children_changed(0) // , sendObject_children_changed_add(0) // , sendObject_children_changed_remove(0) , sendObject_column_deleted(0) , sendObject_column_inserted(0) , sendObject_column_reordered(0) , sendObject_link_selected(0) , sendObject_model_changed(0) , sendObject_property_change(0) , sendObject_property_change_accessible_description(0) , sendObject_property_change_accessible_name(0) , sendObject_property_change_accessible_parent(0) , sendObject_property_change_accessible_role(0) , sendObject_property_change_accessible_table_caption(0) , sendObject_property_change_accessible_table_column_description(0) , sendObject_property_change_accessible_table_column_header(0) , sendObject_property_change_accessible_table_row_description(0) , sendObject_property_change_accessible_table_row_header(0) , sendObject_property_change_accessible_table_summary(0) , sendObject_property_change_accessible_value(0) , sendObject_row_deleted(0) , sendObject_row_inserted(0) , sendObject_row_reordered(0) , sendObject_selection_changed(0) , sendObject_state_changed(0) , sendObject_text_attributes_changed(0) , sendObject_text_bounds_changed(0) , sendObject_text_caret_moved(0) , sendObject_text_changed(0) // , sendObject_text_changed_delete(0) // , sendObject_text_changed_insert(0) , sendObject_text_selection_changed(0) , sendObject_value_changed(0) , sendObject_visible_data_changed(0) , sendWindow(0) , sendWindow_activate(0) , sendWindow_close(0) , sendWindow_create(0) , sendWindow_deactivate(0) // , sendWindow_desktop_create(0) // , sendWindow_desktop_destroy(0) , sendWindow_lower(0) , sendWindow_maximize(0) , sendWindow_minimize(0) , sendWindow_move(0) , sendWindow_raise(0) , sendWindow_reparent(0) , sendWindow_resize(0) , sendWindow_restore(0) , sendWindow_restyle(0) , sendWindow_shade(0) , sendWindow_unshade(0) { m_applicationAdaptor = new QSpiApplicationAdaptor(m_dbus->connection(), this); connect(m_applicationAdaptor, SIGNAL(windowActivated(QObject*,bool)), this, SLOT(windowActivated(QObject*,bool))); updateEventListeners(); bool success = m_dbus->connection().connect("org.a11y.atspi.Registry"_L1, "/org/a11y/atspi/registry"_L1, "org.a11y.atspi.Registry"_L1, "EventListenerRegistered"_L1, this, SLOT(eventListenerRegistered(QString,QString))); success = success && m_dbus->connection().connect("org.a11y.atspi.Registry"_L1, "/org/a11y/atspi/registry"_L1, "org.a11y.atspi.Registry"_L1, "EventListenerDeregistered"_L1, this, SLOT(eventListenerDeregistered(QString,QString))); } AtSpiAdaptor::~AtSpiAdaptor() { } /*! Provide DBus introspection. */ QString AtSpiAdaptor::introspect(const QString &path) const { static const QLatin1StringView accessibleIntrospection( " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" ); static const QLatin1StringView actionIntrospection( " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" ); static const QLatin1StringView applicationIntrospection( " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" ); static const QLatin1StringView componentIntrospection( " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" ); static const QLatin1StringView editableTextIntrospection( " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" ); static const QLatin1StringView selectionIntrospection( " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" ); static const QLatin1StringView tableIntrospection( " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" ); static const QLatin1StringView tableCellIntrospection( " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" ); static const QLatin1StringView textIntrospection( " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" ); static const QLatin1StringView valueIntrospection( " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" ); QAccessibleInterface * interface = interfaceFromPath(path); if (!interface) { qCWarning(lcAccessibilityAtspi) << "Could not find accessible on path:" << path; return QString(); } QStringList interfaces = accessibleInterfaces(interface); QString xml; xml.append(accessibleIntrospection); if (interfaces.contains(ATSPI_DBUS_INTERFACE_COMPONENT ""_L1)) xml.append(componentIntrospection); if (interfaces.contains(ATSPI_DBUS_INTERFACE_TEXT ""_L1)) xml.append(textIntrospection); if (interfaces.contains(ATSPI_DBUS_INTERFACE_EDITABLE_TEXT ""_L1)) xml.append(editableTextIntrospection); if (interfaces.contains(ATSPI_DBUS_INTERFACE_ACTION ""_L1)) xml.append(actionIntrospection); if (interfaces.contains(ATSPI_DBUS_INTERFACE_SELECTION ""_L1)) xml.append(selectionIntrospection); if (interfaces.contains(ATSPI_DBUS_INTERFACE_TABLE ""_L1)) xml.append(tableIntrospection); if (interfaces.contains(ATSPI_DBUS_INTERFACE_TABLE_CELL ""_L1)) xml.append(tableCellIntrospection); if (interfaces.contains(ATSPI_DBUS_INTERFACE_VALUE ""_L1)) xml.append(valueIntrospection); if (path == QSPI_OBJECT_PATH_ROOT ""_L1) xml.append(applicationIntrospection); return xml; } void AtSpiAdaptor::setBitFlag(const QString &flag) { Q_ASSERT(flag.size()); // assume we don't get nonsense - look at first letter only switch (flag.at(0).toLower().toLatin1()) { case 'o': { if (flag.size() <= 8) { // Object:: sendObject = 1; } else { // Object:Foo:Bar QString right = flag.mid(7); if (false) { } else if (right.startsWith("ActiveDescendantChanged"_L1)) { sendObject_active_descendant_changed = 1; } else if (right.startsWith("Announcement"_L1)) { sendObject_announcement = 1; } else if (right.startsWith("AttributesChanged"_L1)) { sendObject_attributes_changed = 1; } else if (right.startsWith("BoundsChanged"_L1)) { sendObject_bounds_changed = 1; } else if (right.startsWith("ChildrenChanged"_L1)) { sendObject_children_changed = 1; } else if (right.startsWith("ColumnDeleted"_L1)) { sendObject_column_deleted = 1; } else if (right.startsWith("ColumnInserted"_L1)) { sendObject_column_inserted = 1; } else if (right.startsWith("ColumnReordered"_L1)) { sendObject_column_reordered = 1; } else if (right.startsWith("LinkSelected"_L1)) { sendObject_link_selected = 1; } else if (right.startsWith("ModelChanged"_L1)) { sendObject_model_changed = 1; } else if (right.startsWith("PropertyChange"_L1)) { if (right == "PropertyChange:AccessibleDescription"_L1) { sendObject_property_change_accessible_description = 1; } else if (right == "PropertyChange:AccessibleName"_L1) { sendObject_property_change_accessible_name = 1; } else if (right == "PropertyChange:AccessibleParent"_L1) { sendObject_property_change_accessible_parent = 1; } else if (right == "PropertyChange:AccessibleRole"_L1) { sendObject_property_change_accessible_role = 1; } else if (right == "PropertyChange:TableCaption"_L1) { sendObject_property_change_accessible_table_caption = 1; } else if (right == "PropertyChange:TableColumnDescription"_L1) { sendObject_property_change_accessible_table_column_description = 1; } else if (right == "PropertyChange:TableColumnHeader"_L1) { sendObject_property_change_accessible_table_column_header = 1; } else if (right == "PropertyChange:TableRowDescription"_L1) { sendObject_property_change_accessible_table_row_description = 1; } else if (right == "PropertyChange:TableRowHeader"_L1) { sendObject_property_change_accessible_table_row_header = 1; } else if (right == "PropertyChange:TableSummary"_L1) { sendObject_property_change_accessible_table_summary = 1; } else if (right == "PropertyChange:AccessibleValue"_L1) { sendObject_property_change_accessible_value = 1; } else { sendObject_property_change = 1; } } else if (right.startsWith("RowDeleted"_L1)) { sendObject_row_deleted = 1; } else if (right.startsWith("RowInserted"_L1)) { sendObject_row_inserted = 1; } else if (right.startsWith("RowReordered"_L1)) { sendObject_row_reordered = 1; } else if (right.startsWith("SelectionChanged"_L1)) { sendObject_selection_changed = 1; } else if (right.startsWith("StateChanged"_L1)) { sendObject_state_changed = 1; } else if (right.startsWith("TextAttributesChanged"_L1)) { sendObject_text_attributes_changed = 1; } else if (right.startsWith("TextBoundsChanged"_L1)) { sendObject_text_bounds_changed = 1; } else if (right.startsWith("TextCaretMoved"_L1)) { sendObject_text_caret_moved = 1; } else if (right.startsWith("TextChanged"_L1)) { sendObject_text_changed = 1; } else if (right.startsWith("TextSelectionChanged"_L1)) { sendObject_text_selection_changed = 1; } else if (right.startsWith("ValueChanged"_L1)) { sendObject_value_changed = 1; } else if (right.startsWith("VisibleDataChanged"_L1) || right.startsWith("VisibledataChanged"_L1)) { // typo in libatspi sendObject_visible_data_changed = 1; } else { qCWarning(lcAccessibilityAtspi) << "Subscription string not handled:" << flag; } } break; } case 'w': { // window if (flag.size() <= 8) { sendWindow = 1; } else { // object:Foo:Bar QString right = flag.mid(7); if (false) { } else if (right.startsWith("Activate"_L1)) { sendWindow_activate = 1; } else if (right.startsWith("Close"_L1)) { sendWindow_close= 1; } else if (right.startsWith("Create"_L1)) { sendWindow_create = 1; } else if (right.startsWith("Deactivate"_L1)) { sendWindow_deactivate = 1; } else if (right.startsWith("Lower"_L1)) { sendWindow_lower = 1; } else if (right.startsWith("Maximize"_L1)) { sendWindow_maximize = 1; } else if (right.startsWith("Minimize"_L1)) { sendWindow_minimize = 1; } else if (right.startsWith("Move"_L1)) { sendWindow_move = 1; } else if (right.startsWith("Raise"_L1)) { sendWindow_raise = 1; } else if (right.startsWith("Reparent"_L1)) { sendWindow_reparent = 1; } else if (right.startsWith("Resize"_L1)) { sendWindow_resize = 1; } else if (right.startsWith("Restore"_L1)) { sendWindow_restore = 1; } else if (right.startsWith("Restyle"_L1)) { sendWindow_restyle = 1; } else if (right.startsWith("Shade"_L1)) { sendWindow_shade = 1; } else if (right.startsWith("Unshade"_L1)) { sendWindow_unshade = 1; } else if (right.startsWith("DesktopCreate"_L1)) { // ignore this one } else if (right.startsWith("DesktopDestroy"_L1)) { // ignore this one } else { qCWarning(lcAccessibilityAtspi) << "Subscription string not handled:" << flag; } } break; } case 'f': { sendFocus = 1; break; } case 'd': { // document is not implemented break; } case 't': { // terminal is not implemented break; } case 'm': { // mouse* is handled in a different way by the gnome atspi stack break; } default: qCWarning(lcAccessibilityAtspi) << "Subscription string not handled:" << flag; } } /*! Checks via dbus which events should be sent. */ void AtSpiAdaptor::updateEventListeners() { QDBusMessage m = QDBusMessage::createMethodCall("org.a11y.atspi.Registry"_L1, "/org/a11y/atspi/registry"_L1, "org.a11y.atspi.Registry"_L1, "GetRegisteredEvents"_L1); QDBusReply listenersReply = m_dbus->connection().call(m); if (listenersReply.isValid()) { const QSpiEventListenerArray evList = listenersReply.value(); for (const QSpiEventListener &ev : evList) setBitFlag(ev.eventName); m_applicationAdaptor->sendEvents(!evList.isEmpty()); } else { qCDebug(lcAccessibilityAtspi) << "Could not query active accessibility event listeners."; } } void AtSpiAdaptor::eventListenerDeregistered(const QString &/*bus*/, const QString &/*path*/) { // qCDebug(lcAccessibilityAtspi) << "AtSpiAdaptor::eventListenerDeregistered: " << bus << path; updateEventListeners(); } void AtSpiAdaptor::eventListenerRegistered(const QString &/*bus*/, const QString &/*path*/) { // qCDebug(lcAccessibilityAtspi) << "AtSpiAdaptor::eventListenerRegistered: " << bus << path; updateEventListeners(); } /*! This slot needs to get called when a \a window has be activated or deactivated (become focused). When \a active is true, the window just received focus, otherwise it lost the focus. */ void AtSpiAdaptor::windowActivated(QObject* window, bool active) { if (!(sendWindow || sendWindow_activate)) return; QAccessibleInterface *iface = QAccessible::queryAccessibleInterface(window); // If the window has been quickly activated or disabled, it will cause a crash. if (iface == nullptr) return; Q_ASSERT(!active || iface->isValid()); QString windowTitle; // in dtor it may be invalid if (iface->isValid()) windowTitle = iface->text(QAccessible::Name); QDBusVariant data; data.setVariant(windowTitle); QVariantList args = packDBusSignalArguments(QString(), 0, 0, QVariant::fromValue(data)); QString status = active ? "Activate"_L1 : "Deactivate"_L1; QString path = pathForObject(window); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_WINDOW ""_L1, status, args); QVariantList stateArgs = packDBusSignalArguments("active"_L1, active ? 1 : 0, 0, variantForPath(path)); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "StateChanged"_L1, stateArgs); } QVariantList AtSpiAdaptor::packDBusSignalArguments(const QString &type, int data1, int data2, const QVariant &variantData) const { QVariantList arguments; arguments << type << data1 << data2 << variantData << QVariant::fromValue(QSpiObjectReference(m_dbus->connection(), QDBusObjectPath(QSPI_OBJECT_PATH_ROOT))); return arguments; } QVariant AtSpiAdaptor::variantForPath(const QString &path) const { QDBusVariant data; data.setVariant(QVariant::fromValue(QSpiObjectReference(m_dbus->connection(), QDBusObjectPath(path)))); return QVariant::fromValue(data); } bool AtSpiAdaptor::sendDBusSignal(const QString &path, const QString &interface, const QString &signalName, const QVariantList &arguments) const { QDBusMessage message = QDBusMessage::createSignal(path, interface, signalName); message.setArguments(arguments); return m_dbus->connection().send(message); } QAccessibleInterface *AtSpiAdaptor::interfaceFromPath(const QString& dbusPath) const { if (dbusPath == QSPI_OBJECT_PATH_ROOT ""_L1) return QAccessible::queryAccessibleInterface(qApp); QStringList parts = dbusPath.split(u'/'); if (parts.size() != 6) { qCDebug(lcAccessibilityAtspi) << "invalid path: " << dbusPath; return nullptr; } QString objectString = parts.at(5); QAccessible::Id id = objectString.toUInt(); // The id is always in the range [INT_MAX+1, UINT_MAX] if ((int)id >= 0) qCWarning(lcAccessibilityAtspi) << "No accessible object found for id: " << id; return QAccessible::accessibleInterface(id); } void AtSpiAdaptor::notifyStateChange(QAccessibleInterface *interface, const QString &state, int value) { QString path = pathForInterface(interface); QVariantList stateArgs = packDBusSignalArguments(state, value, 0, variantForPath(path)); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "StateChanged"_L1, stateArgs); } void AtSpiAdaptor::sendAnnouncement(QAccessibleAnnouncementEvent *event) { QAccessibleInterface *iface = event->accessibleInterface(); if (!iface) { qCWarning(lcAccessibilityAtspi, "Announcement event has no accessible set."); return; } if (!iface->isValid()) { qCWarning(lcAccessibilityAtspi) << "Announcement event with invalid accessible: " << iface; return; } const QString path = pathForInterface(iface); const QString message = event->message(); const QAccessible::AnnouncementPriority prio = event->priority(); const int politeness = (prio == QAccessible::AnnouncementPriority::Assertive) ? ATSPI_LIVE_ASSERTIVE : ATSPI_LIVE_POLITE; const QVariantList args = packDBusSignalArguments(QString(), politeness, 0, QVariant::fromValue(QDBusVariant(message))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "Announcement"_L1, args); } /*! This function gets called when Qt notifies about accessibility updates. */ void AtSpiAdaptor::notify(QAccessibleEvent *event) { switch (event->type()) { case QAccessible::ObjectCreated: if (sendObject || sendObject_children_changed) notifyAboutCreation(event->accessibleInterface()); break; case QAccessible::ObjectShow: { if (sendObject || sendObject_state_changed) { notifyStateChange(event->accessibleInterface(), "showing"_L1, 1); } break; } case QAccessible::ObjectHide: { if (sendObject || sendObject_state_changed) { notifyStateChange(event->accessibleInterface(), "showing"_L1, 0); } break; } case QAccessible::ObjectDestroyed: { if (sendObject || sendObject_state_changed) notifyAboutDestruction(event->accessibleInterface()); break; } case QAccessible::ObjectReorder: { if (sendObject || sendObject_children_changed) childrenChanged(event->accessibleInterface()); break; } case QAccessible::NameChanged: { if (sendObject || sendObject_property_change || sendObject_property_change_accessible_name) { QAccessibleInterface *iface = event->accessibleInterface(); if (!iface) { qCDebug(lcAccessibilityAtspi, "NameChanged event from invalid accessible."); return; } QString path = pathForInterface(iface); QVariantList args = packDBusSignalArguments( "accessible-name"_L1, 0, 0, QVariant::fromValue(QDBusVariant(iface->text(QAccessible::Name)))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "PropertyChange"_L1, args); } break; } case QAccessible::DescriptionChanged: { if (sendObject || sendObject_property_change || sendObject_property_change_accessible_description) { QAccessibleInterface *iface = event->accessibleInterface(); if (!iface) { qCDebug(lcAccessibilityAtspi, "DescriptionChanged event from invalid accessible."); return; } QString path = pathForInterface(iface); QVariantList args = packDBusSignalArguments( "accessible-description"_L1, 0, 0, QVariant::fromValue(QDBusVariant(iface->text(QAccessible::Description)))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "PropertyChange"_L1, args); } break; } case QAccessible::Focus: { if (sendFocus || sendObject || sendObject_state_changed) sendFocusChanged(event->accessibleInterface()); break; } case QAccessible::Announcement: { if (sendObject || sendObject_announcement) { QAccessibleAnnouncementEvent *announcementEvent = static_cast(event); sendAnnouncement(announcementEvent); } break; } case QAccessible::TextInserted: case QAccessible::TextRemoved: case QAccessible::TextUpdated: { if (sendObject || sendObject_text_changed) { QAccessibleInterface * iface = event->accessibleInterface(); if (!iface || !iface->textInterface()) { qCWarning(lcAccessibilityAtspi) << "Received text event for invalid interface."; return; } QString path = pathForInterface(iface); int changePosition = 0; int cursorPosition = 0; QString textRemoved; QString textInserted; if (event->type() == QAccessible::TextInserted) { QAccessibleTextInsertEvent *textEvent = static_cast(event); textInserted = textEvent->textInserted(); changePosition = textEvent->changePosition(); cursorPosition = textEvent->cursorPosition(); } else if (event->type() == QAccessible::TextRemoved) { QAccessibleTextRemoveEvent *textEvent = static_cast(event); textRemoved = textEvent->textRemoved(); changePosition = textEvent->changePosition(); cursorPosition = textEvent->cursorPosition(); } else if (event->type() == QAccessible::TextUpdated) { QAccessibleTextUpdateEvent *textEvent = static_cast(event); textInserted = textEvent->textInserted(); textRemoved = textEvent->textRemoved(); changePosition = textEvent->changePosition(); cursorPosition = textEvent->cursorPosition(); } QDBusVariant data; if (!textRemoved.isEmpty()) { data.setVariant(QVariant::fromValue(textRemoved)); QVariantList args = packDBusSignalArguments("delete"_L1, changePosition, textRemoved.size(), QVariant::fromValue(data)); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "TextChanged"_L1, args); } if (!textInserted.isEmpty()) { data.setVariant(QVariant::fromValue(textInserted)); QVariantList args = packDBusSignalArguments("insert"_L1, changePosition, textInserted.size(), QVariant::fromValue(data)); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "TextChanged"_L1, args); } // send a cursor update Q_UNUSED(cursorPosition); // QDBusVariant cursorData; // cursorData.setVariant(QVariant::fromValue(cursorPosition)); // QVariantList args = packDBusSignalArguments(QString(), cursorPosition, 0, QVariant::fromValue(cursorData)); // sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, // "TextCaretMoved"_L1, args); } break; } case QAccessible::TextCaretMoved: { if (sendObject || sendObject_text_caret_moved) { QAccessibleInterface * iface = event->accessibleInterface(); if (!iface || !iface->textInterface()) { qCWarning(lcAccessibilityAtspi) << "Sending TextCaretMoved from object that does not implement text interface: " << iface; return; } QString path = pathForInterface(iface); QDBusVariant cursorData; int pos = iface->textInterface()->cursorPosition(); cursorData.setVariant(QVariant::fromValue(pos)); QVariantList args = packDBusSignalArguments(QString(), pos, 0, QVariant::fromValue(cursorData)); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "TextCaretMoved"_L1, args); } break; } case QAccessible::TextSelectionChanged: { if (sendObject || sendObject_text_selection_changed) { QAccessibleInterface * iface = event->accessibleInterface(); QString path = pathForInterface(iface); QVariantList args = packDBusSignalArguments(QString(), 0, 0, QVariant::fromValue(QDBusVariant(QVariant(QString())))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "TextSelectionChanged"_L1, args); } break; } case QAccessible::ValueChanged: { if (sendObject || sendObject_value_changed || sendObject_property_change_accessible_value) { QAccessibleInterface * iface = event->accessibleInterface(); if (!iface) { qCWarning(lcAccessibilityAtspi) << "ValueChanged event from invalid accessible."; return; } if (iface->valueInterface()) { QString path = pathForInterface(iface); QVariantList args = packDBusSignalArguments("accessible-value"_L1, 0, 0, variantForPath(path)); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "PropertyChange"_L1, args); } else if (iface->role() == QAccessible::ComboBox) { // Combo Box with AT-SPI likes to be special // It requires a name-change to update caches and then selection-changed QString path = pathForInterface(iface); QVariantList args1 = packDBusSignalArguments( "accessible-name"_L1, 0, 0, QVariant::fromValue(QDBusVariant(iface->text(QAccessible::Name)))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "PropertyChange"_L1, args1); QVariantList args2 = packDBusSignalArguments(QString(), 0, 0, QVariant::fromValue(QDBusVariant(QVariant(0)))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "SelectionChanged"_L1, args2); } else { qCWarning(lcAccessibilityAtspi) << "ValueChanged event and no ValueInterface or ComboBox: " << iface; } } break; } case QAccessible::SelectionAdd: case QAccessible::SelectionRemove: case QAccessible::Selection: { QAccessibleInterface * iface = event->accessibleInterface(); if (!iface) { qCWarning(lcAccessibilityAtspi) << "Selection event from invalid accessible."; return; } // send event for change of selected state for the interface itself QString path = pathForInterface(iface); int selected = iface->state().selected ? 1 : 0; QVariantList stateArgs = packDBusSignalArguments("selected"_L1, selected, 0, variantForPath(path)); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "StateChanged"_L1, stateArgs); // send SelectionChanged event for the parent QAccessibleInterface* parent = iface->parent(); if (!parent) { qCDebug(lcAccessibilityAtspi) << "No valid parent in selection event."; return; } QString parentPath = pathForInterface(parent); QVariantList args = packDBusSignalArguments(QString(), 0, 0, variantForPath(parentPath)); sendDBusSignal(parentPath, QLatin1String(ATSPI_DBUS_INTERFACE_EVENT_OBJECT), QLatin1String("SelectionChanged"), args); break; } case QAccessible::SelectionWithin: { QAccessibleInterface * iface = event->accessibleInterface(); if (!iface) { qCWarning(lcAccessibilityAtspi) << "SelectionWithin event from invalid accessible."; return; } QString path = pathForInterface(iface); QVariantList args = packDBusSignalArguments(QString(), 0, 0, variantForPath(path)); sendDBusSignal(path, QLatin1String(ATSPI_DBUS_INTERFACE_EVENT_OBJECT), QLatin1String("SelectionChanged"), args); break; } case QAccessible::StateChanged: { if (sendObject || sendObject_state_changed || sendWindow || sendWindow_activate) { QAccessible::State stateChange = static_cast(event)->changedStates(); if (stateChange.checked) { QAccessibleInterface * iface = event->accessibleInterface(); if (!iface) { qCWarning(lcAccessibilityAtspi) << "StateChanged event from invalid accessible."; return; } int checked = iface->state().checked; notifyStateChange(iface, "checked"_L1, checked); } else if (stateChange.active) { QAccessibleInterface * iface = event->accessibleInterface(); if (!iface || !(iface->role() == QAccessible::Window && (sendWindow || sendWindow_activate))) return; int isActive = iface->state().active; QString windowTitle = iface->text(QAccessible::Name); QDBusVariant data; data.setVariant(windowTitle); QVariantList args = packDBusSignalArguments(QString(), 0, 0, QVariant::fromValue(data)); QString status = isActive ? "Activate"_L1 : "Deactivate"_L1; QString path = pathForInterface(iface); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_WINDOW ""_L1, status, args); notifyStateChange(iface, "active"_L1, isActive); } else if (stateChange.disabled) { QAccessibleInterface *iface = event->accessibleInterface(); QAccessible::State state = iface->state(); bool enabled = !state.disabled; notifyStateChange(iface, "enabled"_L1, enabled); notifyStateChange(iface, "sensitive"_L1, enabled); } else if (stateChange.focused) { QAccessibleInterface *iface = event->accessibleInterface(); QAccessible::State state = iface->state(); bool focused = state.focused; notifyStateChange(iface, "focused"_L1, focused); } } break; } case QAccessible::TableModelChanged: { QAccessibleInterface *interface = event->accessibleInterface(); if (!interface || !interface->isValid()) { qCWarning(lcAccessibilityAtspi) << "TableModelChanged event from invalid accessible."; return; } const QString path = pathForInterface(interface); QAccessibleTableModelChangeEvent *tableModelEvent = static_cast(event); switch (tableModelEvent->modelChangeType()) { case QAccessibleTableModelChangeEvent::ColumnsInserted: { if (sendObject || sendObject_column_inserted) { const int firstColumn = tableModelEvent->firstColumn(); const int insertedColumnCount = tableModelEvent->lastColumn() - firstColumn + 1; QVariantList args = packDBusSignalArguments(QString(), firstColumn, insertedColumnCount, QVariant::fromValue(QDBusVariant(QVariant(QString())))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "ColumnInserted"_L1, args); } break; } case QAccessibleTableModelChangeEvent::ColumnsRemoved: { if (sendObject || sendObject_column_deleted) { const int firstColumn = tableModelEvent->firstColumn(); const int removedColumnCount = tableModelEvent->lastColumn() - firstColumn + 1; QVariantList args = packDBusSignalArguments(QString(), firstColumn, removedColumnCount, QVariant::fromValue(QDBusVariant(QVariant(QString())))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "ColumnDeleted"_L1, args); } break; } case QAccessibleTableModelChangeEvent::RowsInserted: { if (sendObject || sendObject_row_inserted) { const int firstRow = tableModelEvent->firstRow(); const int insertedRowCount = tableModelEvent->lastRow() - firstRow + 1; QVariantList args = packDBusSignalArguments(QString(), firstRow, insertedRowCount, QVariant::fromValue(QDBusVariant(QVariant(QString())))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "RowInserted"_L1, args); } break; } case QAccessibleTableModelChangeEvent::RowsRemoved: { if (sendObject || sendObject_row_deleted) { const int firstRow = tableModelEvent->firstRow(); const int removedRowCount = tableModelEvent->lastRow() - firstRow + 1; QVariantList args = packDBusSignalArguments(QString(), firstRow, removedRowCount, QVariant::fromValue(QDBusVariant(QVariant(QString())))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "RowDeleted"_L1, args); } break; } case QAccessibleTableModelChangeEvent::ModelChangeType::ModelReset: { if (sendObject || sendObject_model_changed) { QVariantList args = packDBusSignalArguments(QString(), 0, 0, QVariant::fromValue(QDBusVariant(QVariant(QString())))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "ModelChanged"_L1, args); } break; } case QAccessibleTableModelChangeEvent::DataChanged: { if (sendObject || sendObject_visible_data_changed) { QVariantList args = packDBusSignalArguments(QString(), 0, 0, QVariant::fromValue(QDBusVariant(QVariant(QString())))); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "VisibleDataChanged"_L1, args); } break; } } break; } // For now we ignore these events case QAccessible::ParentChanged: case QAccessible::DialogStart: case QAccessible::DialogEnd: case QAccessible::PopupMenuStart: case QAccessible::PopupMenuEnd: case QAccessible::SoundPlayed: case QAccessible::Alert: case QAccessible::ForegroundChanged: case QAccessible::MenuStart: case QAccessible::MenuEnd: case QAccessible::ContextHelpStart: case QAccessible::ContextHelpEnd: case QAccessible::DragDropStart: case QAccessible::DragDropEnd: case QAccessible::ScrollingStart: case QAccessible::ScrollingEnd: case QAccessible::MenuCommand: case QAccessible::ActionChanged: case QAccessible::ActiveDescendantChanged: case QAccessible::AttributeChanged: case QAccessible::DocumentContentChanged: case QAccessible::DocumentLoadComplete: case QAccessible::DocumentLoadStopped: case QAccessible::DocumentReload: case QAccessible::HyperlinkEndIndexChanged: case QAccessible::HyperlinkNumberOfAnchorsChanged: case QAccessible::HyperlinkSelectedLinkChanged: case QAccessible::HypertextLinkActivated: case QAccessible::HypertextLinkSelected: case QAccessible::HyperlinkStartIndexChanged: case QAccessible::HypertextChanged: case QAccessible::HypertextNLinksChanged: case QAccessible::ObjectAttributeChanged: case QAccessible::PageChanged: case QAccessible::SectionChanged: case QAccessible::TableCaptionChanged: case QAccessible::TableColumnDescriptionChanged: case QAccessible::TableColumnHeaderChanged: case QAccessible::TableRowDescriptionChanged: case QAccessible::TableRowHeaderChanged: case QAccessible::TableSummaryChanged: case QAccessible::TextAttributeChanged: case QAccessible::TextColumnChanged: case QAccessible::VisibleDataChanged: case QAccessible::LocationChanged: case QAccessible::HelpChanged: case QAccessible::DefaultActionChanged: case QAccessible::AcceleratorChanged: case QAccessible::InvalidEvent: break; } } void AtSpiAdaptor::sendFocusChanged(QAccessibleInterface *interface) const { static QString lastFocusPath; // "remove" old focus if (!lastFocusPath.isEmpty()) { QVariantList stateArgs = packDBusSignalArguments("focused"_L1, 0, 0, variantForPath(lastFocusPath)); sendDBusSignal(lastFocusPath, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "StateChanged"_L1, stateArgs); } // send new focus { QString path = pathForInterface(interface); QVariantList stateArgs = packDBusSignalArguments("focused"_L1, 1, 0, variantForPath(path)); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "StateChanged"_L1, stateArgs); QVariantList focusArgs = packDBusSignalArguments(QString(), 0, 0, variantForPath(path)); sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_FOCUS ""_L1, "Focus"_L1, focusArgs); lastFocusPath = path; } } void AtSpiAdaptor::childrenChanged(QAccessibleInterface *interface) const { QString parentPath = pathForInterface(interface); int childCount = interface->childCount(); for (int i = 0; i < interface->childCount(); ++i) { QString childPath = pathForInterface(interface->child(i)); QVariantList args = packDBusSignalArguments("add"_L1, childCount, 0, childPath); sendDBusSignal(parentPath, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "ChildrenChanged"_L1, args); } } void AtSpiAdaptor::notifyAboutCreation(QAccessibleInterface *interface) const { // notify about the new child of our parent QAccessibleInterface * parent = interface->parent(); if (!parent) { qCDebug(lcAccessibilityAtspi) << "AtSpiAdaptor::notifyAboutCreation: Could not find parent for " << interface->object(); return; } QString path = pathForInterface(interface); int childCount = parent->childCount(); QString parentPath = pathForInterface(parent); QVariantList args = packDBusSignalArguments("add"_L1, childCount, 0, variantForPath(path)); sendDBusSignal(parentPath, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "ChildrenChanged"_L1, args); } void AtSpiAdaptor::notifyAboutDestruction(QAccessibleInterface *interface) const { if (!interface || !interface->isValid()) return; QAccessibleInterface * parent = interface->parent(); if (!parent) { qCDebug(lcAccessibilityAtspi) << "AtSpiAdaptor::notifyAboutDestruction: Could not find parent for " << interface->object(); return; } QString path = pathForInterface(interface); // this is in the destructor. we have no clue which child we used to be. // FIXME int childIndex = -1; // if (child) { // childIndex = child; // } else { // childIndex = parent->indexOfChild(interface); // } QString parentPath = pathForInterface(parent); QVariantList args = packDBusSignalArguments("remove"_L1, childIndex, 0, variantForPath(path)); sendDBusSignal(parentPath, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "ChildrenChanged"_L1, args); } /*! Handle incoming DBus message. This function dispatches the dbus message to the right interface handler. */ bool AtSpiAdaptor::handleMessage(const QDBusMessage &message, const QDBusConnection &connection) { // get accessible interface QAccessibleInterface * accessible = interfaceFromPath(message.path()); if (!accessible) { qCWarning(lcAccessibilityAtspi) << "Could not find accessible on path:" << message.path(); return false; } if (!accessible->isValid()) { qCWarning(lcAccessibilityAtspi) << "Accessible invalid:" << accessible << message.path(); return false; } QString interface = message.interface(); QString function = message.member(); // qCDebug(lcAccessibilityAtspi) << "AtSpiAdaptor::handleMessage: " << interface << function; if (function == "Introspect"_L1) { //introspect(message.path()); return false; } // handle properties like regular functions if (interface == "org.freedesktop.DBus.Properties"_L1) { interface = message.arguments().at(0).toString(); // Get/Set + Name function = message.member() + message.arguments().at(1).toString(); } // switch interface to call if (interface == ATSPI_DBUS_INTERFACE_ACCESSIBLE ""_L1) return accessibleInterface(accessible, function, message, connection); if (interface == ATSPI_DBUS_INTERFACE_APPLICATION ""_L1) return applicationInterface(accessible, function, message, connection); if (interface == ATSPI_DBUS_INTERFACE_COMPONENT ""_L1) return componentInterface(accessible, function, message, connection); if (interface == ATSPI_DBUS_INTERFACE_ACTION ""_L1) return actionInterface(accessible, function, message, connection); if (interface == ATSPI_DBUS_INTERFACE_SELECTION ""_L1) return selectionInterface(accessible, function, message, connection); if (interface == ATSPI_DBUS_INTERFACE_TEXT ""_L1) return textInterface(accessible, function, message, connection); if (interface == ATSPI_DBUS_INTERFACE_EDITABLE_TEXT ""_L1) return editableTextInterface(accessible, function, message, connection); if (interface == ATSPI_DBUS_INTERFACE_VALUE ""_L1) return valueInterface(accessible, function, message, connection); if (interface == ATSPI_DBUS_INTERFACE_TABLE ""_L1) return tableInterface(accessible, function, message, connection); if (interface == ATSPI_DBUS_INTERFACE_TABLE_CELL ""_L1) return tableCellInterface(accessible, function, message, connection); qCDebug(lcAccessibilityAtspi) << "AtSpiAdaptor::handleMessage with unknown interface: " << message.path() << interface << function; return false; } // Application bool AtSpiAdaptor::applicationInterface(QAccessibleInterface *interface, const QString &function, const QDBusMessage &message, const QDBusConnection &connection) { if (message.path() != ATSPI_DBUS_PATH_ROOT ""_L1) { qCWarning(lcAccessibilityAtspi) << "Could not find application interface for:" << message.path() << interface; return false; } if (function == "SetId"_L1) { Q_ASSERT(message.signature() == "ssv"_L1); QVariant value = qvariant_cast(message.arguments().at(2)).variant(); m_applicationId = value.toInt(); return true; } if (function == "GetId"_L1) { Q_ASSERT(message.signature() == "ss"_L1); QDBusMessage reply = message.createReply(QVariant::fromValue(QDBusVariant(m_applicationId))); return connection.send(reply); } if (function == "GetToolkitName"_L1) { Q_ASSERT(message.signature() == "ss"_L1); QDBusMessage reply = message.createReply(QVariant::fromValue(QDBusVariant("Qt"_L1))); return connection.send(reply); } if (function == "GetVersion"_L1) { Q_ASSERT(message.signature() == "ss"_L1); QDBusMessage reply = message.createReply(QVariant::fromValue(QDBusVariant(QLatin1StringView(qVersion())))); return connection.send(reply); } if (function == "GetLocale"_L1) { Q_ASSERT(message.signature() == "u"_L1); QDBusMessage reply = message.createReply(QVariant::fromValue(QLocale().name())); return connection.send(reply); } qCDebug(lcAccessibilityAtspi) << "AtSpiAdaptor::applicationInterface " << message.path() << interface << function; return false; } /*! Register this application as accessible on the accessibility DBus. */ void AtSpiAdaptor::registerApplication() { OrgA11yAtspiSocketInterface *registry; registry = new OrgA11yAtspiSocketInterface(QSPI_REGISTRY_NAME ""_L1, QSPI_OBJECT_PATH_ROOT ""_L1, m_dbus->connection()); QDBusPendingReply reply; QSpiObjectReference ref = QSpiObjectReference(m_dbus->connection(), QDBusObjectPath(QSPI_OBJECT_PATH_ROOT)); reply = registry->Embed(ref); reply.waitForFinished(); // TODO: make this async if (reply.isValid ()) { const QSpiObjectReference &socket = reply.value(); accessibilityRegistry = QSpiObjectReference(socket); } else { qCWarning(lcAccessibilityAtspi) << "Error in contacting registry:" << reply.error().name() << reply.error().message(); } 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) { if (function == "GetRole"_L1) { sendReply(connection, message, (uint) getRole(interface)); } else if (function == "GetName"_L1) { sendReply(connection, message, QVariant::fromValue(QDBusVariant(interface->text(QAccessible::Name)))); } else if (function == "GetRoleName"_L1) { sendReply(connection, message, QSpiAccessibleBridge::namesForRole(interface->role()).name()); } else if (function == "GetLocalizedRoleName"_L1) { sendReply(connection, message, QVariant::fromValue(QSpiAccessibleBridge::namesForRole(interface->role()).localizedName())); } else if (function == "GetChildCount"_L1) { sendReply(connection, message, QVariant::fromValue(QDBusVariant(interface->childCount()))); } else if (function == "GetIndexInParent"_L1) { int childIndex = -1; QAccessibleInterface * parent = interface->parent(); if (parent) { childIndex = parent->indexOfChild(interface); if (childIndex < 0) { qCDebug(lcAccessibilityAtspi) << "GetIndexInParent get invalid index: " << childIndex << interface; } } sendReply(connection, message, childIndex); } else if (function == "GetParent"_L1) { QString path; QAccessibleInterface * parent = interface->parent(); if (!parent) { path = ATSPI_DBUS_PATH_NULL ""_L1; } else if (parent->role() == QAccessible::Application) { path = ATSPI_DBUS_PATH_ROOT ""_L1; } else { path = pathForInterface(parent); } // Parent is a property, so it needs to be wrapped inside an extra variant. sendReply(connection, message, QVariant::fromValue( QDBusVariant(QVariant::fromValue(QSpiObjectReference(connection, QDBusObjectPath(path)))))); } else if (function == "GetChildAtIndex"_L1) { const int index = message.arguments().at(0).toInt(); if (index < 0) { sendReply(connection, message, QVariant::fromValue( QSpiObjectReference(connection, QDBusObjectPath(ATSPI_DBUS_PATH_NULL)))); } else { QAccessibleInterface * childInterface = interface->child(index); sendReply(connection, message, QVariant::fromValue( QSpiObjectReference(connection, QDBusObjectPath(pathForInterface(childInterface))))); } } else if (function == "GetInterfaces"_L1) { sendReply(connection, message, accessibleInterfaces(interface)); } else if (function == "GetDescription"_L1) { sendReply(connection, message, QVariant::fromValue(QDBusVariant(interface->text(QAccessible::Description)))); } else if (function == "GetHelpText"_L1) { sendReply(connection, message, QVariant::fromValue(QDBusVariant(interface->text(QAccessible::Help)))); } else if (function == "GetState"_L1) { quint64 spiState = spiStatesFromQState(interface->state()); if (interface->tableInterface()) { // For tables, setting manages_descendants should // indicate to the client that it cannot cache these // interfaces. setSpiStateBit(&spiState, ATSPI_STATE_MANAGES_DESCENDANTS); } QAccessible::Role role = interface->role(); if (role == QAccessible::TreeItem || role == QAccessible::ListItem) { /* Transient means libatspi2 will not cache items. This is important because when adding/removing an item the cache becomes outdated and we don't change the paths of items in lists/trees/tables. */ setSpiStateBit(&spiState, ATSPI_STATE_TRANSIENT); } sendReply(connection, message, QVariant::fromValue(spiStateSetFromSpiStates(spiState))); } else if (function == "GetAttributes"_L1) { sendReply(connection, message, QVariant::fromValue(QSpiAttributeSet())); } else if (function == "GetRelationSet"_L1) { sendReply(connection, message, QVariant::fromValue(relationSet(interface, connection))); } else if (function == "GetApplication"_L1) { sendReply(connection, message, QVariant::fromValue( QSpiObjectReference(connection, QDBusObjectPath(QSPI_OBJECT_PATH_ROOT)))); } else if (function == "GetChildren"_L1) { QSpiObjectReferenceArray children; const int numChildren = interface->childCount(); children.reserve(numChildren); for (int i = 0; i < numChildren; ++i) { QString childPath = pathForInterface(interface->child(i)); QSpiObjectReference ref(connection, QDBusObjectPath(childPath)); children << ref; } connection.send(message.createReply(QVariant::fromValue(children))); } else if (function == "GetAccessibleId"_L1) { sendReply(connection, message, QVariant::fromValue(QDBusVariant(accessibleIdForAccessible(interface)))); } else { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::accessibleInterface does not implement" << function << message.path(); return false; } return true; } AtspiRole AtSpiAdaptor::getRole(QAccessibleInterface *interface) const { if ((interface->role() == QAccessible::EditableText) && interface->state().passwordEdit) return ATSPI_ROLE_PASSWORD_TEXT; return QSpiAccessibleBridge::namesForRole(interface->role()).spiRole(); } QStringList AtSpiAdaptor::accessibleInterfaces(QAccessibleInterface *interface) const { QStringList ifaces; qCDebug(lcAccessibilityAtspiCreation) << "AtSpiAdaptor::accessibleInterfaces create: " << interface->object(); ifaces << u"" ATSPI_DBUS_INTERFACE_ACCESSIBLE ""_s; if ( (!interface->rect().isEmpty()) || (interface->object() && interface->object()->isWidgetType()) || (interface->role() == QAccessible::ListItem) || (interface->role() == QAccessible::Cell) || (interface->role() == QAccessible::TreeItem) || (interface->role() == QAccessible::Row) || (interface->object() && interface->object()->inherits("QSGItem")) ) { ifaces << u"" ATSPI_DBUS_INTERFACE_COMPONENT ""_s; } else { qCDebug(lcAccessibilityAtspiCreation) << " IS NOT a component"; } if (interface->role() == QAccessible::Application) ifaces << u"" ATSPI_DBUS_INTERFACE_APPLICATION ""_s; if (interface->actionInterface() || interface->valueInterface()) ifaces << u"" ATSPI_DBUS_INTERFACE_ACTION ""_s; if (interface->selectionInterface()) ifaces << ATSPI_DBUS_INTERFACE_SELECTION ""_L1; if (interface->textInterface()) ifaces << u"" ATSPI_DBUS_INTERFACE_TEXT ""_s; if (interface->editableTextInterface()) ifaces << u"" ATSPI_DBUS_INTERFACE_EDITABLE_TEXT ""_s; if (interface->valueInterface()) ifaces << u"" ATSPI_DBUS_INTERFACE_VALUE ""_s; if (interface->tableInterface()) ifaces << u"" ATSPI_DBUS_INTERFACE_TABLE ""_s; if (interface->tableCellInterface()) ifaces << u"" ATSPI_DBUS_INTERFACE_TABLE_CELL ""_s; return ifaces; } QSpiRelationArray AtSpiAdaptor::relationSet(QAccessibleInterface *interface, const QDBusConnection &connection) const { typedef QPair RelationPair; const QList relationInterfaces = interface->relations(); QSpiRelationArray relations; for (const RelationPair &pair : relationInterfaces) { // FIXME: this loop seems a bit strange... "related" always have one item when we check. //And why is it a list, when it always have one item? And it seems to assume that the QAccessible::Relation enum maps directly to AtSpi QSpiObjectReferenceArray related; QDBusObjectPath path = QDBusObjectPath(pathForInterface(pair.first)); related.append(QSpiObjectReference(connection, path)); if (!related.isEmpty()) relations.append(QSpiRelationArrayEntry(qAccessibleRelationToAtSpiRelation(pair.second), related)); } return relations; } void AtSpiAdaptor::sendReply(const QDBusConnection &connection, const QDBusMessage &message, const QVariant &argument) const { QDBusMessage reply = message.createReply(argument); connection.send(reply); } QString AtSpiAdaptor::pathForObject(QObject *object) const { Q_ASSERT(object); if (inheritsQAction(object)) { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::pathForObject: Creating path with QAction as object."; } QAccessibleInterface *iface = QAccessible::queryAccessibleInterface(object); return pathForInterface(iface); } QString AtSpiAdaptor::pathForInterface(QAccessibleInterface *interface) const { if (!interface || !interface->isValid()) return u"" ATSPI_DBUS_PATH_NULL ""_s; if (interface->role() == QAccessible::Application) return u"" QSPI_OBJECT_PATH_ROOT ""_s; QAccessible::Id id = QAccessible::uniqueId(interface); Q_ASSERT((int)id < 0); return QSPI_OBJECT_PATH_PREFIX ""_L1 + QString::number(id); } bool AtSpiAdaptor::inheritsQAction(QObject *object) { const QMetaObject *mo = object->metaObject(); while (mo) { const QLatin1StringView cn(mo->className()); if (cn == "QAction"_L1) return true; mo = mo->superClass(); } return false; } // Component static QAccessibleInterface * getWindow(QAccessibleInterface * interface) { // find top-level window in a11y hierarchy (either has a // corresponding role or is a direct child of the application object) QAccessibleInterface* app = QAccessible::queryAccessibleInterface(qApp); while (interface && interface->role() != QAccessible::Dialog && interface->role() != QAccessible::Window && interface->parent() != app) interface = interface->parent(); return interface; } bool AtSpiAdaptor::componentInterface(QAccessibleInterface *interface, const QString &function, const QDBusMessage &message, const QDBusConnection &connection) { if (function == "Contains"_L1) { bool ret = false; int x = message.arguments().at(0).toInt(); int y = message.arguments().at(1).toInt(); uint coordType = message.arguments().at(2).toUInt(); if (!isValidCoordType(coordType)) return false; ret = getExtents(interface, coordType).contains(x, y); sendReply(connection, message, ret); } else if (function == "GetAccessibleAtPoint"_L1) { QPoint point(message.arguments().at(0).toInt(), message.arguments().at(1).toInt()); uint coordType = message.arguments().at(2).toUInt(); if (!isValidCoordType(coordType)) return false; QPoint screenPos = translateToScreenCoordinates(interface, point, coordType); QAccessibleInterface * childInterface(interface->childAt(screenPos.x(), screenPos.y())); QAccessibleInterface * iface = nullptr; while (childInterface) { iface = childInterface; childInterface = iface->childAt(screenPos.x(), screenPos.y()); } if (iface) { QString path = pathForInterface(iface); sendReply(connection, message, QVariant::fromValue( QSpiObjectReference(connection, QDBusObjectPath(path)))); } else { sendReply(connection, message, QVariant::fromValue( QSpiObjectReference(connection, QDBusObjectPath(ATSPI_DBUS_PATH_NULL)))); } } else if (function == "GetAlpha"_L1) { sendReply(connection, message, (double) 1.0); } else if (function == "GetExtents"_L1) { uint coordType = message.arguments().at(0).toUInt(); if (!isValidCoordType(coordType)) return false; sendReply(connection, message, QVariant::fromValue(getExtents(interface, coordType))); } else if (function == "GetLayer"_L1) { sendReply(connection, message, QVariant::fromValue((uint)1)); } else if (function == "GetMDIZOrder"_L1) { sendReply(connection, message, QVariant::fromValue((short)0)); } else if (function == "GetPosition"_L1) { uint coordType = message.arguments().at(0).toUInt(); if (!isValidCoordType(coordType)) return false; QRect rect = getExtents(interface, coordType); QVariantList pos; pos << rect.x() << rect.y(); connection.send(message.createReply(pos)); } else if (function == "GetSize"_L1) { QRect rect = interface->rect(); QVariantList size; size << rect.width() << rect.height(); connection.send(message.createReply(size)); } else if (function == "GrabFocus"_L1) { QAccessibleActionInterface *actionIface = interface->actionInterface(); if (actionIface && actionIface->actionNames().contains(QAccessibleActionInterface::setFocusAction())) { actionIface->doAction(QAccessibleActionInterface::setFocusAction()); sendReply(connection, message, true); } else { sendReply(connection, message, false); } } else if (function == "SetExtents"_L1) { // int x = message.arguments().at(0).toInt(); // int y = message.arguments().at(1).toInt(); // int width = message.arguments().at(2).toInt(); // int height = message.arguments().at(3).toInt(); // uint coordinateType = message.arguments().at(4).toUInt(); qCDebug(lcAccessibilityAtspi) << "SetExtents is not implemented."; sendReply(connection, message, false); } else if (function == "SetPosition"_L1) { // int x = message.arguments().at(0).toInt(); // int y = message.arguments().at(1).toInt(); // uint coordinateType = message.arguments().at(2).toUInt(); qCDebug(lcAccessibilityAtspi) << "SetPosition is not implemented."; sendReply(connection, message, false); } else if (function == "SetSize"_L1) { // int width = message.arguments().at(0).toInt(); // int height = message.arguments().at(1).toInt(); qCDebug(lcAccessibilityAtspi) << "SetSize is not implemented."; sendReply(connection, message, false); } else { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::componentInterface does not implement" << function << message.path(); return false; } return true; } QRect AtSpiAdaptor::getExtents(QAccessibleInterface *interface, uint coordType) { return translateFromScreenCoordinates(interface, interface->rect(), coordType); } // Action interface bool AtSpiAdaptor::actionInterface(QAccessibleInterface *interface, const QString &function, const QDBusMessage &message, const QDBusConnection &connection) { if (function == "GetNActions"_L1) { int count = QAccessibleBridgeUtils::effectiveActionNames(interface).size(); sendReply(connection, message, QVariant::fromValue(QDBusVariant(QVariant::fromValue(count)))); } else if (function == "DoAction"_L1) { int index = message.arguments().at(0).toInt(); const QStringList actionNames = QAccessibleBridgeUtils::effectiveActionNames(interface); if (index < 0 || index >= actionNames.size()) return false; const QString actionName = actionNames.at(index); bool success = QAccessibleBridgeUtils::performEffectiveAction(interface, actionName); sendReply(connection, message, success); } else if (function == "GetActions"_L1) { sendReply(connection, message, QVariant::fromValue(getActions(interface))); } else if (function == "GetName"_L1) { int index = message.arguments().at(0).toInt(); const QStringList actionNames = QAccessibleBridgeUtils::effectiveActionNames(interface); if (index < 0 || index >= actionNames.size()) return false; sendReply(connection, message, actionNames.at(index)); } else if (function == "GetDescription"_L1) { int index = message.arguments().at(0).toInt(); const QStringList actionNames = QAccessibleBridgeUtils::effectiveActionNames(interface); if (index < 0 || index >= actionNames.size()) return false; QString description; if (QAccessibleActionInterface *actionIface = interface->actionInterface()) description = actionIface->localizedActionDescription(actionNames.at(index)); else description = qAccessibleLocalizedActionDescription(actionNames.at(index)); sendReply(connection, message, description); } else if (function == "GetKeyBinding"_L1) { int index = message.arguments().at(0).toInt(); const QStringList actionNames = QAccessibleBridgeUtils::effectiveActionNames(interface); if (index < 0 || index >= actionNames.size()) return false; QStringList keyBindings; if (QAccessibleActionInterface *actionIface = interface->actionInterface()) keyBindings = actionIface->keyBindingsForAction(actionNames.at(index)); if (keyBindings.isEmpty()) { QString acc = interface->text(QAccessible::Accelerator); if (!acc.isEmpty()) keyBindings.append(acc); } if (keyBindings.size() > 0) sendReply(connection, message, keyBindings.join(u';')); else sendReply(connection, message, QString()); } else { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::actionInterface does not implement" << function << message.path(); return false; } return true; } QSpiActionArray AtSpiAdaptor::getActions(QAccessibleInterface *interface) const { QAccessibleActionInterface *actionInterface = interface->actionInterface(); QSpiActionArray actions; const QStringList actionNames = QAccessibleBridgeUtils::effectiveActionNames(interface); actions.reserve(actionNames.size()); for (const QString &actionName : actionNames) { QSpiAction action; action.name = actionName; if (actionInterface) { action.description = actionInterface->localizedActionDescription(actionName); const QStringList keyBindings = actionInterface->keyBindingsForAction(actionName); if (!keyBindings.isEmpty()) action.keyBinding = keyBindings.front(); } else { action.description = qAccessibleLocalizedActionDescription(actionName); } actions.append(std::move(action)); } return actions; } // Text interface bool AtSpiAdaptor::textInterface(QAccessibleInterface *interface, const QString &function, const QDBusMessage &message, const QDBusConnection &connection) { if (!interface->textInterface()) return false; // properties if (function == "GetCaretOffset"_L1) { sendReply(connection, message, QVariant::fromValue(QDBusVariant(QVariant::fromValue(interface->textInterface()->cursorPosition())))); } else if (function == "GetCharacterCount"_L1) { sendReply(connection, message, QVariant::fromValue(QDBusVariant(QVariant::fromValue(interface->textInterface()->characterCount())))); // functions } else if (function == "AddSelection"_L1) { int startOffset = message.arguments().at(0).toInt(); int endOffset = message.arguments().at(1).toInt(); int lastSelection = interface->textInterface()->selectionCount(); interface->textInterface()->setSelection(lastSelection, startOffset, endOffset); sendReply(connection, message, (interface->textInterface()->selectionCount() > lastSelection)); } else if (function == "GetAttributeRun"_L1) { int offset = message.arguments().at(0).toInt(); bool includeDefaults = message.arguments().at(1).toBool(); Q_UNUSED(includeDefaults); connection.send(message.createReply(getAttributes(interface, offset, includeDefaults))); } else if (function == "GetAttributeValue"_L1) { int offset = message.arguments().at(0).toInt(); QString attributeName = message.arguments().at(1).toString(); connection.send(message.createReply(QVariant(getAttributeValue(interface, offset, attributeName)))); } else if (function == "GetAttributes"_L1) { int offset = message.arguments().at(0).toInt(); connection.send(message.createReply(getAttributes(interface, offset, true))); } else if (function == "GetBoundedRanges"_L1) { int x = message.arguments().at(0).toInt(); int y = message.arguments().at(1).toInt(); int width = message.arguments().at(2).toInt(); int height = message.arguments().at(3).toInt(); uint coordType = message.arguments().at(4).toUInt(); uint xClipType = message.arguments().at(5).toUInt(); uint yClipType = message.arguments().at(6).toUInt(); Q_UNUSED(x); Q_UNUSED(y); Q_UNUSED(width); Q_UNUSED(height); Q_UNUSED(coordType); Q_UNUSED(xClipType); Q_UNUSED(yClipType); qCDebug(lcAccessibilityAtspi) << "Not implemented: QSpiAdaptor::GetBoundedRanges"; sendReply(connection, message, QVariant::fromValue(QSpiTextRangeList())); } else if (function == "GetCharacterAtOffset"_L1) { int offset = message.arguments().at(0).toInt(); int start; int end; const QString charString = interface->textInterface() ->textAtOffset(offset, QAccessible::CharBoundary, &start, &end); int codePoint = 0; QStringIterator stringIt(charString); if (stringIt.hasNext()) codePoint = static_cast(stringIt.peekNext()); sendReply(connection, message, codePoint); } else if (function == "GetCharacterExtents"_L1) { int offset = message.arguments().at(0).toInt(); int coordType = message.arguments().at(1).toUInt(); connection.send(message.createReply(getCharacterExtents(interface, offset, coordType))); } else if (function == "GetDefaultAttributeSet"_L1 || function == "GetDefaultAttributes"_L1) { // GetDefaultAttributes is deprecated in favour of GetDefaultAttributeSet. // Empty set seems reasonable. There is no default attribute set. sendReply(connection, message, QVariant::fromValue(QSpiAttributeSet())); } else if (function == "GetNSelections"_L1) { sendReply(connection, message, interface->textInterface()->selectionCount()); } else if (function == "GetOffsetAtPoint"_L1) { qCDebug(lcAccessibilityAtspi) << message.signature(); Q_ASSERT(!message.signature().isEmpty()); QPoint point(message.arguments().at(0).toInt(), message.arguments().at(1).toInt()); uint coordType = message.arguments().at(2).toUInt(); if (!isValidCoordType(coordType)) return false; QPoint screenPos = translateToScreenCoordinates(interface, point, coordType); int offset = interface->textInterface()->offsetAtPoint(screenPos); sendReply(connection, message, offset); } else if (function == "GetRangeExtents"_L1) { int startOffset = message.arguments().at(0).toInt(); int endOffset = message.arguments().at(1).toInt(); uint coordType = message.arguments().at(2).toUInt(); connection.send(message.createReply(getRangeExtents(interface, startOffset, endOffset, coordType))); } else if (function == "GetSelection"_L1) { int selectionNum = message.arguments().at(0).toInt(); int start, end; interface->textInterface()->selection(selectionNum, &start, &end); if (start < 0) start = end = interface->textInterface()->cursorPosition(); QVariantList sel; sel << start << end; connection.send(message.createReply(sel)); } else if (function == "GetStringAtOffset"_L1) { int offset = message.arguments().at(0).toInt(); uint granularity = message.arguments().at(1).toUInt(); if (!isValidAtspiTextGranularity(granularity)) return false; int startOffset, endOffset; QString text = interface->textInterface()->textAtOffset(offset, qAccessibleBoundaryTypeFromAtspiTextGranularity(granularity), &startOffset, &endOffset); QVariantList ret; ret << text << startOffset << endOffset; connection.send(message.createReply(ret)); } else if (function == "GetText"_L1) { int startOffset = message.arguments().at(0).toInt(); int endOffset = message.arguments().at(1).toInt(); if (endOffset == -1) // AT-SPI uses -1 to signal all characters endOffset = interface->textInterface()->characterCount(); sendReply(connection, message, interface->textInterface()->text(startOffset, endOffset)); } else if (function == "GetTextAfterOffset"_L1) { int offset = message.arguments().at(0).toInt(); int type = message.arguments().at(1).toUInt(); int startOffset, endOffset; QString text = interface->textInterface()->textAfterOffset(offset, qAccessibleBoundaryTypeFromAtspiBoundaryType(type), &startOffset, &endOffset); QVariantList ret; ret << text << startOffset << endOffset; connection.send(message.createReply(ret)); } else if (function == "GetTextAtOffset"_L1) { int offset = message.arguments().at(0).toInt(); int type = message.arguments().at(1).toUInt(); int startOffset, endOffset; QString text = interface->textInterface()->textAtOffset(offset, qAccessibleBoundaryTypeFromAtspiBoundaryType(type), &startOffset, &endOffset); QVariantList ret; ret << text << startOffset << endOffset; connection.send(message.createReply(ret)); } else if (function == "GetTextBeforeOffset"_L1) { int offset = message.arguments().at(0).toInt(); int type = message.arguments().at(1).toUInt(); int startOffset, endOffset; QString text = interface->textInterface()->textBeforeOffset(offset, qAccessibleBoundaryTypeFromAtspiBoundaryType(type), &startOffset, &endOffset); QVariantList ret; ret << text << startOffset << endOffset; connection.send(message.createReply(ret)); } else if (function == "RemoveSelection"_L1) { int selectionNum = message.arguments().at(0).toInt(); interface->textInterface()->removeSelection(selectionNum); sendReply(connection, message, true); } else if (function == "SetCaretOffset"_L1) { int offset = message.arguments().at(0).toInt(); interface->textInterface()->setCursorPosition(offset); sendReply(connection, message, true); } else if (function == "ScrollSubstringTo"_L1) { int startOffset = message.arguments().at(0).toInt(); int endOffset = message.arguments().at(1).toInt(); // ignore third parameter (scroll type), since QAccessibleTextInterface::scrollToSubstring doesn't have that qCInfo(lcAccessibilityAtspi) << "AtSpiAdaptor::ScrollSubstringTo doesn'take take scroll type into account."; interface->textInterface()->scrollToSubstring(startOffset, endOffset); sendReply(connection, message, true); } else if (function == "SetSelection"_L1) { int selectionNum = message.arguments().at(0).toInt(); int startOffset = message.arguments().at(1).toInt(); int endOffset = message.arguments().at(2).toInt(); interface->textInterface()->setSelection(selectionNum, startOffset, endOffset); sendReply(connection, message, true); } else { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::textInterface does not implement" << function << message.path(); return false; } return true; } QAccessible::TextBoundaryType AtSpiAdaptor::qAccessibleBoundaryTypeFromAtspiBoundaryType(int atspiTextBoundaryType) { switch (atspiTextBoundaryType) { case ATSPI_TEXT_BOUNDARY_CHAR: return QAccessible::CharBoundary; case ATSPI_TEXT_BOUNDARY_WORD_START: case ATSPI_TEXT_BOUNDARY_WORD_END: return QAccessible::WordBoundary; case ATSPI_TEXT_BOUNDARY_SENTENCE_START: case ATSPI_TEXT_BOUNDARY_SENTENCE_END: return QAccessible::SentenceBoundary; case ATSPI_TEXT_BOUNDARY_LINE_START: case ATSPI_TEXT_BOUNDARY_LINE_END: return QAccessible::LineBoundary; } Q_ASSERT_X(0, "", "Requested invalid boundary type."); return QAccessible::CharBoundary; } bool AtSpiAdaptor::isValidAtspiTextGranularity(uint atspiTextGranularity) { if (atspiTextGranularity == ATSPI_TEXT_GRANULARITY_CHAR || atspiTextGranularity == ATSPI_TEXT_GRANULARITY_WORD || atspiTextGranularity == ATSPI_TEXT_GRANULARITY_SENTENCE || atspiTextGranularity == ATSPI_TEXT_GRANULARITY_LINE || atspiTextGranularity == ATSPI_TEXT_GRANULARITY_PARAGRAPH) return true; qCWarning(lcAccessibilityAtspi) << "Unknown value" << atspiTextGranularity << "for AT-SPI text granularity type"; return false; } QAccessible::TextBoundaryType AtSpiAdaptor::qAccessibleBoundaryTypeFromAtspiTextGranularity(uint atspiTextGranularity) { Q_ASSERT(isValidAtspiTextGranularity(atspiTextGranularity)); switch (atspiTextGranularity) { case ATSPI_TEXT_GRANULARITY_CHAR: return QAccessible::CharBoundary; case ATSPI_TEXT_GRANULARITY_WORD: return QAccessible::WordBoundary; case ATSPI_TEXT_GRANULARITY_SENTENCE: return QAccessible::SentenceBoundary; case ATSPI_TEXT_GRANULARITY_LINE: return QAccessible::LineBoundary; case ATSPI_TEXT_GRANULARITY_PARAGRAPH: return QAccessible::ParagraphBoundary; } return QAccessible::CharBoundary; } namespace { struct AtSpiAttribute { QString name; QString value; AtSpiAttribute(const QString &aName, const QString &aValue) : name(aName), value(aValue) {} bool isNull() const { return name.isNull() || value.isNull(); } }; QString atspiColor(const QString &ia2Color) { // "rgb(%u,%u,%u)" -> "%u,%u,%u" return ia2Color.mid(4, ia2Color.size() - (4+1)).replace(u"\\,"_s, u","_s); } QString atspiSize(const QString &ia2Size) { // "%fpt" -> "%f" return ia2Size.left(ia2Size.size() - 2); } AtSpiAttribute atspiTextAttribute(const QString &ia2Name, const QString &ia2Value) { QString name = ia2Name; QString value = ia2Value; // IAccessible2: https://wiki.linuxfoundation.org/accessibility/iaccessible2/textattributes // ATK attribute names: https://gitlab.gnome.org/GNOME/orca/-/blob/master/src/orca/text_attribute_names.py // ATK attribute values: https://gnome.pages.gitlab.gnome.org/atk/AtkText.html#AtkTextAttribute // https://bugzilla.gnome.org/show_bug.cgi?id=744553 "ATK docs provide no guidance for allowed values of some text attributes" // specifically for "weight", "invalid", "language" and value range for colors if (ia2Name == "background-color"_L1) { name = QStringLiteral("bg-color"); value = atspiColor(value); } else if (ia2Name == "font-family"_L1) { name = QStringLiteral("family-name"); } else if (ia2Name == "color"_L1) { name = QStringLiteral("fg-color"); value = atspiColor(value); } else if (ia2Name == "text-align"_L1) { name = QStringLiteral("justification"); if (value == "justify"_L1) { value = QStringLiteral("fill"); } else if (value != "left"_L1 && value != "right"_L1 && value != "center"_L1) { qCDebug(lcAccessibilityAtspi) << "Unknown text-align attribute value \"" << value << "\" cannot be translated to AT-SPI."; value = QString(); } } else if (ia2Name == "font-size"_L1) { name = QStringLiteral("size"); value = atspiSize(value); } else if (ia2Name == "font-style"_L1) { name = QStringLiteral("style"); if (value != "normal"_L1 && value != "italic"_L1 && value != "oblique"_L1) { qCDebug(lcAccessibilityAtspi) << "Unknown font-style attribute value \"" << value << "\" cannot be translated to AT-SPI."; value = QString(); } } else if (ia2Name == "text-underline-type"_L1) { name = QStringLiteral("underline"); if (value != "none"_L1 && value != "single"_L1 && value != "double"_L1) { qCDebug(lcAccessibilityAtspi) << "Unknown text-underline-type attribute value \"" << value << "\" cannot be translated to AT-SPI."; value = QString(); } } else if (ia2Name == "font-weight"_L1) { name = QStringLiteral("weight"); if (value == "normal"_L1) // Orca seems to accept all IAccessible2 values except for "normal" // (on which it produces traceback and fails to read any following text attributes), // but that is the default value, so omit it anyway value = QString(); } else if (((ia2Name == "text-line-through-style"_L1 || ia2Name == "text-line-through-type"_L1) && (ia2Value != "none"_L1)) || (ia2Name == "text-line-through-text"_L1 && !ia2Value.isEmpty())) { // if any of the above is set, set "strikethrough" to true, but don't explicitly set // to false otherwise, since any of the others might still be set to indicate strikethrough // and no strikethrough is assumed anyway when nothing is explicitly set name = QStringLiteral("strikethrough"); value = QStringLiteral("true"); } else if (ia2Name == "text-position"_L1) { name = QStringLiteral("vertical-align"); if (value != "baseline"_L1 && value != "super"_L1 && value != "sub"_L1) { qCDebug(lcAccessibilityAtspi) << "Unknown text-position attribute value \"" << value << "\" cannot be translated to AT-SPI."; value = QString(); } } else if (ia2Name == "writing-mode"_L1) { name = QStringLiteral("direction"); if (value == "lr"_L1) value = QStringLiteral("ltr"); else if (value == "rl"_L1) value = QStringLiteral("rtl"); else if (value == "tb"_L1) { // IAccessible2 docs refer to XSL, which specifies "tb" is shorthand for "tb-rl"; so at least give a hint about the horizontal direction (ATK does not support vertical direction in this attribute (yet)) value = QStringLiteral("rtl"); qCDebug(lcAccessibilityAtspi) << "writing-mode attribute value \"tb\" translated only w.r.t. horizontal direction; vertical direction ignored"; } else { qCDebug(lcAccessibilityAtspi) << "Unknown writing-mode attribute value \"" << value << "\" cannot be translated to AT-SPI."; value = QString(); } } else if (ia2Name == "language"_L1) { // OK - ATK has no docs on the format of the value, IAccessible2 has reasonable format - leave it at that now } else if (ia2Name == "invalid"_L1) { // OK - ATK docs are vague but suggest they support the same range of values as IAccessible2 } else { // attribute we know nothing about name = QString(); value = QString(); } return AtSpiAttribute(name, value); } } // FIXME all attribute methods below should share code QVariantList AtSpiAdaptor::getAttributes(QAccessibleInterface *interface, int offset, bool includeDefaults) const { Q_UNUSED(includeDefaults); QSpiAttributeSet set; int startOffset; int endOffset; QString joined = interface->textInterface()->attributes(offset, &startOffset, &endOffset); const QStringList attributes = joined.split(u';', Qt::SkipEmptyParts, Qt::CaseSensitive); for (const QString &attr : attributes) { QStringList items = attr.split(u':', Qt::SkipEmptyParts, Qt::CaseSensitive); if (items.count() == 2) { AtSpiAttribute attribute = atspiTextAttribute(items[0], items[1]); if (!attribute.isNull()) set[attribute.name] = attribute.value; } } QVariantList list; list << QVariant::fromValue(set) << startOffset << endOffset; return list; } QString AtSpiAdaptor::getAttributeValue(QAccessibleInterface *interface, int offset, const QString &attributeName) const { QString joined; QSpiAttributeSet map; int startOffset; int endOffset; joined = interface->textInterface()->attributes(offset, &startOffset, &endOffset); const QStringList attributes = joined.split (u';', Qt::SkipEmptyParts, Qt::CaseSensitive); for (const QString& attr : attributes) { QStringList items; items = attr.split(u':', Qt::SkipEmptyParts, Qt::CaseSensitive); AtSpiAttribute attribute = atspiTextAttribute(items[0], items[1]); if (!attribute.isNull()) map[attribute.name] = attribute.value; } return map[attributeName]; } QList AtSpiAdaptor::getCharacterExtents(QAccessibleInterface *interface, int offset, uint coordType) const { QRect rect = interface->textInterface()->characterRect(offset); rect = translateFromScreenCoordinates(interface, rect, coordType); return QList() << rect.x() << rect.y() << rect.width() << rect.height(); } QList AtSpiAdaptor::getRangeExtents(QAccessibleInterface *interface, int startOffset, int endOffset, uint coordType) const { if (endOffset == -1) endOffset = interface->textInterface()->characterCount(); QAccessibleTextInterface *textInterface = interface->textInterface(); if (endOffset <= startOffset || !textInterface) return QList() << -1 << -1 << 0 << 0; QRect rect = textInterface->characterRect(startOffset); for (int i=startOffset + 1; i <= endOffset; i++) rect = rect | textInterface->characterRect(i); rect = translateFromScreenCoordinates(interface, rect, coordType); return QList() << rect.x() << rect.y() << rect.width() << rect.height(); } bool AtSpiAdaptor::isValidCoordType(uint coordType) { if (coordType == ATSPI_COORD_TYPE_SCREEN || coordType == ATSPI_COORD_TYPE_WINDOW || coordType == ATSPI_COORD_TYPE_PARENT) return true; qCWarning(lcAccessibilityAtspi) << "Unknown value" << coordType << "for AT-SPI coord type"; return false; } QRect AtSpiAdaptor::translateFromScreenCoordinates(QAccessibleInterface *interface, const QRect &screenRect, uint targetCoordType) { Q_ASSERT(isValidCoordType(targetCoordType)); QAccessibleInterface *upper = nullptr; if (targetCoordType == ATSPI_COORD_TYPE_WINDOW) upper = getWindow(interface); else if (targetCoordType == ATSPI_COORD_TYPE_PARENT) upper = interface->parent(); QRect rect = screenRect; if (upper) rect.translate(-upper->rect().x(), -upper->rect().y()); return rect; } QPoint AtSpiAdaptor::translateToScreenCoordinates(QAccessibleInterface *interface, const QPoint &pos, uint fromCoordType) { Q_ASSERT(isValidCoordType(fromCoordType)); QAccessibleInterface *upper = nullptr; if (fromCoordType == ATSPI_COORD_TYPE_WINDOW) upper = getWindow(interface); else if (fromCoordType == ATSPI_COORD_TYPE_PARENT) upper = interface->parent(); QPoint screenPos = pos; if (upper) screenPos += upper->rect().topLeft(); return screenPos; } // Editable Text interface static QString textForRange(QAccessibleInterface *accessible, int startOffset, int endOffset) { if (QAccessibleTextInterface *textIface = accessible->textInterface()) { if (endOffset == -1) endOffset = textIface->characterCount(); return textIface->text(startOffset, endOffset); } QString txt = accessible->text(QAccessible::Value); if (endOffset == -1) endOffset = txt.size(); return txt.mid(startOffset, endOffset - startOffset); } static void replaceTextFallback(QAccessibleInterface *accessible, long startOffset, long endOffset, const QString &txt) { QString t = textForRange(accessible, 0, -1); if (endOffset == -1) endOffset = t.size(); if (endOffset - startOffset == 0) t.insert(startOffset, txt); else t.replace(startOffset, endOffset - startOffset, txt); accessible->setText(QAccessible::Value, t); } bool AtSpiAdaptor::editableTextInterface(QAccessibleInterface *interface, const QString &function, const QDBusMessage &message, const QDBusConnection &connection) { if (function == "CopyText"_L1) { #ifndef QT_NO_CLIPBOARD int startOffset = message.arguments().at(0).toInt(); int endOffset = message.arguments().at(1).toInt(); const QString t = textForRange(interface, startOffset, endOffset); QGuiApplication::clipboard()->setText(t); #endif connection.send(message.createReply(true)); } else if (function == "CutText"_L1) { #ifndef QT_NO_CLIPBOARD int startOffset = message.arguments().at(0).toInt(); int endOffset = message.arguments().at(1).toInt(); const QString t = textForRange(interface, startOffset, endOffset); if (QAccessibleEditableTextInterface *editableTextIface = interface->editableTextInterface()) editableTextIface->deleteText(startOffset, endOffset); else replaceTextFallback(interface, startOffset, endOffset, QString()); QGuiApplication::clipboard()->setText(t); #endif connection.send(message.createReply(true)); } else if (function == "DeleteText"_L1) { int startOffset = message.arguments().at(0).toInt(); int endOffset = message.arguments().at(1).toInt(); if (QAccessibleEditableTextInterface *editableTextIface = interface->editableTextInterface()) editableTextIface->deleteText(startOffset, endOffset); else replaceTextFallback(interface, startOffset, endOffset, QString()); connection.send(message.createReply(true)); } else if (function == "InsertText"_L1) { int position = message.arguments().at(0).toInt(); QString text = message.arguments().at(1).toString(); int length = message.arguments().at(2).toInt(); text.resize(length); if (QAccessibleEditableTextInterface *editableTextIface = interface->editableTextInterface()) editableTextIface->insertText(position, text); else replaceTextFallback(interface, position, position, text); connection.send(message.createReply(true)); } else if (function == "PasteText"_L1) { #ifndef QT_NO_CLIPBOARD int position = message.arguments().at(0).toInt(); const QString txt = QGuiApplication::clipboard()->text(); if (QAccessibleEditableTextInterface *editableTextIface = interface->editableTextInterface()) editableTextIface->insertText(position, txt); else replaceTextFallback(interface, position, position, txt); #endif connection.send(message.createReply(true)); } else if (function == "SetTextContents"_L1) { QString newContents = message.arguments().at(0).toString(); if (QAccessibleEditableTextInterface *editableTextIface = interface->editableTextInterface()) editableTextIface->replaceText(0, interface->textInterface()->characterCount(), newContents); else replaceTextFallback(interface, 0, -1, newContents); connection.send(message.createReply(true)); } else if (function.isEmpty()) { connection.send(message.createReply()); } else { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::editableTextInterface does not implement" << function << message.path(); return false; } return true; } // Value interface bool AtSpiAdaptor::valueInterface(QAccessibleInterface *interface, const QString &function, const QDBusMessage &message, const QDBusConnection &connection) { QAccessibleValueInterface *valueIface = interface->valueInterface(); if (!valueIface) return false; if (function == "SetCurrentValue"_L1) { QDBusVariant v = qvariant_cast(message.arguments().at(2)); double value = v.variant().toDouble(); //Temporary fix //See https://bugzilla.gnome.org/show_bug.cgi?id=652596 valueIface->setCurrentValue(value); connection.send(message.createReply()); } else { QVariant value; if (function == "GetCurrentValue"_L1) value = valueIface->currentValue(); else if (function == "GetMaximumValue"_L1) value = valueIface->maximumValue(); else if (function == "GetMinimumIncrement"_L1) value = valueIface->minimumStepSize(); else if (function == "GetMinimumValue"_L1) value = valueIface->minimumValue(); else { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::valueInterface does not implement" << function << message.path(); return false; } if (!value.canConvert()) { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::valueInterface: Could not convert to double:" << function; } // explicitly convert to dbus-variant containing one double since atspi expects that // everything else might fail to convert back on the other end connection.send(message.createReply( QVariant::fromValue(QDBusVariant(QVariant::fromValue(value.toDouble()))))); } return true; } // Selection interface bool AtSpiAdaptor::selectionInterface(QAccessibleInterface *interface, const QString &function, const QDBusMessage &message, const QDBusConnection &connection) { QAccessibleSelectionInterface* selectionInterface = interface->selectionInterface(); if (!selectionInterface) { qCWarning(lcAccessibilityAtspi) << "Could not find selection interface for: " << message.path() << interface; return false; } if (function == "ClearSelection"_L1 ) { connection.send(message.createReply(QVariant::fromValue((selectionInterface->clear())))); } else if (function == "DeselectChild"_L1 ) { int childIndex = message.arguments().at(0).toInt(); bool ret = false; QAccessibleInterface *child = interface->child(childIndex); if (child) ret = selectionInterface->unselect(child); connection.send(message.createReply(QVariant::fromValue(ret))); } else if (function == "DeselectSelectedChild"_L1 ) { int selectionIndex = message.arguments().at(0).toInt(); bool ret = false; QAccessibleInterface *selectedChild = selectionInterface->selectedItem(selectionIndex); if (selectedChild) ret = selectionInterface->unselect(selectedChild); connection.send(message.createReply(QVariant::fromValue(ret))); } else if (function == "GetNSelectedChildren"_L1) { connection.send(message.createReply(QVariant::fromValue(QDBusVariant( QVariant::fromValue(selectionInterface->selectedItemCount()))))); } else if (function == "GetSelectedChild"_L1) { int selectionIndex = message.arguments().at(0).toInt(); QSpiObjectReference ref(connection, QDBusObjectPath(pathForInterface(selectionInterface->selectedItem(selectionIndex)))); connection.send(message.createReply(QVariant::fromValue(ref))); } else if (function == "IsChildSelected"_L1 ) { int childIndex = message.arguments().at(0).toInt(); bool ret = false; QAccessibleInterface *child = interface->child(childIndex); if (child) ret = selectionInterface->isSelected(child); connection.send(message.createReply(QVariant::fromValue(ret))); } else if (function == "SelectAll"_L1 ) { connection.send(message.createReply(QVariant::fromValue(selectionInterface->selectAll()))); } else if (function == "SelectChild"_L1 ) { int childIndex = message.arguments().at(0).toInt(); bool ret = false; QAccessibleInterface *child = interface->child(childIndex); if (child) ret = selectionInterface->select(child); connection.send(message.createReply(QVariant::fromValue(ret))); } else { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::selectionInterface does not implement " << function << message.path(); return false; } return true; } // Table interface bool AtSpiAdaptor::tableInterface(QAccessibleInterface *interface, const QString &function, const QDBusMessage &message, const QDBusConnection &connection) { if (!(interface->tableInterface() || interface->tableCellInterface())) { qCWarning(lcAccessibilityAtspi) << "Qt AtSpiAdaptor: Could not find table interface for:" << message.path() << interface; return false; } if (function == "GetCaption"_L1) { QAccessibleInterface * captionInterface= interface->tableInterface()->caption(); if (captionInterface) { QSpiObjectReference ref = QSpiObjectReference(connection, QDBusObjectPath(pathForInterface(captionInterface))); sendReply(connection, message, QVariant::fromValue(QDBusVariant(QVariant::fromValue(ref)))); } else { sendReply(connection, message, QVariant::fromValue(QDBusVariant(QVariant::fromValue( QSpiObjectReference(connection, QDBusObjectPath(ATSPI_DBUS_PATH_NULL)))))); } } else if (function == "GetNColumns"_L1) { connection.send(message.createReply(QVariant::fromValue(QDBusVariant( QVariant::fromValue(interface->tableInterface()->columnCount()))))); } else if (function == "GetNRows"_L1) { connection.send(message.createReply(QVariant::fromValue(QDBusVariant( QVariant::fromValue(interface->tableInterface()->rowCount()))))); } else if (function == "GetNSelectedColumns"_L1) { connection.send(message.createReply(QVariant::fromValue(QDBusVariant( QVariant::fromValue(interface->tableInterface()->selectedColumnCount()))))); } else if (function == "GetNSelectedRows"_L1) { connection.send(message.createReply(QVariant::fromValue(QDBusVariant( QVariant::fromValue(interface->tableInterface()->selectedRowCount()))))); } else if (function == "GetSummary"_L1) { QAccessibleInterface *summary = interface->tableInterface() ? interface->tableInterface()->summary() : nullptr; QSpiObjectReference ref(connection, QDBusObjectPath(pathForInterface(summary))); connection.send(message.createReply(QVariant::fromValue(QDBusVariant(QVariant::fromValue(ref))))); } else if (function == "GetAccessibleAt"_L1) { int row = message.arguments().at(0).toInt(); int column = message.arguments().at(1).toInt(); if ((row < 0) || (column < 0) || (row >= interface->tableInterface()->rowCount()) || (column >= interface->tableInterface()->columnCount())) { qCWarning(lcAccessibilityAtspi) << "Invalid index for tableInterface GetAccessibleAt (" << row << "," << column << ')'; return false; } QSpiObjectReference ref; QAccessibleInterface * cell(interface->tableInterface()->cellAt(row, column)); if (cell) { ref = QSpiObjectReference(connection, QDBusObjectPath(pathForInterface(cell))); } else { qCWarning(lcAccessibilityAtspi) << "No cell interface returned for" << interface->object() << row << column; ref = QSpiObjectReference(); } connection.send(message.createReply(QVariant::fromValue(ref))); } else if (function == "GetIndexAt"_L1) { int row = message.arguments().at(0).toInt(); int column = message.arguments().at(1).toInt(); QAccessibleInterface *cell = interface->tableInterface()->cellAt(row, column); if (!cell) { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::GetIndexAt(" << row << ',' << column << ") did not find a cell." << interface; return false; } int index = interface->indexOfChild(cell); qCDebug(lcAccessibilityAtspi) << "QSpiAdaptor::GetIndexAt row:" << row << " col:" << column << " logical index:" << index; Q_ASSERT(index > 0); connection.send(message.createReply(index)); } else if ((function == "GetColumnAtIndex"_L1) || (function == "GetRowAtIndex"_L1)) { int index = message.arguments().at(0).toInt(); int ret = -1; if (index >= 0) { QAccessibleInterface * cell = interface->child(index); if (cell) { if (function == "GetColumnAtIndex"_L1) { if (cell->role() == QAccessible::ColumnHeader) { ret = index; } else if (cell->role() == QAccessible::RowHeader) { ret = -1; } else { if (!cell->tableCellInterface()) { qCWarning(lcAccessibilityAtspi).nospace() << "AtSpiAdaptor::" << function << " No table cell interface: " << cell; return false; } ret = cell->tableCellInterface()->columnIndex(); } } else { if (cell->role() == QAccessible::ColumnHeader) { ret = -1; } else if (cell->role() == QAccessible::RowHeader) { ret = index % interface->tableInterface()->columnCount(); } else { if (!cell->tableCellInterface()) { qCWarning(lcAccessibilityAtspi).nospace() << "AtSpiAdaptor::" << function << " No table cell interface: " << cell; return false; } ret = cell->tableCellInterface()->rowIndex(); } } } else { qCWarning(lcAccessibilityAtspi).nospace() << "AtSpiAdaptor::" << function << " No cell at index: " << index << " " << interface; return false; } } connection.send(message.createReply(ret)); } else if (function == "GetColumnDescription"_L1) { int column = message.arguments().at(0).toInt(); connection.send(message.createReply(interface->tableInterface()->columnDescription(column))); } else if (function == "GetRowDescription"_L1) { int row = message.arguments().at(0).toInt(); connection.send(message.createReply(interface->tableInterface()->rowDescription(row))); } else if (function == "GetRowColumnExtentsAtIndex"_L1) { int index = message.arguments().at(0).toInt(); bool success = false; int row = -1; int col = -1; int rowExtents = -1; int colExtents = -1; bool isSelected = false; int cols = interface->tableInterface()->columnCount(); if (cols > 0) { row = index / cols; col = index % cols; if (QAccessibleInterface *cell = interface->tableInterface()->cellAt(row, col)) { if (QAccessibleTableCellInterface *cellIface = cell->tableCellInterface()) { row = cellIface->rowIndex(); col = cellIface->columnIndex(); rowExtents = cellIface->rowExtent(); colExtents = cellIface->columnExtent(); isSelected = cellIface->isSelected(); success = true; } } } QVariantList list; list << success << row << col << rowExtents << colExtents << isSelected; connection.send(message.createReply(list)); } else if (function == "GetColumnExtentAt"_L1) { int row = message.arguments().at(0).toInt(); int column = message.arguments().at(1).toInt(); int columnExtent = 0; if (QAccessibleInterface *cell = interface->tableInterface()->cellAt(row, column)) { if (QAccessibleTableCellInterface *cellIface = cell->tableCellInterface()) columnExtent = cellIface->columnExtent(); } connection.send(message.createReply(columnExtent)); } else if (function == "GetRowExtentAt"_L1) { int row = message.arguments().at(0).toInt(); int column = message.arguments().at(1).toInt(); int rowExtent = 0; if (QAccessibleInterface *cell = interface->tableInterface()->cellAt(row, column)) { if (QAccessibleTableCellInterface *cellIface = cell->tableCellInterface()) rowExtent = cellIface->rowExtent(); } connection.send(message.createReply(rowExtent)); } else if (function == "GetColumnHeader"_L1) { int column = message.arguments().at(0).toInt(); QSpiObjectReference ref; QAccessibleInterface * cell(interface->tableInterface()->cellAt(0, column)); if (cell && cell->tableCellInterface()) { QList header = cell->tableCellInterface()->columnHeaderCells(); if (header.size() > 0) { ref = QSpiObjectReference(connection, QDBusObjectPath(pathForInterface(header.takeAt(0)))); } } connection.send(message.createReply(QVariant::fromValue(ref))); } else if (function == "GetRowHeader"_L1) { int row = message.arguments().at(0).toInt(); QSpiObjectReference ref; QAccessibleInterface *cell = interface->tableInterface()->cellAt(row, 0); if (cell && cell->tableCellInterface()) { QList header = cell->tableCellInterface()->rowHeaderCells(); if (header.size() > 0) { ref = QSpiObjectReference(connection, QDBusObjectPath(pathForInterface(header.takeAt(0)))); } } connection.send(message.createReply(QVariant::fromValue(ref))); } else if (function == "GetSelectedColumns"_L1) { connection.send(message.createReply(QVariant::fromValue(interface->tableInterface()->selectedColumns()))); } else if (function == "GetSelectedRows"_L1) { connection.send(message.createReply(QVariant::fromValue(interface->tableInterface()->selectedRows()))); } else if (function == "IsColumnSelected"_L1) { int column = message.arguments().at(0).toInt(); connection.send(message.createReply(interface->tableInterface()->isColumnSelected(column))); } else if (function == "IsRowSelected"_L1) { int row = message.arguments().at(0).toInt(); connection.send(message.createReply(interface->tableInterface()->isRowSelected(row))); } else if (function == "IsSelected"_L1) { int row = message.arguments().at(0).toInt(); int column = message.arguments().at(1).toInt(); bool selected = false; if (QAccessibleInterface* cell = interface->tableInterface()->cellAt(row, column)) { if (QAccessibleTableCellInterface *cellIface = cell->tableCellInterface()) selected = cellIface->isSelected(); } connection.send(message.createReply(selected)); } else if (function == "AddColumnSelection"_L1) { int column = message.arguments().at(0).toInt(); connection.send(message.createReply(interface->tableInterface()->selectColumn(column))); } else if (function == "AddRowSelection"_L1) { int row = message.arguments().at(0).toInt(); connection.send(message.createReply(interface->tableInterface()->selectRow(row))); } else if (function == "RemoveColumnSelection"_L1) { int column = message.arguments().at(0).toInt(); connection.send(message.createReply(interface->tableInterface()->unselectColumn(column))); } else if (function == "RemoveRowSelection"_L1) { int row = message.arguments().at(0).toInt(); connection.send(message.createReply(interface->tableInterface()->unselectRow(row))); } else { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::tableInterface does not implement" << function << message.path(); return false; } return true; } // Table cell interface bool AtSpiAdaptor::tableCellInterface(QAccessibleInterface *interface, const QString &function, const QDBusMessage &message, const QDBusConnection &connection) { QAccessibleTableCellInterface* cellInterface = interface->tableCellInterface(); if (!cellInterface) { qCWarning(lcAccessibilityAtspi) << "Could not find table cell interface for: " << message.path() << interface; return false; } if (function == "GetColumnHeaderCells"_L1) { QSpiObjectReferenceArray headerCells; const auto headerCellInterfaces = cellInterface->columnHeaderCells(); headerCells.reserve(headerCellInterfaces.size()); for (QAccessibleInterface *cell : headerCellInterfaces) { const QString childPath = pathForInterface(cell); const QSpiObjectReference ref(connection, QDBusObjectPath(childPath)); headerCells << ref; } connection.send(message.createReply(QVariant::fromValue(headerCells))); } else if (function == "GetColumnSpan"_L1) { connection.send(message.createReply(QVariant::fromValue(QDBusVariant( QVariant::fromValue(cellInterface->columnExtent()))))); } else if (function == "GetPosition"_L1) { const int row = cellInterface->rowIndex(); const int column = cellInterface->columnIndex(); connection.send(message.createReply(QVariant::fromValue(QDBusVariant( QVariant::fromValue(QPoint(row, column)))))); } else if (function == "GetRowHeaderCells"_L1) { QSpiObjectReferenceArray headerCells; const auto headerCellInterfaces = cellInterface->rowHeaderCells(); headerCells.reserve(headerCellInterfaces.size()); for (QAccessibleInterface *cell : headerCellInterfaces) { const QString childPath = pathForInterface(cell); const QSpiObjectReference ref(connection, QDBusObjectPath(childPath)); headerCells << ref; } connection.send(message.createReply(QVariant::fromValue(headerCells))); } else if (function == "GetRowSpan"_L1) { connection.send(message.createReply(QVariant::fromValue(QDBusVariant( QVariant::fromValue(cellInterface->rowExtent()))))); } else if (function == "GetRowColumnSpan"_L1) { QVariantList list; list << cellInterface->rowIndex() << cellInterface->columnIndex() << cellInterface->rowExtent() << cellInterface->columnExtent(); connection.send(message.createReply(list)); } else if (function == "GetTable"_L1) { QSpiObjectReference ref; QAccessibleInterface* table = cellInterface->table(); if (table && table->tableInterface()) ref = QSpiObjectReference(connection, QDBusObjectPath(pathForInterface(table))); connection.send(message.createReply(QVariant::fromValue(QDBusVariant(QVariant::fromValue(ref))))); } else { qCWarning(lcAccessibilityAtspi) << "AtSpiAdaptor::tableCellInterface does not implement" << function << message.path(); return false; } return true; } QT_END_NAMESPACE #include "moc_atspiadaptor_p.cpp" #endif // QT_CONFIG(accessibility)