Revision control

++ // Copyright (C) 2025 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 "qspimatchrulematcher_p.h"
++
++ #if QT_CONFIG(accessibility)
++
++ #include "atspiadaptor_p.h"
++ #include "qspi_constant_mappings_p.h"
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ Q_STATIC_LOGGING_CATEGORY(lcAccessibilityAtspi, "qt.accessibility.atspi")
++
++ QSpiMatchRuleMatcher::QSpiMatchRuleMatcher(const QSpiMatchRule &matchRule)
++ : m_states(spiStatesFromSpiStateSet(matchRule.states)),
++ m_stateMatchType(matchRule.stateMatchType),
++ m_attributes(matchRule.attributes),
++ m_attributeMatchType(matchRule.attributeMatchType),
++ m_roleMatchType(matchRule.roleMatchType),
++ m_interfaceMatchType(matchRule.interfaceMatchType)
++ {
++ // extract roles encoded in bitset stored in multiple 32 bit integers
++ for (qsizetype i = 0; i < matchRule.roles.size(); ++i) {
++ const auto role = matchRule.roles.at(i);
++ for (int j = 0; j < 32; j++) {
++ if (role & (1 << j)) {
++ const auto atspiRole = i * 32 + j;
++ if (atspiRole < ATSPI_ROLE_LAST_DEFINED)
++ m_roles.insert(AtspiRole(atspiRole));
++ else
++ qCWarning(lcAccessibilityAtspi)
++ << "Ignoring invalid AT-SPI role value" << atspiRole;
++ }
++ }
++ }
++
++ // use qualified interface names to match what AtSpiAdaptor::accessibleInterfaces returns
++ m_interfaces.reserve(matchRule.interfaces.size());
++ for (const QString &ifaceName : matchRule.interfaces)
++ m_interfaces.push_back("org.a11y.atspi."_L1 + ifaceName);
++ }
++
++ bool QSpiMatchRuleMatcher::matchAttributes(QAccessibleInterface &iface) const
++ {
++ switch (m_attributeMatchType) {
++ case ATSPI_Collection_MATCH_EMPTY:
++ if (m_attributes.empty())
++ return AtSpiAdaptor::getAttributes(&iface).isEmpty();
++ [[fallthrough]];
++ case ATSPI_Collection_MATCH_ALL: {
++ if (m_attributes.empty())
++ return true;
++ const QSpiAttributeSet attributes = AtSpiAdaptor::getAttributes(&iface);
++ for (const auto &[key, value] : m_attributes.asKeyValueRange()) {
++ if (!attributes.contains(key) || attributes[key] != value)
++ return false;
++ }
++ return true;
++ }
++ case ATSPI_Collection_MATCH_ANY: {
++ const QSpiAttributeSet attributes = AtSpiAdaptor::getAttributes(&iface);
++ for (const auto &[key, value] : m_attributes.asKeyValueRange()) {
++ if (attributes.contains(key) && attributes[key] == value)
++ return true;
++ }
++ return false;
++ }
++ case ATSPI_Collection_MATCH_NONE: {
++ const QSpiAttributeSet attributes = AtSpiAdaptor::getAttributes(&iface);
++ for (const auto &[key, value] : m_attributes.asKeyValueRange()) {
++ if (attributes.contains(key) && attributes[key] == value)
++ return false;
++ }
++ return true;
++ }
++ default:
++ qCWarning(lcAccessibilityAtspi)
++ << "QSpiMatchRuleMatcher::matchAttributes called with invalid match type "
++ << m_attributeMatchType;
++ return false;
++ }
++ }
++
++ bool QSpiMatchRuleMatcher::matchInterfaces(QAccessibleInterface &iface) const
++ {
++ switch (m_interfaceMatchType) {
++ case ATSPI_Collection_MATCH_EMPTY:
++ if (m_interfaces.empty())
++ return AtSpiAdaptor::accessibleInterfaces(&iface).isEmpty();
++ [[fallthrough]];
++ case ATSPI_Collection_MATCH_ALL: {
++ if (m_interfaces.empty())
++ return true;
++ const QStringList interfaces = AtSpiAdaptor::accessibleInterfaces(&iface);
++ for (const QString &atSpiInterface : m_interfaces) {
++ if (!interfaces.contains(atSpiInterface))
++ return false;
++ }
++ return true;
++ }
++ case ATSPI_Collection_MATCH_ANY: {
++ const QStringList interfaces = AtSpiAdaptor::accessibleInterfaces(&iface);
++ for (const QString &atSpiInterface : m_interfaces) {
++ if (interfaces.contains(atSpiInterface))
++ return true;
++ }
++ return false;
++ }
++ case ATSPI_Collection_MATCH_NONE: {
++ const QStringList interfaces = AtSpiAdaptor::accessibleInterfaces(&iface);
++ for (const QString &atSpiInterface : m_interfaces) {
++ if (interfaces.contains(atSpiInterface))
++ return false;
++ }
++ return true;
++ }
++ default:
++ qCWarning(lcAccessibilityAtspi)
++ << "QSpiMatchRuleMatcher::matchInterfaces called with invalid match type "
++ << m_interfaceMatchType;
++ return false;
++ }
++ }
++
++ bool QSpiMatchRuleMatcher::matchRoles(QAccessibleInterface &iface) const
++ {
++ switch (m_roleMatchType) {
++ case ATSPI_Collection_MATCH_EMPTY:
++ if (m_roles.empty())
++ // accessible always has exactly one role, i.e. can't have no roles
++ return false;
++ [[fallthrough]];
++ case ATSPI_Collection_MATCH_ALL:
++ if (m_roles.empty())
++ return true;
++ if (m_roles.size() > 1)
++ // accessible only has a single role
++ return false;
++ [[fallthrough]];
++ case ATSPI_Collection_MATCH_ANY:
++ return m_roles.find(AtSpiAdaptor::getRole(&iface)) != m_roles.end();
++ case ATSPI_Collection_MATCH_NONE:
++ return m_roles.find(AtSpiAdaptor::getRole(&iface)) == m_roles.end();
++ default:
++ qCWarning(lcAccessibilityAtspi)
++ << "QSpiMatchRuleMatcher::matchRoles called with invalid match type "
++ << m_roleMatchType;
++ return false;
++ }
++ }
++
++ bool QSpiMatchRuleMatcher::matchStates(QAccessibleInterface &iface) const
++ {
++ switch (m_stateMatchType) {
++ case ATSPI_Collection_MATCH_EMPTY:
++ if (m_states == 0)
++ return spiStatesFromQState(iface.state()) == 0;
++ [[fallthrough]];
++ case ATSPI_Collection_MATCH_ALL:
++ return (spiStatesFromQState(iface.state()) & m_states) == m_states;
++ case ATSPI_Collection_MATCH_ANY:
++ return (spiStatesFromQState(iface.state()) & m_states) != 0;
++ case ATSPI_Collection_MATCH_NONE:
++ return (spiStatesFromQState(iface.state()) & m_states) == 0;
++ default:
++ qCWarning(lcAccessibilityAtspi)
++ << "QSpiMatchRuleMatcher::matchStates called with invalid match type "
++ << m_stateMatchType;
++ return false;
++ }
++ }
++
++ bool QSpiMatchRuleMatcher::match(QAccessibleInterface &iface) const
++ {
++ return matchRoles(iface) && matchStates(iface) && matchInterfaces(iface)
++ && matchAttributes(iface);
++ }
++
++ QT_END_NAMESPACE
++
++ #endif // QT_CONFIG(accessibility)