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 <m.weghorn@posteo.de>
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Albert Astals Cid 2025-03-18 10:06:47 +01:00
parent 420db269fe
commit 0475bc57d6
4 changed files with 105 additions and 3 deletions

View File

@ -12,6 +12,8 @@
#include "qcocoaaccessibilityelement.h" #include "qcocoaaccessibilityelement.h"
#include <functional>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QCocoaAccessibility : public QPlatformAccessibility class QCocoaAccessibility : public QPlatformAccessibility
@ -49,7 +51,9 @@ namespace QCocoaAccessible {
NSString *macRole(QAccessibleInterface *interface); NSString *macRole(QAccessibleInterface *interface);
NSString *macSubrole(QAccessibleInterface *interface); NSString *macSubrole(QAccessibleInterface *interface);
bool shouldBeIgnored(QAccessibleInterface *interface); bool shouldBeIgnored(QAccessibleInterface *interface);
NSArray<QMacAccessibilityElement *> *unignoredChildren(QAccessibleInterface *interface); bool defaultUnignored(QAccessibleInterface *interface);
NSArray<QMacAccessibilityElement *> *unignoredChildren(QAccessibleInterface *interface,
const std::function<bool(QAccessibleInterface *)> &p = defaultUnignored);
NSString *getTranslatedAction(const QString &qtAction); NSString *getTranslatedAction(const QString &qtAction);
QString translateAction(NSString *nsAction, QAccessibleInterface *interface); QString translateAction(NSString *nsAction, QAccessibleInterface *interface);
bool hasValueAttribute(QAccessibleInterface *interface); bool hasValueAttribute(QAccessibleInterface *interface);

View File

@ -122,6 +122,7 @@ static void populateRoleMap()
roleMap[QAccessible::Separator] = NSAccessibilitySplitterRole; roleMap[QAccessible::Separator] = NSAccessibilitySplitterRole;
roleMap[QAccessible::ToolBar] = NSAccessibilityToolbarRole; roleMap[QAccessible::ToolBar] = NSAccessibilityToolbarRole;
roleMap[QAccessible::PageTab] = NSAccessibilityRadioButtonRole; roleMap[QAccessible::PageTab] = NSAccessibilityRadioButtonRole;
roleMap[QAccessible::PageTabList] = NSAccessibilityTabGroupRole;
roleMap[QAccessible::ButtonMenu] = NSAccessibilityMenuButtonRole; roleMap[QAccessible::ButtonMenu] = NSAccessibilityMenuButtonRole;
roleMap[QAccessible::ButtonDropDown] = NSAccessibilityPopUpButtonRole; roleMap[QAccessible::ButtonDropDown] = NSAccessibilityPopUpButtonRole;
roleMap[QAccessible::SpinBox] = NSAccessibilityIncrementorRole; roleMap[QAccessible::SpinBox] = NSAccessibilityIncrementorRole;
@ -200,6 +201,8 @@ NSString *macSubrole(QAccessibleInterface *interface)
return NSAccessibilitySearchFieldSubrole; return NSAccessibilitySearchFieldSubrole;
if (s.passwordEdit) if (s.passwordEdit)
return NSAccessibilitySecureTextFieldSubrole; return NSAccessibilitySecureTextFieldSubrole;
if (interface->role() == QAccessible::PageTab)
return NSAccessibilityTabButtonSubrole;
return nil; return nil;
} }
@ -249,14 +252,25 @@ bool shouldBeIgnored(QAccessibleInterface *interface)
return false; return false;
} }
NSArray<QMacAccessibilityElement *> *unignoredChildren(QAccessibleInterface *interface) bool defaultUnignored(QAccessibleInterface *child)
{
if (child && child->isValid()) {
const auto state = child->state();
return !state.invalid && !state.invisible;
}
return false;
}
NSArray<QMacAccessibilityElement *> *unignoredChildren(QAccessibleInterface *interface,
const std::function<bool(QAccessibleInterface *child)> &pred)
{ {
int numKids = interface->childCount(); int numKids = interface->childCount();
NSMutableArray<QMacAccessibilityElement *> *kids = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:numKids]; NSMutableArray<QMacAccessibilityElement *> *kids = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:numKids];
for (int i = 0; i < numKids; ++i) { for (int i = 0; i < numKids; ++i) {
QAccessibleInterface *child = interface->child(i); QAccessibleInterface *child = interface->child(i);
if (!child || !child->isValid() || child->state().invalid || child->state().invisible)
if (!pred(child))
continue; continue;
QAccessible::Id childId = QAccessible::uniqueId(child); QAccessible::Id childId = QAccessible::uniqueId(child);
@ -269,6 +283,7 @@ NSArray<QMacAccessibilityElement *> *unignoredChildren(QAccessibleInterface *int
} }
return NSAccessibilityUnignoredChildren(kids); return NSAccessibilityUnignoredChildren(kids);
} }
/* /*
Translates a predefined QAccessibleActionInterface action to a Mac action constant. 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 Returns 0 if the Qt Action has no mac equivalent. Ownership of the NSString is

View File

@ -1093,6 +1093,19 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of
return nil; 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 @end
#endif // QT_CONFIG(accessibility) #endif // QT_CONFIG(accessibility)

View File

@ -66,6 +66,7 @@ QDebug operator<<(QDebug dbg, AXErrorTag err)
bool axError; bool axError;
} }
@property (readonly) NSString *role; @property (readonly) NSString *role;
@property (readonly) NSString *roleDescription;
@property (readonly) NSString *title; @property (readonly) NSString *title;
@property (readonly) NSString *description; @property (readonly) NSString *description;
@property (readonly) NSString *value; @property (readonly) NSString *value;
@ -153,6 +154,21 @@ QDebug operator<<(QDebug dbg, AXErrorTag err)
return arr; 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 - (AXUIElementRef) findDirectChildByRole: (CFStringRef) role
{ {
TestAXObject *result = nil; TestAXObject *result = nil;
@ -353,6 +369,7 @@ QDebug operator<<(QDebug dbg, AXErrorTag err)
- (bool) valid { return reference != nil; } - (bool) valid { return reference != nil; }
- (NSString*) role { return [self _stringAttributeValue:kAXRoleAttribute]; } - (NSString*) role { return [self _stringAttributeValue:kAXRoleAttribute]; }
- (NSString*) roleDescription { return [self _stringAttributeValue:kAXRoleDescriptionAttribute]; }
- (NSString*) title { return [self _stringAttributeValue:kAXTitleAttribute]; } - (NSString*) title { return [self _stringAttributeValue:kAXTitleAttribute]; }
- (NSString*) description { return [self _stringAttributeValue:kAXDescriptionAttribute]; } - (NSString*) description { return [self _stringAttributeValue:kAXDescriptionAttribute]; }
- (NSString*) value { return [self _stringAttributeValue:kAXValueAttribute]; } - (NSString*) value { return [self _stringAttributeValue:kAXValueAttribute]; }
@ -431,6 +448,7 @@ private Q_SLOTS:
void checkBoxTest(); void checkBoxTest();
void tableViewTest(); void tableViewTest();
void treeViewTest(); void treeViewTest();
void tabBarTest();
private: private:
AccessibleTestWindow *m_window; AccessibleTestWindow *m_window;
@ -863,5 +881,57 @@ void tst_QAccessibilityMac::treeViewTest()
[window release]; [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) QTEST_MAIN(tst_QAccessibilityMac)
#include "tst_qaccessibilitymac.moc" #include "tst_qaccessibilitymac.moc"