diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm index ccd5f7b665..535d0a3d8a 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm @@ -54,6 +54,10 @@ void QCocoaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) case QAccessible::NameChanged: NSAccessibilityPostNotification(element, NSAccessibilityTitleChangedNotification); break; + case QAccessible::TableModelChanged: + // ### Could NSAccessibilityRowCountChangedNotification be relevant here? + [element updateTableModel]; + break; default: break; } diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h index 806359888f..1f121e2fd8 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h +++ b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h @@ -13,7 +13,9 @@ QT_DECLARE_NAMESPACED_OBJC_INTERFACE(QMacAccessibilityElement, NSObject - (instancetype)initWithId:(QAccessible::Id)anId; +- (instancetype)initWithId:(QAccessible::Id)anId role:(NSAccessibilityRole)role; + (instancetype)elementWithId:(QAccessible::Id)anId; +- (void)updateTableModel; ) #endif // QT_CONFIG(accessibility) diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm index f3c3d0bfaa..68e7947162 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm @@ -80,14 +80,37 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of @implementation QMacAccessibilityElement { QAccessible::Id axid; + + // used by NSAccessibilityTable + NSMutableArray *rows; // corresponds to accessibilityRows + NSMutableArray *columns; // corresponds to accessibilityColumns + + // If synthesizedRole is set, this means that this objects does not have a corresponding + // QAccessibleInterface, but it is synthesized by the cocoa plugin in order to meet the + // NSAccessibility requirements. + // The ownership is controlled by the parent object identified with the axid member variable. + // (Therefore, if this member is set, this objects axid member is the same as the parents axid + // member) + NSString *synthesizedRole; } - (instancetype)initWithId:(QAccessible::Id)anId +{ + return [self initWithId:anId role:nil]; +} + +- (instancetype)initWithId:(QAccessible::Id)anId role:(NSAccessibilityRole)role { Q_ASSERT((int)anId < 0); self = [super init]; if (self) { axid = anId; + rows = nil; + columns = nil; + synthesizedRole = role; + QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); + if (iface && iface->tableInterface() && !synthesizedRole) + [self updateTableModel]; } return self; @@ -115,18 +138,26 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of - (void)invalidate { axid = 0; + rows = nil; + columns = nil; + synthesizedRole = nil; + NSAccessibilityPostNotification(self, NSAccessibilityUIElementDestroyedNotification); [self release]; } - (void)dealloc { + if (rows) + [rows release]; // will also release all entries first + if (columns) + [columns release]; // will also release all entries first [super dealloc]; } - (BOOL)isEqual:(id)object { if ([object isKindOfClass:[QMacAccessibilityElement class]]) { QMacAccessibilityElement *other = object; - return other->axid == axid; + return other->axid == axid && other->synthesizedRole == synthesizedRole; } else { return NO; } @@ -136,6 +167,50 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of return axid; } +- (BOOL)isManagedByParent { + return synthesizedRole != nil; +} + +- (NSMutableArray *)populateTableArray:(NSMutableArray *)array role:(NSAccessibilityRole)role count:(int)count +{ + QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); + if (iface && iface->isValid()) { + if (!array) { + array = [NSMutableArray arrayWithCapacity:count]; + [array retain]; + } else { + [array removeAllObjects]; + } + Q_ASSERT(array); + for (int n = 0; n < count; ++n) { + // columns will have same axid as table (but not inserted in cache) + QMacAccessibilityElement *element = + [[QMacAccessibilityElement alloc] initWithId:axid role:role]; + if (element) { + [array addObject:element]; + [element release]; + } else { + qWarning("QCocoaAccessibility: invalid child"); + } + } + return array; + } + return nil; +} + + +- (void)updateTableModel +{ + QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); + if (iface && iface->isValid()) { + if (QAccessibleTableInterface *table = iface->tableInterface()) { + Q_ASSERT(!self.isManagedByParent); + rows = [self populateTableArray:rows role:NSAccessibilityRowRole count:table->rowCount()]; + columns = [self populateTableArray:columns role:NSAccessibilityColumnRole count:table->columnCount()]; + } + } +} + // // accessibility protocol // @@ -168,6 +243,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); if (!iface || !iface->isValid()) return NSAccessibilityUnknownRole; + if (synthesizedRole) + return synthesizedRole; return QCocoaAccessible::macRole(iface); } @@ -189,6 +266,81 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); if (!iface || !iface->isValid()) return nil; + if (QAccessibleTableInterface *table = iface->tableInterface()) { + // either a table or table rows/columns + if (!synthesizedRole) { + // This is the table element, parent of all rows and columns + /* + * Typical 2x2 table hierarchy as can be observed in a table found under + * Apple -> System Settings -> General -> Login Items (macOS 13) + * + * (AXTable) + * | Columns: NSArray* (2 items) + * | Rows: NSArray* (2 items) + * | Visible Columns: NSArray* (2 items) + * | Visible Rows: NSArray* (2 items) + * | Children: NSArray* (5 items) + +----<--| Header: (AXGroup) + | * +-- (AXRow) + | * | +-- (AXText) + | * | +-- (AXTextField) + | * +-- (AXRow) + | * | +-- (AXText) + | * | +-- (AXTextField) + | * +-- (AXColumn) + | * | Header: "Item" (sort button) + | * | Index: 0 + | * | Rows: NSArray* (2 items) + | * | Visible Rows: NSArray* (2 items) + | * +-- (AXColumn) + | * | Header: "Kind" (sort button) + | * | Index: 1 + | * | Rows: NSArray* (2 items) + | * | Visible Rows: NSArray* (2 items) + +----> +-- (AXGroup) + * +-- (AXButton/AXSortButton) Item [NSAccessibilityTableHeaderCellProxy] + * +-- (AXButton/AXSortButton) Kind [NSAccessibilityTableHeaderCellProxy] + */ + NSArray *rs = [self accessibilityRows]; + NSArray *cs = [self accessibilityColumns]; + const int rCount = int([rs count]); + const int cCount = int([cs count]); + int childCount = rCount + cCount; + NSMutableArray *tableChildren = + [NSMutableArray arrayWithCapacity:childCount]; + for (int i = 0; i < rCount; ++i) { + [tableChildren addObject:[rs objectAtIndex:i]]; + } + for (int i = 0; i < cCount; ++i) { + [tableChildren addObject:[cs objectAtIndex:i]]; + } + return NSAccessibilityUnignoredChildren(tableChildren); + } else if (synthesizedRole == NSAccessibilityColumnRole) { + return nil; + } else if (synthesizedRole == NSAccessibilityRowRole) { + // axid matches the parent table axid so that we can easily find the parent table + // children of row are cell/any items + QMacAccessibilityElement *tableElement = [QMacAccessibilityElement elementWithId:axid]; + Q_ASSERT(tableElement->rows); + NSUInteger rowIndex = [tableElement->rows indexOfObjectIdenticalTo:self]; + Q_ASSERT(rowIndex != NSNotFound); + int numColumns = table->columnCount(); + NSMutableArray *cells = + [NSMutableArray arrayWithCapacity:numColumns]; + for (int i = 0; i < numColumns; ++i) { + QAccessibleInterface *cell = table->cellAt(rowIndex, i); + if (cell && cell->isValid()) { + QAccessible::Id cellId = QAccessible::uniqueId(cell); + QMacAccessibilityElement *element = + [QMacAccessibilityElement elementWithId:cellId]; + if (element) { + [cells addObject:element]; + } + } + } + return NSAccessibilityUnignoredChildren(cells); + } + } return QCocoaAccessible::unignoredChildren(iface); } @@ -208,6 +360,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of return nil; if (iface->role() == QAccessible::StaticText) return nil; + if (self.isManagedByParent) + return nil; return iface->text(QAccessible::Name).toNSString(); } @@ -223,12 +377,31 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of if (!iface || !iface->isValid()) return nil; + if (self.isManagedByParent) { + // axid is the same for the parent element + return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithId:axid]); + } + // macOS expects that the hierarchy is: // App -> Window -> Children // We don't actually have the window reflected properly in QAccessibility. // Check if the parent is the application and then instead return the native window. if (QAccessibleInterface *parent = iface->parent()) { + if (parent->tableInterface()) { + if (QAccessibleTableCellInterface *cell = iface->tableCellInterface()) { + // parent of cell should be row + QAccessible::Id parentId = QAccessible::uniqueId(parent); + QMacAccessibilityElement *tableElement = + [QMacAccessibilityElement elementWithId:parentId]; + + const int rowIndex = cell->rowIndex(); + if (tableElement->rows && int([tableElement->rows count]) > rowIndex) { + QMacAccessibilityElement *rowElement = tableElement->rows[rowIndex]; + return NSAccessibilityUnignoredAncestor(rowElement); + } + } + } if (parent->role() != QAccessible::Application) { QAccessible::Id parentId = QAccessible::uniqueId(parent); return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithId: parentId]); @@ -249,7 +422,44 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); if (!iface || !iface->isValid()) return NSZeroRect; - return QCocoaScreen::mapToNative(iface->rect()); + + QRect rect; + if (self.isManagedByParent) { + if (QAccessibleTableInterface *table = iface->tableInterface()) { + // Construct the geometry of the Row/Column by looking at the individual table cells + // ### Assumes that cells logical coordinates have spatial ordering (e.g finds the + // rows width by taking the union between the leftmost item and the rightmost item in + // a row). + // Otherwise, we have to iterate over *all* cells in a row/columns to + // find out the Row/Column geometry + const bool isRow = synthesizedRole == NSAccessibilityRowRole; + QPoint cellPos; + int &row = isRow ? cellPos.ry() : cellPos.rx(); + int &col = isRow ? cellPos.rx() : cellPos.ry(); + + QMacAccessibilityElement *tableElement = + [QMacAccessibilityElement elementWithId:axid]; + NSArray *tracks = isRow ? tableElement->rows : tableElement->columns; + NSUInteger trackIndex = [tracks indexOfObjectIdenticalTo:self]; + if (trackIndex != NSNotFound) { + row = int(trackIndex); + if (QAccessibleInterface *firstCell = table->cellAt(cellPos.y(), cellPos.x())) { + rect = firstCell->rect(); + col = isRow ? table->columnCount() : table->rowCount(); + if (col > 1) { + --col; + if (QAccessibleInterface *lastCell = + table->cellAt(cellPos.y(), cellPos.x())) + rect = rect.united(lastCell->rect()); + } + } + } + } + } else { + rect = iface->rect(); + } + + return QCocoaScreen::mapToNative(rect); } - (NSString*)accessibilityLabel { @@ -614,6 +824,49 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of return nil; } +/* + * Support for table + */ +- (NSInteger) accessibilityIndex { + NSInteger index = 0; + QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); + if (iface && iface->isValid()) { + if (self.isManagedByParent) { + // axid matches the parent table axid so that we can easily find the parent table + // children of row are cell/any items + if (QAccessibleTableInterface *table = iface->tableInterface()) { + QMacAccessibilityElement *tableElement = [QMacAccessibilityElement elementWithId:axid]; + NSArray *track = synthesizedRole == NSAccessibilityRowRole + ? tableElement->rows : tableElement->columns; + if (track) { + NSUInteger trackIndex = [track indexOfObjectIdenticalTo:self]; + index = (NSInteger)trackIndex; + } + } + } + } + return index; +} + +- (NSArray *) accessibilityRows { + QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); + if (iface && iface->isValid() && iface->tableInterface() && !synthesizedRole) { + if (rows) + return NSAccessibilityUnignoredChildren(rows); + } + return nil; +} + +- (NSArray *) accessibilityColumns { + QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); + if (iface && iface->isValid() && iface->tableInterface() && !synthesizedRole) { + if (columns) + return NSAccessibilityUnignoredChildren(columns); + } + 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 39ffe504c7..3b1979c030 100644 --- a/tests/auto/other/qaccessibilitymac/tst_qaccessibilitymac.mm +++ b/tests/auto/other/qaccessibilitymac/tst_qaccessibilitymac.mm @@ -109,6 +109,28 @@ QDebug operator<<(QDebug dbg, AXErrorTag err) return list; } +- (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; @@ -384,6 +406,7 @@ private Q_SLOTS: void hierarchyTest(); void notificationsTest(); void checkBoxTest(); + void tableViewTest(); private: AccessibleTestWindow *m_window; @@ -641,5 +664,71 @@ void tst_QAccessibilityMac::checkBoxTest() QVERIFY([cb valueNumber] == 2); } +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 + NSArray *columnArray = [tv tableColumns]; + QCOMPARE([columnArray count], 2); + + // should have 3 rows + NSArray *rowArray = [tv tableRows]; + QCOMPARE([rowArray count], 3); + + // 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], 5); // 3 rows + 2 columns + 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"