From 0475bc57d699ac3bc82e2970b97ff267922bd15e Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Tue, 18 Mar 2025 10:06:47 +0100 Subject: [PATCH] Improve macos tabs accessbility support Map PageTabList controls (i.e. QTabBar) to the TabGroup role, and report the list of PageTabs as the tabs. Add a predicate-argument to the unignoredChildren() function so that we can re-use that logic to return the list of tabs, while also ignoring otherwise accessible children, such as the scroll buttons. Change-Id: I57472e9213dd178e018449e542de276051097073 Fixes: QTBUG-134807 Pick-to: 6.9 6.8 Reviewed-by: Michael Weghorn Reviewed-by: Volker Hilsheimer --- .../platforms/cocoa/qcocoaaccessibility.h | 6 +- .../platforms/cocoa/qcocoaaccessibility.mm | 19 ++++- .../cocoa/qcocoaaccessibilityelement.mm | 13 ++++ .../tst_qaccessibilitymac.mm | 70 +++++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibility.h b/src/plugins/platforms/cocoa/qcocoaaccessibility.h index cdd9aafc801..0cb63098c83 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibility.h +++ b/src/plugins/platforms/cocoa/qcocoaaccessibility.h @@ -12,6 +12,8 @@ #include "qcocoaaccessibilityelement.h" +#include + QT_BEGIN_NAMESPACE class QCocoaAccessibility : public QPlatformAccessibility @@ -49,7 +51,9 @@ namespace QCocoaAccessible { NSString *macRole(QAccessibleInterface *interface); NSString *macSubrole(QAccessibleInterface *interface); bool shouldBeIgnored(QAccessibleInterface *interface); -NSArray *unignoredChildren(QAccessibleInterface *interface); +bool defaultUnignored(QAccessibleInterface *interface); +NSArray *unignoredChildren(QAccessibleInterface *interface, + const std::function &p = defaultUnignored); NSString *getTranslatedAction(const QString &qtAction); QString translateAction(NSString *nsAction, QAccessibleInterface *interface); bool hasValueAttribute(QAccessibleInterface *interface); diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm index 4aace8838e9..c825919e38e 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm @@ -122,6 +122,7 @@ static void populateRoleMap() roleMap[QAccessible::Separator] = NSAccessibilitySplitterRole; roleMap[QAccessible::ToolBar] = NSAccessibilityToolbarRole; roleMap[QAccessible::PageTab] = NSAccessibilityRadioButtonRole; + roleMap[QAccessible::PageTabList] = NSAccessibilityTabGroupRole; roleMap[QAccessible::ButtonMenu] = NSAccessibilityMenuButtonRole; roleMap[QAccessible::ButtonDropDown] = NSAccessibilityPopUpButtonRole; roleMap[QAccessible::SpinBox] = NSAccessibilityIncrementorRole; @@ -200,6 +201,8 @@ NSString *macSubrole(QAccessibleInterface *interface) return NSAccessibilitySearchFieldSubrole; if (s.passwordEdit) return NSAccessibilitySecureTextFieldSubrole; + if (interface->role() == QAccessible::PageTab) + return NSAccessibilityTabButtonSubrole; return nil; } @@ -249,14 +252,25 @@ bool shouldBeIgnored(QAccessibleInterface *interface) return false; } -NSArray *unignoredChildren(QAccessibleInterface *interface) +bool defaultUnignored(QAccessibleInterface *child) +{ + if (child && child->isValid()) { + const auto state = child->state(); + return !state.invalid && !state.invisible; + } + return false; +} + +NSArray *unignoredChildren(QAccessibleInterface *interface, + const std::function &pred) { int numKids = interface->childCount(); NSMutableArray *kids = [NSMutableArray arrayWithCapacity:numKids]; for (int i = 0; i < numKids; ++i) { QAccessibleInterface *child = interface->child(i); - if (!child || !child->isValid() || child->state().invalid || child->state().invisible) + + if (!pred(child)) continue; QAccessible::Id childId = QAccessible::uniqueId(child); @@ -269,6 +283,7 @@ NSArray *unignoredChildren(QAccessibleInterface *int } return NSAccessibilityUnignoredChildren(kids); } + /* Translates a predefined QAccessibleActionInterface action to a Mac action constant. Returns 0 if the Qt Action has no mac equivalent. Ownership of the NSString is diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm index 19134f4b376..2d062c25387 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm @@ -1093,6 +1093,19 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of return nil; } +// tabs + +- (NSArray *) accessibilityTabs { + QAccessibleInterface *iface = self.qtInterface; + if (iface && iface->role() == QAccessible::PageTabList) { + return QCocoaAccessible::unignoredChildren(iface, [](QAccessibleInterface *child){ + return QCocoaAccessible::defaultUnignored(child) + && child->role() == QAccessible::PageTab; + }); + } + return nil; +} + @end #endif // QT_CONFIG(accessibility) diff --git a/tests/auto/other/qaccessibilitymac/tst_qaccessibilitymac.mm b/tests/auto/other/qaccessibilitymac/tst_qaccessibilitymac.mm index 63971b0bf0b..146fa08a65d 100644 --- a/tests/auto/other/qaccessibilitymac/tst_qaccessibilitymac.mm +++ b/tests/auto/other/qaccessibilitymac/tst_qaccessibilitymac.mm @@ -66,6 +66,7 @@ QDebug operator<<(QDebug dbg, AXErrorTag err) bool axError; } @property (readonly) NSString *role; + @property (readonly) NSString *roleDescription; @property (readonly) NSString *title; @property (readonly) NSString *description; @property (readonly) NSString *value; @@ -153,6 +154,21 @@ QDebug operator<<(QDebug dbg, AXErrorTag err) return arr; } +- (NSArray *)tabs +{ + NSArray *arr; + AXError err; + + if (kAXErrorSuccess != (err = AXUIElementCopyAttributeValues(reference, kAXTabsAttribute, + 0, 100, /*min, max*/ + (CFArrayRef *) &arr))) { + axError = true; + qDebug() << "AXUIElementCopyAttributeValue(kAXTabsAttribute) returned error = " + << AXErrorTag(err) << "with reference" << reference; + } + return arr; +} + - (AXUIElementRef) findDirectChildByRole: (CFStringRef) role { TestAXObject *result = nil; @@ -353,6 +369,7 @@ QDebug operator<<(QDebug dbg, AXErrorTag err) - (bool) valid { return reference != nil; } - (NSString*) role { return [self _stringAttributeValue:kAXRoleAttribute]; } +- (NSString*) roleDescription { return [self _stringAttributeValue:kAXRoleDescriptionAttribute]; } - (NSString*) title { return [self _stringAttributeValue:kAXTitleAttribute]; } - (NSString*) description { return [self _stringAttributeValue:kAXDescriptionAttribute]; } - (NSString*) value { return [self _stringAttributeValue:kAXValueAttribute]; } @@ -431,6 +448,7 @@ private Q_SLOTS: void checkBoxTest(); void tableViewTest(); void treeViewTest(); + void tabBarTest(); private: AccessibleTestWindow *m_window; @@ -863,5 +881,57 @@ void tst_QAccessibilityMac::treeViewTest() [window release]; } +void tst_QAccessibilityMac::tabBarTest() +{ + QTabBar *tbar = new QTabBar; + static const unsigned int nTabs = 20; + for (unsigned int i = 0; i < nTabs; ++i) + tbar->addTab(QString::number(i)); + tbar->setUsesScrollButtons(true); + + m_window->addWidget(tbar); + QVERIFY(QTest::qWaitForWindowExposed(m_window)); + QCoreApplication::processEvents(); + + TestAXObject *appObject = [TestAXObject getApplicationAXObject]; + QVERIFY(appObject); + + NSArray *windowList = [appObject windowList]; + // one window + QVERIFY([windowList count] == 1); + AXUIElementRef windowRef = (AXUIElementRef)[windowList objectAtIndex:0]; + QVERIFY(windowRef != nil); + TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef:windowRef]; + QVERIFY(window.valid); + + // children of window + AXUIElementRef axTarBar = [window findDirectChildByRole:kAXTabGroupRole]; + QVERIFY(axTarBar != nil); + + TestAXObject *tb = [[TestAXObject alloc] initWithAXUIElementRef:axTarBar]; + QVERIFY(tb.valid); + + [appObject release]; + [window release]; + + NSArray *tbChildList = [tb childList]; + // +2 because of the scroll buttons + QCOMPARE([tbChildList count], nTabs + 2); + + NSArray *tbTabsList = [tb tabs]; + QCOMPARE([tbTabsList count], nTabs); + + for (unsigned int i = 0; i < nTabs; ++i) { + AXUIElementRef axTab = (AXUIElementRef)[tbTabsList objectAtIndex:i]; + QVERIFY(axTab != nil); + + TestAXObject *tab = [[TestAXObject alloc] initWithAXUIElementRef:axTab]; + QVERIFY(tab.valid); + QCOMPARE(QString::fromNSString(tab.role), QString::fromCFString(kAXRadioButtonRole)); + QCOMPARE(QString::fromNSString(tab.title), QString::number(i)); + QCOMPARE(QString::fromNSString(tab.roleDescription), "tab"); + } +} + QTEST_MAIN(tst_QAccessibilityMac) #include "tst_qaccessibilitymac.moc"