qt5base-lts/tests/auto/other/qaccessibilitymac/tst_qaccessibilitymac.mm

737 lines
26 KiB
Plaintext
Raw Normal View History

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QApplication>
#include <QtWidgets>
#include <QTest>
#include <QtCore/qcoreapplication.h>
// some versions of CALayer.h use 'slots' as an identifier
#define QT_NO_KEYWORDS
#include <QtWidgets/qapplication.h>
#include <QtWidgets/qlineedit.h>
#include <QtWidgets/qpushbutton.h>
#include <QtWidgets>
#include <QTest>
#include <unistd.h>
#import <AppKit/AppKit.h>
#import <ApplicationServices/ApplicationServices.h>
QT_USE_NAMESPACE
struct AXErrorTag {
AXError err;
explicit AXErrorTag(AXError theErr) : err(theErr) {}
};
QDebug operator<<(QDebug dbg, AXErrorTag err)
{
QDebugStateSaver saver(dbg);
const char *errDesc = 0;
const char *errName = 0;
switch (err.err) {
#define HANDLE_ERR(error, desc) case kAXError##error: errName = "kAXError" #error; errDesc = desc; break
HANDLE_ERR(Success, "Success");
HANDLE_ERR(Failure, "A system error occurred, such as the failure to allocate an object.");
HANDLE_ERR(IllegalArgument, "An illegal argument was passed to the function.");
HANDLE_ERR(InvalidUIElement, "The AXUIElementRef passed to the function is invalid.");
HANDLE_ERR(InvalidUIElementObserver, "The AXObserverRef passed to the function is not a valid observer.");
HANDLE_ERR(CannotComplete, "The function cannot complete because messaging failed in some way or because the application with which the function is communicating is busy or unresponsive.");
HANDLE_ERR(AttributeUnsupported, "The attribute is not supported by the AXUIElementRef.");
HANDLE_ERR(ActionUnsupported, "The action is not supported by the AXUIElementRef.");
HANDLE_ERR(NotificationUnsupported, "The notification is not supported by the AXUIElementRef.");
HANDLE_ERR(NotImplemented, "Indicates that the function or method is not implemented (this can be returned if a process does not support the accessibility API).");
HANDLE_ERR(NotificationAlreadyRegistered, "This notification has already been registered for.");
HANDLE_ERR(NotificationNotRegistered, "Indicates that a notification is not registered yet.");
HANDLE_ERR(APIDisabled, "The accessibility API is disabled (as when, for example, the user deselects \"Enable access for assistive devices\" in Universal Access Preferences).");
HANDLE_ERR(NoValue, "The requested value or AXUIElementRef does not exist.");
HANDLE_ERR(ParameterizedAttributeUnsupported, "The parameterized attribute is not supported by the AXUIElementRef.");
HANDLE_ERR(NotEnoughPrecision, "Not enough precision.");
default: errName = "<unknown error>"; errDesc = "UNKNOWN ERROR"; break;
}
#undef HANDLE_ERR
dbg.nospace() << "AXError(value=" << err.err << ", name=" << errName << ", description=\"" << errDesc << "\")";
return dbg;
}
@interface TestAXObject : NSObject
{
AXUIElementRef reference;
}
@property (readonly) NSString *role;
@property (readonly) NSString *title;
@property (readonly) NSString *description;
@property (readonly) NSString *value;
@property (readonly) CGRect rect;
@property (readonly) NSArray *actions;
@end
@implementation TestAXObject
- (instancetype)initWithAXUIElementRef:(AXUIElementRef)ref {
if ((self = [super init])) {
reference = ref;
}
return self;
}
- (AXUIElementRef) ref { return reference; }
- (void) print {
NSLog(@"Accessible Object role: '%@', title: '%@', description: '%@', value: '%@', rect: '%@'", self.role, self.title, self.description, self.value, NSStringFromRect(NSRectFromCGRect(self.rect)));
NSLog(@" Children: %ld", [[self childList] count]);
}
- (NSArray*) windowList
{
NSArray *list;
AXUIElementCopyAttributeValues(
reference,
kAXWindowsAttribute,
0, 100, /*min, max*/
(CFArrayRef *) &list);
return list;
}
- (NSArray*) childList
{
NSArray *list;
AXUIElementCopyAttributeValues(
reference,
kAXChildrenAttribute,
0, 100, /*min, max*/
(CFArrayRef *) &list);
return list;
}
a11y: Add support for Tables in macOS bridge For a11y purposes, a table needs to be mapped into a logical accessibility hierarchy. There are several ways of doing this mapping, and unfortunately macOS expects something different than what QAccessibleInterface does. So suppose we have a a 2x2 QTableView with both horizontal and vertical header like this (the names reflect the QAccessible::Role names): +-----------+--------------+--------------+ | | ColumnHeader | ColumnHeader | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ In order to be presented to the screen reader on a platform, it goes through two rounds of mapping: QAccessibleInterface will have all headers and cells as direct children of the table: - Table +- ColumnHeader +- ColumnHeader +- RowHeader +- Cell +- Cell +- RowHeader +- Cell +- Cell macOS expects a deeper hierarchy: - AXTable [QAccessible::Table] +- AXRow [Qt:no eqiuivalent] +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXRow +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXColumn (this seems to just store the geometry of the column) +- AXColumn (this seems to just store the geometry of the column) +- AXGroup (this represents the column headers) +- AXSortButton (clicking a header cell will trigger sorting) +- AXSortButton (clicking a header cell will trigger sorting) It's unclear to me how RowHeaders are mapped (they are rarer than ColumnHeaders, I expect to find them in e.g. spreadsheet applications). I haven't found any native usage of them. So this patch simply ignores them. Notice that macOS have a three layer deep hierarchy to represent a table (Table->Row->Cell), while QAccessibleInterface has a two-layer deep hierarchy (Table->Row/Cell). In the macOS bridge we therefore need to "inject" the Row/Column element to be "between" the table and the cell. The table will take ownership of all row and column elements that are children of the table. These elements are not inserted into the cache (it would be pointless, since the cache is basically just a mapping between the QAccessibleInterface -> QMacAccessibilityElement, and the row and column elements does not have a corresponding QAccessibleInterface to be mapped from). The rows and columns are therefore also created as soon as the table element is initialized, and they are stored in two NSMutableArray members of QMacAccessibilityElement. A table is constructed like any other accessibility element, with a valid axid and synthesizedRole set to nil. Each child row and column element is constructed with the same axid as the parent table element, and they will have the synthesizedRole set to either NSAccessibilityRow or NSAccessibilityColumn. With the synthesizedRole member we can then identify if we are a row, column or the actual table, and implement their respective behaviors. Notice that the child row/column is created with the parent's table axid in order for them to have a way of finding their parent table element. (there is no 'parent' member variable in QMacAccessibilityElement) This glorious scheme isn't pretty, but seems to work. Fixes: QTBUG-37207 Change-Id: I7c2451e629f5331b9a0ed61dc22c6e74a82cc173 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io> Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
2022-10-26 15:04:04 +00:00
- (NSArray *)tableRows
{
NSArray *arr;
AXUIElementCopyAttributeValues(
reference,
kAXRowsAttribute,
0, 100, /*min, max*/
(CFArrayRef *) &arr);
return arr;
}
- (NSArray *)tableColumns
{
NSArray *arr;
AXUIElementCopyAttributeValues(
reference,
kAXColumnsAttribute,
0, 100, /*min, max*/
(CFArrayRef *) &arr);
return arr;
}
- (AXUIElementRef) findDirectChildByRole: (CFStringRef) role
{
TestAXObject *result = nil;
NSArray *childList = [self childList];
for (id child in childList) {
TestAXObject *childObject = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)child];
if ([childObject.role isEqualToString:(NSString*)role]) {
result = childObject;
break;
}
}
AXUIElementRef ret = [result ref];
[result release];
return ret;
}
+ (TestAXObject *) getApplicationAXObject
{
pid_t pid = getpid();
AXUIElementRef appRef = AXUIElementCreateApplication(pid);
TestAXObject *appObject = [[TestAXObject alloc] initWithAXUIElementRef: appRef];
return appObject;
}
+ (NSInteger)_numberFromValue:(CFTypeRef)value
{
NSInteger number = -1;
if (!CFNumberGetValue((CFNumberRef)value, kCFNumberNSIntegerType, &number))
{
qDebug() << "Could not get NSInteger value out of CFNumberRef";
}
return number;
}
+ (BOOL)_boolFromValue:(CFTypeRef)value
{
return CFBooleanGetValue((CFBooleanRef)value);
}
+ (NSRange)_rangeFromValue:(CFTypeRef)value
{
CFRange cfRange;
NSRange range = NSMakeRange(0, 0);
if (!AXValueGetValue(AXValueRef(value), AXValueType(kAXValueCFRangeType), &cfRange))
qDebug() << "Could not get CFRange value out of AXValueRef";
else if (cfRange.location < 0 || cfRange.length < 0)
qDebug() << "Cannot convert CFRange with negative location or length to NSRange";
else if (static_cast<uintmax_t>(cfRange.location) > NSUIntegerMax || static_cast<uintmax_t>(cfRange.length) > NSUIntegerMax)
qDebug() << "Cannot convert CFRange with location or length out of bounds for NSUInteger";
else
{
range.length = static_cast<NSUInteger>(cfRange.length);
range.location = static_cast<NSUInteger>(cfRange.location);
}
return range;
}
+ (NSRect)_rectFromValue:(CFTypeRef)value
{
NSRect rect = NSMakeRect(0, 0, 0, 0);
if (!AXValueGetValue(AXValueRef(value), AXValueType(kAXValueCGRectType), reinterpret_cast<CGRect*>(&rect)))
{
qDebug() << "Could not get CGRect value out of AXValueRef";
}
return rect;
}
+ (NSPoint)_pointFromValue:(CFTypeRef)value
{
NSPoint point = NSMakePoint(0, 0);
if (!AXValueGetValue(AXValueRef(value), AXValueType(kAXValueCGPointType), reinterpret_cast<CGPoint*>(&point)))
{
qDebug() << "Could not get CGPoint value out of AXValueRef";
}
return point;
}
+ (NSSize)_sizeFromValue:(CFTypeRef)value
{
NSSize size = NSMakeSize(0, 0);
if (!AXValueGetValue(AXValueRef(value), AXValueType(kAXValueCGSizeType), reinterpret_cast<CGSize*>(&size)))
{
qDebug() << "Could not get CGSize value out of AXValueRef";
}
return size;
}
- (CFTypeRef)_attributeValue:(CFStringRef)attribute
{
CFTypeRef value = NULL;
AXError err;
if (kAXErrorSuccess != (err = AXUIElementCopyAttributeValue(reference, attribute, &value)))
{
qDebug() << "AXUIElementCopyAttributeValue(" << QString::fromCFString(attribute) << ") returned error = " << AXErrorTag(err);
}
return value;
}
- (NSString*)_stringAttributeValue:(CFStringRef)attribute
{
return (NSString*)[self _attributeValue:attribute];
}
- (NSInteger)_numberAttributeValue:(CFStringRef)attribute
{
return [[self class] _numberFromValue:[self _attributeValue:attribute]];
}
- (BOOL)_boolAttributeValue:(CFStringRef)attribute
{
return [[self class] _boolFromValue:[self _attributeValue:attribute]];
}
- (NSRange)_rangeAttributeValue:(CFStringRef)attribute
{
return [[self class] _rangeFromValue:[self _attributeValue:attribute]];
}
- (NSRect)_rectAttributeValue:(CFStringRef)attribute
{
return [[self class] _rectFromValue:[self _attributeValue:attribute]];
}
- (NSPoint)_pointAttributeValue:(CFStringRef)attribute
{
return [[self class] _pointFromValue:[self _attributeValue:attribute]];
}
- (NSSize)_sizeAttributeValue:(CFStringRef)attribute
{
return [[self class] _sizeFromValue:[self _attributeValue:attribute]];
}
- (CFTypeRef)_attributeValue:(CFStringRef)attribute forParameter:(CFTypeRef)parameter
{
CFTypeRef value = NULL;
AXError err;
if (kAXErrorSuccess != (err = AXUIElementCopyParameterizedAttributeValue(reference, attribute, parameter, &value)))
{
CFStringRef description = CFCopyDescription(parameter);
qDebug() << "AXUIElementCopyParameterizedAttributeValue(" << QString::fromCFString(attribute) << ", parameter=" << QString::fromCFString(description) << ") returned error = " << AXErrorTag(err);
CFRelease(description);
}
return value;
}
- (CFTypeRef)_attributeValue:(CFStringRef)attribute forRange:(NSRange)aRange
{
CFRange cfRange = CFRangeMake(aRange.location, aRange.length);
AXValueRef range = AXValueCreate(AXValueType(kAXValueCFRangeType), &cfRange);
CFTypeRef value = [self _attributeValue:attribute forParameter:range];
CFRelease(range);
return value;
}
- (CFTypeRef)_attributeValue:(CFStringRef)attribute forNumber:(NSInteger)aNumber
{
CFNumberRef number = CFNumberCreate(NULL, kCFNumberNSIntegerType, &aNumber);
CFTypeRef value = [self _attributeValue:attribute forParameter:number];
CFRelease(number);
return value;
}
- (CFTypeRef)_attributeValue:(CFStringRef)attribute forPoint:(CGPoint)aPoint
{
AXValueRef point = AXValueCreate(AXValueType(kAXValueCGPointType), &aPoint);
CFTypeRef value = [self _attributeValue:attribute forParameter:point];
CFRelease(point);
return value;
}
- (NSArray*)actions
{
AXError err;
CFArrayRef actions;
if (kAXErrorSuccess != (err = AXUIElementCopyActionNames(reference, &actions)))
{
qDebug() << "AXUIElementCopyActionNames(...) returned error = " << AXErrorTag(err);
}
return (NSArray*)actions;
}
- (void)performAction:(CFStringRef)action
{
AXError err;
if (kAXErrorSuccess != (err = AXUIElementPerformAction(reference, action)))
{
qDebug() << "AXUIElementPerformAction(" << QString::fromCFString(action) << ") returned error = " << AXErrorTag(err);
}
}
- (NSString*) role { return [self _stringAttributeValue:kAXRoleAttribute]; }
- (NSString*) title { return [self _stringAttributeValue:kAXTitleAttribute]; }
- (NSString*) description { return [self _stringAttributeValue:kAXDescriptionAttribute]; }
- (NSString*) value { return [self _stringAttributeValue:kAXValueAttribute]; }
- (NSInteger) valueNumber { return [self _numberAttributeValue:kAXValueAttribute]; }
- (NSRect) rect
{
NSRect rect;
rect.origin = [self _pointAttributeValue:kAXPositionAttribute];
rect.size = [self _sizeAttributeValue:kAXSizeAttribute];
return rect;
}
- (AXUIElementRef) parent { return (AXUIElementRef)[self _attributeValue:kAXParentAttribute]; }
- (BOOL) focused { return [self _boolAttributeValue:kAXFocusedAttribute]; }
- (NSInteger) numberOfCharacters { return [self _numberAttributeValue:kAXNumberOfCharactersAttribute]; }
- (NSString*) selectedText { return [self _stringAttributeValue:kAXSelectedTextAttribute]; }
- (NSRange) selectedTextRange { return [self _rangeAttributeValue:kAXSelectedTextRangeAttribute]; }
- (NSRange) visibleCharacterRange { return [self _rangeAttributeValue:kAXVisibleCharacterRangeAttribute]; }
- (NSString*) help { return [self _stringAttributeValue:kAXHelpAttribute]; }
- (NSInteger) insertionPointLineNumber { return [self _numberAttributeValue:kAXInsertionPointLineNumberAttribute]; }
- (NSInteger) lineForIndex:(NSInteger)index { return [[self class] _numberFromValue:[self _attributeValue:kAXLineForIndexParameterizedAttribute forNumber:index]]; }
- (NSRange) rangeForLine:(NSInteger)line { return [[self class] _rangeFromValue:[self _attributeValue:kAXRangeForLineParameterizedAttribute forNumber:line]]; }
- (NSString*) stringForRange:(NSRange)range { return (NSString*)[self _attributeValue:kAXStringForRangeParameterizedAttribute forRange:range]; }
- (NSAttributedString*) attributedStringForRange:(NSRange)range { return (NSAttributedString*)[self _attributeValue:kAXAttributedStringForRangeParameterizedAttribute forRange:range]; }
- (NSRect) boundsForRange:(NSRange)range { return [[self class] _rectFromValue:[self _attributeValue:kAXBoundsForRangeParameterizedAttribute forRange:range]]; }
- (NSRange) styleRangeForIndex:(NSInteger)index { return [[self class] _rangeFromValue:[self _attributeValue:kAXStyleRangeForIndexParameterizedAttribute forNumber:index]]; }
@end
QVector<int> notificationList;
void observerCallback(AXObserverRef /*observer*/, AXUIElementRef /*element*/, CFStringRef notification, void *)
{
if ([(NSString*)notification isEqualToString: NSAccessibilityFocusedUIElementChangedNotification])
notificationList.append(QAccessible::Focus);
else if ([(NSString*)notification isEqualToString: NSAccessibilityValueChangedNotification])
notificationList.append(QAccessible::ValueChanged);
else
notificationList.append(-1);
}
class AccessibleTestWindow : public QWidget
{
Q_OBJECT
public:
AccessibleTestWindow()
{
new QHBoxLayout(this);
}
void addWidget(QWidget* widget)
{
layout()->addWidget(widget);
widget->show();
QVERIFY(QTest::qWaitForWindowExposed(widget));
}
void clearChildren()
{
qDeleteAll(children());
new QHBoxLayout(this);
}
};
class tst_QAccessibilityMac : public QObject
{
Q_OBJECT
private Q_SLOTS:
void init();
void cleanup();
void singleWidgetTest();
void lineEditTest();
void hierarchyTest();
void notificationsTest();
void checkBoxTest();
a11y: Add support for Tables in macOS bridge For a11y purposes, a table needs to be mapped into a logical accessibility hierarchy. There are several ways of doing this mapping, and unfortunately macOS expects something different than what QAccessibleInterface does. So suppose we have a a 2x2 QTableView with both horizontal and vertical header like this (the names reflect the QAccessible::Role names): +-----------+--------------+--------------+ | | ColumnHeader | ColumnHeader | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ In order to be presented to the screen reader on a platform, it goes through two rounds of mapping: QAccessibleInterface will have all headers and cells as direct children of the table: - Table +- ColumnHeader +- ColumnHeader +- RowHeader +- Cell +- Cell +- RowHeader +- Cell +- Cell macOS expects a deeper hierarchy: - AXTable [QAccessible::Table] +- AXRow [Qt:no eqiuivalent] +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXRow +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXColumn (this seems to just store the geometry of the column) +- AXColumn (this seems to just store the geometry of the column) +- AXGroup (this represents the column headers) +- AXSortButton (clicking a header cell will trigger sorting) +- AXSortButton (clicking a header cell will trigger sorting) It's unclear to me how RowHeaders are mapped (they are rarer than ColumnHeaders, I expect to find them in e.g. spreadsheet applications). I haven't found any native usage of them. So this patch simply ignores them. Notice that macOS have a three layer deep hierarchy to represent a table (Table->Row->Cell), while QAccessibleInterface has a two-layer deep hierarchy (Table->Row/Cell). In the macOS bridge we therefore need to "inject" the Row/Column element to be "between" the table and the cell. The table will take ownership of all row and column elements that are children of the table. These elements are not inserted into the cache (it would be pointless, since the cache is basically just a mapping between the QAccessibleInterface -> QMacAccessibilityElement, and the row and column elements does not have a corresponding QAccessibleInterface to be mapped from). The rows and columns are therefore also created as soon as the table element is initialized, and they are stored in two NSMutableArray members of QMacAccessibilityElement. A table is constructed like any other accessibility element, with a valid axid and synthesizedRole set to nil. Each child row and column element is constructed with the same axid as the parent table element, and they will have the synthesizedRole set to either NSAccessibilityRow or NSAccessibilityColumn. With the synthesizedRole member we can then identify if we are a row, column or the actual table, and implement their respective behaviors. Notice that the child row/column is created with the parent's table axid in order for them to have a way of finding their parent table element. (there is no 'parent' member variable in QMacAccessibilityElement) This glorious scheme isn't pretty, but seems to work. Fixes: QTBUG-37207 Change-Id: I7c2451e629f5331b9a0ed61dc22c6e74a82cc173 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io> Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
2022-10-26 15:04:04 +00:00
void tableViewTest();
private:
AccessibleTestWindow *m_window;
};
void tst_QAccessibilityMac::init()
{
m_window = new AccessibleTestWindow();
m_window->setWindowTitle("Test window");
m_window->show();
m_window->resize(400, 400);
QVERIFY(QTest::qWaitForWindowExposed(m_window));
}
void tst_QAccessibilityMac::cleanup()
{
delete m_window;
}
void tst_QAccessibilityMac::singleWidgetTest()
{
delete m_window;
m_window = 0;
QLineEdit *le = new QLineEdit();
le->setText("button");
le->show();
QVERIFY(QTest::qWaitForWindowExposed(le));
QCoreApplication::processEvents();
TestAXObject *appObject = [TestAXObject getApplicationAXObject];
QVERIFY(appObject);
QTRY_VERIFY(appObject.windowList.count == 1);
AXUIElementRef windowRef = (AXUIElementRef) [appObject.windowList objectAtIndex: 0];
QVERIFY(windowRef != nil);
TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];
AXUIElementRef lineEditRef = [window findDirectChildByRole: kAXTextFieldRole];
QVERIFY(lineEditRef != nil);
TestAXObject *lineEdit = [[TestAXObject alloc] initWithAXUIElementRef: lineEditRef];
QVERIFY([[lineEdit value] isEqualToString:@"button"]);
// Access invalid reference, should return empty value
delete le;
QCoreApplication::processEvents();
TestAXObject *lineEditInvalid = [[TestAXObject alloc] initWithAXUIElementRef: lineEditRef];
QVERIFY([[lineEditInvalid value] length] == 0);
}
void tst_QAccessibilityMac::lineEditTest()
{
QLineEdit *lineEdit = new QLineEdit(m_window);
lineEdit->setText("a11y test QLineEdit");
m_window->addWidget(lineEdit);
QVERIFY(QTest::qWaitForWindowExposed(m_window));
QCoreApplication::processEvents();
TestAXObject *appObject = [TestAXObject getApplicationAXObject];
QVERIFY(appObject);
// one window
QTRY_VERIFY(appObject.windowList.count == 1);
AXUIElementRef windowRef = (AXUIElementRef) [appObject.windowList objectAtIndex: 0];
QVERIFY(windowRef != nil);
TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];
QVERIFY([window rect].size.width == 400);
// height of window includes title bar
QVERIFY([window rect].size.height >= 400);
QVERIFY([window.title isEqualToString:@"Test window"]);
// children of window:
AXUIElementRef lineEditElement = [window findDirectChildByRole: kAXTextFieldRole];
QVERIFY(lineEditElement != nil);
TestAXObject *le = [[TestAXObject alloc] initWithAXUIElementRef: lineEditElement];
NSString *value = @"a11y test QLineEdit";
QVERIFY([le.value isEqualToString:value]);
QVERIFY(value.length <= NSIntegerMax);
QVERIFY(le.numberOfCharacters == static_cast<NSInteger>(value.length));
const NSRange ranges[] = {
{ 0, 0},
{ 0, 1},
{ 0, 5},
{ 5, 0},
{ 5, 1},
{ 0, value.length},
{ value.length, 0},
};
for (size_t i = 0; i < sizeof(ranges)/sizeof(ranges[0]); ++i) {
NSRange range = ranges[i];
NSString *expectedSubstring = [value substringWithRange:range];
NSString *actualSubstring = [le stringForRange:range];
NSString *actualAttributedSubstring = [le attributedStringForRange:range].string;
QVERIFY([actualSubstring isEqualTo:expectedSubstring]);
QVERIFY([actualAttributedSubstring isEqualTo:expectedSubstring]);
}
}
void tst_QAccessibilityMac::hierarchyTest()
{
QWidget *w = new QWidget(m_window);
m_window->addWidget(w);
w->setLayout(new QVBoxLayout());
QPushButton *b = new QPushButton(w);
w->layout()->addWidget(b);
b->setText("I am a button");
QPushButton *b2 = new QPushButton(w);
w->layout()->addWidget(b2);
b2->setText("Button 2");
QVERIFY(QTest::qWaitForWindowExposed(m_window));
QCoreApplication::processEvents();
TestAXObject *appObject = [TestAXObject getApplicationAXObject];
QVERIFY(appObject);
// one window
QTRY_VERIFY(appObject.windowList.count == 1);
AXUIElementRef windowRef = (AXUIElementRef) [appObject.windowList objectAtIndex: 0];
QVERIFY(windowRef != nil);
TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];
// Because the plain widget is filtered out of the hierarchy, we expect the button
// to be a direct child of the window
AXUIElementRef buttonRef = [window findDirectChildByRole: kAXButtonRole];
QVERIFY(buttonRef != nil);
TestAXObject *buttonObject = [[TestAXObject alloc] initWithAXUIElementRef: buttonRef];
TestAXObject *parentObject = [[TestAXObject alloc] initWithAXUIElementRef: [buttonObject parent]];
// check that the parent is a window
QVERIFY([[parentObject role] isEqualToString: NSAccessibilityWindowRole]);
// test the focus
// child 0 is the layout, then button1 and 2
QPushButton *button1 = qobject_cast<QPushButton*>(w->children().at(1));
QVERIFY(button1);
QPushButton *button2 = qobject_cast<QPushButton*>(w->children().at(2));
QVERIFY(button2);
button2->setFocus();
AXUIElementRef systemWideElement = AXUIElementCreateSystemWide();
AXUIElementRef focussedElement = NULL;
AXError error = AXUIElementCopyAttributeValue(systemWideElement,
(CFStringRef)NSAccessibilityFocusedUIElementAttribute, (CFTypeRef*)&focussedElement);
QVERIFY(!error);
QVERIFY(focussedElement);
TestAXObject *focusButton2 = [[TestAXObject alloc] initWithAXUIElementRef: focussedElement];
QVERIFY([[focusButton2 role] isEqualToString: NSAccessibilityButtonRole]);
QVERIFY([[focusButton2 title] isEqualToString: @"Button 2"]);
button1->setFocus();
error = AXUIElementCopyAttributeValue(systemWideElement,
(CFStringRef)NSAccessibilityFocusedUIElementAttribute, (CFTypeRef*)&focussedElement);
QVERIFY(!error);
QVERIFY(focussedElement);
TestAXObject *focusButton1 = [[TestAXObject alloc] initWithAXUIElementRef: focussedElement];
QVERIFY([[focusButton1 role] isEqualToString: NSAccessibilityButtonRole]);
QVERIFY([[focusButton1 title] isEqualToString: @"I am a button"]);
}
void tst_QAccessibilityMac::notificationsTest()
{
auto *w = m_window;
QLineEdit *le1 = new QLineEdit(w);
QLineEdit *le2 = new QLineEdit(w);
w->layout()->addWidget(le1);
w->layout()->addWidget(le2);
QCoreApplication::processEvents();
QTest::qWait(100);
TestAXObject *appObject = [TestAXObject getApplicationAXObject];
QVERIFY(appObject);
// one window
QTRY_VERIFY(appObject.windowList.count == 1);
AXUIElementRef windowRef = (AXUIElementRef) [appObject.windowList objectAtIndex: 0];
QVERIFY(windowRef != nil);
TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];
AXUIElementRef lineEdit1 = [window findDirectChildByRole: kAXTextFieldRole];
QVERIFY(lineEdit1 != nil);
AXObserverRef observer = 0;
AXError err = AXObserverCreate(getpid(), observerCallback, &observer);
QVERIFY(!err);
AXObserverAddNotification(observer, appObject.ref, kAXFocusedUIElementChangedNotification, 0);
AXObserverAddNotification(observer, lineEdit1, kAXValueChangedNotification, 0);
CFRunLoopAddSource( [[NSRunLoop currentRunLoop] getCFRunLoop], AXObserverGetRunLoopSource(observer), kCFRunLoopDefaultMode);
QVERIFY(notificationList.length() == 0);
le2->setFocus();
QTRY_VERIFY(notificationList.length() == 1);
QTRY_VERIFY(notificationList.at(0) == QAccessible::Focus);
le1->setFocus();
QTRY_VERIFY(notificationList.length() == 2);
QTRY_VERIFY(notificationList.at(1) == QAccessible::Focus);
le1->setText("hello");
QTRY_VERIFY(notificationList.length() == 3);
QTRY_VERIFY(notificationList.at(2) == QAccessible::ValueChanged);
le1->setText("foo");
QTRY_VERIFY(notificationList.length() == 4);
QTRY_VERIFY(notificationList.at(3) == QAccessible::ValueChanged);
}
void tst_QAccessibilityMac::checkBoxTest()
{
QCheckBox *ckBox = new QCheckBox(m_window);
ckBox->setText("Great option");
m_window->addWidget(ckBox);
QVERIFY(QTest::qWaitForWindowExposed(m_window));
QCoreApplication::processEvents();
TestAXObject *appObject = [TestAXObject getApplicationAXObject];
QVERIFY(appObject);
// one window
QTRY_VERIFY(appObject.windowList.count == 1);
AXUIElementRef windowRef = (AXUIElementRef) [appObject.windowList objectAtIndex: 0];
QVERIFY(windowRef != nil);
TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];
// children of window:
AXUIElementRef checkBox = [window findDirectChildByRole: kAXCheckBoxRole];
QVERIFY(checkBox != nil);
TestAXObject *cb = [[TestAXObject alloc] initWithAXUIElementRef: checkBox];
// here start actual checkbox tests
QVERIFY([cb valueNumber] == 0);
QVERIFY([cb.title isEqualToString:@"Great option"]);
// EXPECT(cb.description == nil); // currently returns "" instead of nil
QVERIFY([cb.actions containsObject:(NSString*)kAXPressAction]);
[cb performAction:kAXPressAction];
QVERIFY([cb valueNumber] == 1);
[cb performAction:kAXPressAction];
QVERIFY([cb valueNumber] == 0);
ckBox->setCheckState(Qt::PartiallyChecked);
QVERIFY([cb valueNumber] == 2);
}
a11y: Add support for Tables in macOS bridge For a11y purposes, a table needs to be mapped into a logical accessibility hierarchy. There are several ways of doing this mapping, and unfortunately macOS expects something different than what QAccessibleInterface does. So suppose we have a a 2x2 QTableView with both horizontal and vertical header like this (the names reflect the QAccessible::Role names): +-----------+--------------+--------------+ | | ColumnHeader | ColumnHeader | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ In order to be presented to the screen reader on a platform, it goes through two rounds of mapping: QAccessibleInterface will have all headers and cells as direct children of the table: - Table +- ColumnHeader +- ColumnHeader +- RowHeader +- Cell +- Cell +- RowHeader +- Cell +- Cell macOS expects a deeper hierarchy: - AXTable [QAccessible::Table] +- AXRow [Qt:no eqiuivalent] +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXRow +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXColumn (this seems to just store the geometry of the column) +- AXColumn (this seems to just store the geometry of the column) +- AXGroup (this represents the column headers) +- AXSortButton (clicking a header cell will trigger sorting) +- AXSortButton (clicking a header cell will trigger sorting) It's unclear to me how RowHeaders are mapped (they are rarer than ColumnHeaders, I expect to find them in e.g. spreadsheet applications). I haven't found any native usage of them. So this patch simply ignores them. Notice that macOS have a three layer deep hierarchy to represent a table (Table->Row->Cell), while QAccessibleInterface has a two-layer deep hierarchy (Table->Row/Cell). In the macOS bridge we therefore need to "inject" the Row/Column element to be "between" the table and the cell. The table will take ownership of all row and column elements that are children of the table. These elements are not inserted into the cache (it would be pointless, since the cache is basically just a mapping between the QAccessibleInterface -> QMacAccessibilityElement, and the row and column elements does not have a corresponding QAccessibleInterface to be mapped from). The rows and columns are therefore also created as soon as the table element is initialized, and they are stored in two NSMutableArray members of QMacAccessibilityElement. A table is constructed like any other accessibility element, with a valid axid and synthesizedRole set to nil. Each child row and column element is constructed with the same axid as the parent table element, and they will have the synthesizedRole set to either NSAccessibilityRow or NSAccessibilityColumn. With the synthesizedRole member we can then identify if we are a row, column or the actual table, and implement their respective behaviors. Notice that the child row/column is created with the parent's table axid in order for them to have a way of finding their parent table element. (there is no 'parent' member variable in QMacAccessibilityElement) This glorious scheme isn't pretty, but seems to work. Fixes: QTBUG-37207 Change-Id: I7c2451e629f5331b9a0ed61dc22c6e74a82cc173 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io> Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
2022-10-26 15:04:04 +00:00
void tst_QAccessibilityMac::tableViewTest()
{
QTableWidget *tw = new QTableWidget(3, 2, m_window);
struct Person
{
const char *name;
const char *address;
};
const Person contents[] = { { "Socrates", "Greece" },
{ "Confucius", "China" },
{ "Kant", "Preussia" }
};
for (int i = 0; i < int(sizeof(contents) / sizeof(Person)); ++i) {
Person p = contents[i];
QTableWidgetItem *name = new QTableWidgetItem(QString::fromLatin1(p.name));
tw->setItem(i, 0, name);
QTableWidgetItem *address = new QTableWidgetItem(QString::fromLatin1(p.address));
tw->setItem(i, 1, address);
}
m_window->addWidget(tw);
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];
// children of window:
AXUIElementRef tableView = [window findDirectChildByRole:kAXTableRole];
QVERIFY(tableView != nil);
TestAXObject *tv = [[TestAXObject alloc] initWithAXUIElementRef:tableView];
// here start actual tableview tests
// Should have 2 columns
const unsigned int columnCount = 2;
a11y: Add support for Tables in macOS bridge For a11y purposes, a table needs to be mapped into a logical accessibility hierarchy. There are several ways of doing this mapping, and unfortunately macOS expects something different than what QAccessibleInterface does. So suppose we have a a 2x2 QTableView with both horizontal and vertical header like this (the names reflect the QAccessible::Role names): +-----------+--------------+--------------+ | | ColumnHeader | ColumnHeader | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ In order to be presented to the screen reader on a platform, it goes through two rounds of mapping: QAccessibleInterface will have all headers and cells as direct children of the table: - Table +- ColumnHeader +- ColumnHeader +- RowHeader +- Cell +- Cell +- RowHeader +- Cell +- Cell macOS expects a deeper hierarchy: - AXTable [QAccessible::Table] +- AXRow [Qt:no eqiuivalent] +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXRow +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXColumn (this seems to just store the geometry of the column) +- AXColumn (this seems to just store the geometry of the column) +- AXGroup (this represents the column headers) +- AXSortButton (clicking a header cell will trigger sorting) +- AXSortButton (clicking a header cell will trigger sorting) It's unclear to me how RowHeaders are mapped (they are rarer than ColumnHeaders, I expect to find them in e.g. spreadsheet applications). I haven't found any native usage of them. So this patch simply ignores them. Notice that macOS have a three layer deep hierarchy to represent a table (Table->Row->Cell), while QAccessibleInterface has a two-layer deep hierarchy (Table->Row/Cell). In the macOS bridge we therefore need to "inject" the Row/Column element to be "between" the table and the cell. The table will take ownership of all row and column elements that are children of the table. These elements are not inserted into the cache (it would be pointless, since the cache is basically just a mapping between the QAccessibleInterface -> QMacAccessibilityElement, and the row and column elements does not have a corresponding QAccessibleInterface to be mapped from). The rows and columns are therefore also created as soon as the table element is initialized, and they are stored in two NSMutableArray members of QMacAccessibilityElement. A table is constructed like any other accessibility element, with a valid axid and synthesizedRole set to nil. Each child row and column element is constructed with the same axid as the parent table element, and they will have the synthesizedRole set to either NSAccessibilityRow or NSAccessibilityColumn. With the synthesizedRole member we can then identify if we are a row, column or the actual table, and implement their respective behaviors. Notice that the child row/column is created with the parent's table axid in order for them to have a way of finding their parent table element. (there is no 'parent' member variable in QMacAccessibilityElement) This glorious scheme isn't pretty, but seems to work. Fixes: QTBUG-37207 Change-Id: I7c2451e629f5331b9a0ed61dc22c6e74a82cc173 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io> Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
2022-10-26 15:04:04 +00:00
NSArray *columnArray = [tv tableColumns];
QCOMPARE([columnArray count], columnCount);
a11y: Add support for Tables in macOS bridge For a11y purposes, a table needs to be mapped into a logical accessibility hierarchy. There are several ways of doing this mapping, and unfortunately macOS expects something different than what QAccessibleInterface does. So suppose we have a a 2x2 QTableView with both horizontal and vertical header like this (the names reflect the QAccessible::Role names): +-----------+--------------+--------------+ | | ColumnHeader | ColumnHeader | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ In order to be presented to the screen reader on a platform, it goes through two rounds of mapping: QAccessibleInterface will have all headers and cells as direct children of the table: - Table +- ColumnHeader +- ColumnHeader +- RowHeader +- Cell +- Cell +- RowHeader +- Cell +- Cell macOS expects a deeper hierarchy: - AXTable [QAccessible::Table] +- AXRow [Qt:no eqiuivalent] +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXRow +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXColumn (this seems to just store the geometry of the column) +- AXColumn (this seems to just store the geometry of the column) +- AXGroup (this represents the column headers) +- AXSortButton (clicking a header cell will trigger sorting) +- AXSortButton (clicking a header cell will trigger sorting) It's unclear to me how RowHeaders are mapped (they are rarer than ColumnHeaders, I expect to find them in e.g. spreadsheet applications). I haven't found any native usage of them. So this patch simply ignores them. Notice that macOS have a three layer deep hierarchy to represent a table (Table->Row->Cell), while QAccessibleInterface has a two-layer deep hierarchy (Table->Row/Cell). In the macOS bridge we therefore need to "inject" the Row/Column element to be "between" the table and the cell. The table will take ownership of all row and column elements that are children of the table. These elements are not inserted into the cache (it would be pointless, since the cache is basically just a mapping between the QAccessibleInterface -> QMacAccessibilityElement, and the row and column elements does not have a corresponding QAccessibleInterface to be mapped from). The rows and columns are therefore also created as soon as the table element is initialized, and they are stored in two NSMutableArray members of QMacAccessibilityElement. A table is constructed like any other accessibility element, with a valid axid and synthesizedRole set to nil. Each child row and column element is constructed with the same axid as the parent table element, and they will have the synthesizedRole set to either NSAccessibilityRow or NSAccessibilityColumn. With the synthesizedRole member we can then identify if we are a row, column or the actual table, and implement their respective behaviors. Notice that the child row/column is created with the parent's table axid in order for them to have a way of finding their parent table element. (there is no 'parent' member variable in QMacAccessibilityElement) This glorious scheme isn't pretty, but seems to work. Fixes: QTBUG-37207 Change-Id: I7c2451e629f5331b9a0ed61dc22c6e74a82cc173 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io> Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
2022-10-26 15:04:04 +00:00
// should have 3 rows
const unsigned int rowCount = 3;
a11y: Add support for Tables in macOS bridge For a11y purposes, a table needs to be mapped into a logical accessibility hierarchy. There are several ways of doing this mapping, and unfortunately macOS expects something different than what QAccessibleInterface does. So suppose we have a a 2x2 QTableView with both horizontal and vertical header like this (the names reflect the QAccessible::Role names): +-----------+--------------+--------------+ | | ColumnHeader | ColumnHeader | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ In order to be presented to the screen reader on a platform, it goes through two rounds of mapping: QAccessibleInterface will have all headers and cells as direct children of the table: - Table +- ColumnHeader +- ColumnHeader +- RowHeader +- Cell +- Cell +- RowHeader +- Cell +- Cell macOS expects a deeper hierarchy: - AXTable [QAccessible::Table] +- AXRow [Qt:no eqiuivalent] +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXRow +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXColumn (this seems to just store the geometry of the column) +- AXColumn (this seems to just store the geometry of the column) +- AXGroup (this represents the column headers) +- AXSortButton (clicking a header cell will trigger sorting) +- AXSortButton (clicking a header cell will trigger sorting) It's unclear to me how RowHeaders are mapped (they are rarer than ColumnHeaders, I expect to find them in e.g. spreadsheet applications). I haven't found any native usage of them. So this patch simply ignores them. Notice that macOS have a three layer deep hierarchy to represent a table (Table->Row->Cell), while QAccessibleInterface has a two-layer deep hierarchy (Table->Row/Cell). In the macOS bridge we therefore need to "inject" the Row/Column element to be "between" the table and the cell. The table will take ownership of all row and column elements that are children of the table. These elements are not inserted into the cache (it would be pointless, since the cache is basically just a mapping between the QAccessibleInterface -> QMacAccessibilityElement, and the row and column elements does not have a corresponding QAccessibleInterface to be mapped from). The rows and columns are therefore also created as soon as the table element is initialized, and they are stored in two NSMutableArray members of QMacAccessibilityElement. A table is constructed like any other accessibility element, with a valid axid and synthesizedRole set to nil. Each child row and column element is constructed with the same axid as the parent table element, and they will have the synthesizedRole set to either NSAccessibilityRow or NSAccessibilityColumn. With the synthesizedRole member we can then identify if we are a row, column or the actual table, and implement their respective behaviors. Notice that the child row/column is created with the parent's table axid in order for them to have a way of finding their parent table element. (there is no 'parent' member variable in QMacAccessibilityElement) This glorious scheme isn't pretty, but seems to work. Fixes: QTBUG-37207 Change-Id: I7c2451e629f5331b9a0ed61dc22c6e74a82cc173 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io> Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
2022-10-26 15:04:04 +00:00
NSArray *rowArray = [tv tableRows];
QCOMPARE([rowArray count], rowCount);
a11y: Add support for Tables in macOS bridge For a11y purposes, a table needs to be mapped into a logical accessibility hierarchy. There are several ways of doing this mapping, and unfortunately macOS expects something different than what QAccessibleInterface does. So suppose we have a a 2x2 QTableView with both horizontal and vertical header like this (the names reflect the QAccessible::Role names): +-----------+--------------+--------------+ | | ColumnHeader | ColumnHeader | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ In order to be presented to the screen reader on a platform, it goes through two rounds of mapping: QAccessibleInterface will have all headers and cells as direct children of the table: - Table +- ColumnHeader +- ColumnHeader +- RowHeader +- Cell +- Cell +- RowHeader +- Cell +- Cell macOS expects a deeper hierarchy: - AXTable [QAccessible::Table] +- AXRow [Qt:no eqiuivalent] +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXRow +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXColumn (this seems to just store the geometry of the column) +- AXColumn (this seems to just store the geometry of the column) +- AXGroup (this represents the column headers) +- AXSortButton (clicking a header cell will trigger sorting) +- AXSortButton (clicking a header cell will trigger sorting) It's unclear to me how RowHeaders are mapped (they are rarer than ColumnHeaders, I expect to find them in e.g. spreadsheet applications). I haven't found any native usage of them. So this patch simply ignores them. Notice that macOS have a three layer deep hierarchy to represent a table (Table->Row->Cell), while QAccessibleInterface has a two-layer deep hierarchy (Table->Row/Cell). In the macOS bridge we therefore need to "inject" the Row/Column element to be "between" the table and the cell. The table will take ownership of all row and column elements that are children of the table. These elements are not inserted into the cache (it would be pointless, since the cache is basically just a mapping between the QAccessibleInterface -> QMacAccessibilityElement, and the row and column elements does not have a corresponding QAccessibleInterface to be mapped from). The rows and columns are therefore also created as soon as the table element is initialized, and they are stored in two NSMutableArray members of QMacAccessibilityElement. A table is constructed like any other accessibility element, with a valid axid and synthesizedRole set to nil. Each child row and column element is constructed with the same axid as the parent table element, and they will have the synthesizedRole set to either NSAccessibilityRow or NSAccessibilityColumn. With the synthesizedRole member we can then identify if we are a row, column or the actual table, and implement their respective behaviors. Notice that the child row/column is created with the parent's table axid in order for them to have a way of finding their parent table element. (there is no 'parent' member variable in QMacAccessibilityElement) This glorious scheme isn't pretty, but seems to work. Fixes: QTBUG-37207 Change-Id: I7c2451e629f5331b9a0ed61dc22c6e74a82cc173 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io> Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
2022-10-26 15:04:04 +00:00
// The individual cells are children of the rows
TestAXObject *row = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)rowArray[0]];
TestAXObject *cell = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)[row childList][0]];
QVERIFY([cell.title isEqualToString:@"Socrates"]);
row = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)rowArray[2]];
cell = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)[row childList][1]];
QVERIFY([cell.title isEqualToString:@"Preussia"]);
// both rows and columns are direct children of the table
NSArray *childList = [tv childList];
QCOMPARE([childList count], columnCount + rowCount);
a11y: Add support for Tables in macOS bridge For a11y purposes, a table needs to be mapped into a logical accessibility hierarchy. There are several ways of doing this mapping, and unfortunately macOS expects something different than what QAccessibleInterface does. So suppose we have a a 2x2 QTableView with both horizontal and vertical header like this (the names reflect the QAccessible::Role names): +-----------+--------------+--------------+ | | ColumnHeader | ColumnHeader | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ | RowHeader | Cell | Cell | +-----------+--------------+--------------+ In order to be presented to the screen reader on a platform, it goes through two rounds of mapping: QAccessibleInterface will have all headers and cells as direct children of the table: - Table +- ColumnHeader +- ColumnHeader +- RowHeader +- Cell +- Cell +- RowHeader +- Cell +- Cell macOS expects a deeper hierarchy: - AXTable [QAccessible::Table] +- AXRow [Qt:no eqiuivalent] +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXRow +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- [QAccessible::Cell] (The content of the cell, e.g. AXButton, AXGroup or whatever) +- AXColumn (this seems to just store the geometry of the column) +- AXColumn (this seems to just store the geometry of the column) +- AXGroup (this represents the column headers) +- AXSortButton (clicking a header cell will trigger sorting) +- AXSortButton (clicking a header cell will trigger sorting) It's unclear to me how RowHeaders are mapped (they are rarer than ColumnHeaders, I expect to find them in e.g. spreadsheet applications). I haven't found any native usage of them. So this patch simply ignores them. Notice that macOS have a three layer deep hierarchy to represent a table (Table->Row->Cell), while QAccessibleInterface has a two-layer deep hierarchy (Table->Row/Cell). In the macOS bridge we therefore need to "inject" the Row/Column element to be "between" the table and the cell. The table will take ownership of all row and column elements that are children of the table. These elements are not inserted into the cache (it would be pointless, since the cache is basically just a mapping between the QAccessibleInterface -> QMacAccessibilityElement, and the row and column elements does not have a corresponding QAccessibleInterface to be mapped from). The rows and columns are therefore also created as soon as the table element is initialized, and they are stored in two NSMutableArray members of QMacAccessibilityElement. A table is constructed like any other accessibility element, with a valid axid and synthesizedRole set to nil. Each child row and column element is constructed with the same axid as the parent table element, and they will have the synthesizedRole set to either NSAccessibilityRow or NSAccessibilityColumn. With the synthesizedRole member we can then identify if we are a row, column or the actual table, and implement their respective behaviors. Notice that the child row/column is created with the parent's table axid in order for them to have a way of finding their parent table element. (there is no 'parent' member variable in QMacAccessibilityElement) This glorious scheme isn't pretty, but seems to work. Fixes: QTBUG-37207 Change-Id: I7c2451e629f5331b9a0ed61dc22c6e74a82cc173 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io> Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
2022-10-26 15:04:04 +00:00
for (id child in childList) {
TestAXObject *childObject = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)child];
QVERIFY([childObject.role isEqualToString:NSAccessibilityRowRole] ||
[childObject.role isEqualToString:NSAccessibilityColumnRole]);
}
}
QTEST_MAIN(tst_QAccessibilityMac)
#include "tst_qaccessibilitymac.moc"