Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qcocoaaccessibilityelement.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include <AppKit/AppKit.h>
6
10#include "qcocoawindow.h"
11#include "qcocoascreen.h"
12
13#include <QtCore/qlogging.h>
14#include <QtGui/private/qaccessiblecache_p.h>
15#include <QtGui/private/qaccessiblebridgeutils_p.h>
16#include <QtGui/qaccessible.h>
17
18QT_USE_NAMESPACE
19
20Q_STATIC_LOGGING_CATEGORY(lcAccessibilityTable, "qt.accessibility.table")
21
22using namespace Qt::StringLiterals;
23
24#if QT_CONFIG(accessibility)
25
26/**
27 * Converts between absolute character offsets and line numbers of a
28 * QAccessibleTextInterface. Works in exactly one of two modes:
29 *
30 * - Pass *line == -1 in order to get a line containing character at the given
31 * *offset
32 * - Pass *offset == -1 in order to get the offset of first character of the
33 * given *line
34 *
35 * You can optionally also pass non-NULL `start` and `end`, which will in both
36 * modes be filled with the offset of the first and last characters of the
37 * relevant line.
38 */
39static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *offset, NSUInteger *start = 0, NSUInteger *end = 0)
40{
41 Q_ASSERT(*line == -1 || *offset == -1);
42 Q_ASSERT(*line != -1 || *offset != -1);
43 Q_ASSERT(*offset <= text->characterCount());
44
45 int curLine = -1;
46 int curStart = 0, curEnd = 0;
47
48 do {
49 curStart = curEnd;
50 text->textAtOffset(curStart, QAccessible::LineBoundary, &curStart, &curEnd);
51 // If the text is empty then we just return
52 if (curStart == -1 || curEnd == -1) {
53 if (start)
54 *start = 0;
55 if (end)
56 *end = 0;
57 return;
58 }
59 ++curLine;
60 {
61 // check for a case where a single word longer than the text edit's width and gets wrapped
62 // in the middle of the word; in this case curEnd will be an offset belonging to the next line
63 // and therefore nextEnd will not be equal to curEnd
64 int nextStart;
65 int nextEnd;
66 text->textAtOffset(curEnd, QAccessible::LineBoundary, &nextStart, &nextEnd);
67 if (nextEnd == curEnd)
68 ++curEnd;
69 }
70 } while ((*line == -1 || curLine < *line) && (*offset == -1 || (curEnd <= *offset)) && curEnd <= text->characterCount());
71
72 curEnd = qMin(curEnd, text->characterCount());
73
74 if (*line == -1)
75 *line = curLine;
76 if (*offset == -1)
77 *offset = curStart;
78
79 Q_ASSERT(curStart >= 0);
80 Q_ASSERT(curEnd >= 0);
81 if (start)
82 *start = curStart;
83 if (end)
84 *end = curEnd;
85}
86
87@implementation QMacAccessibilityElement {
88 QAccessible::Id axid;
89 int m_rowIndex;
90 int m_columnIndex;
91
92 // used by NSAccessibilityTable
93 NSMutableArray<QMacAccessibilityElement *> *rows; // corresponds to accessibilityRows
94 NSMutableArray<QMacAccessibilityElement *> *columns; // corresponds to accessibilityColumns
95
96 // If synthesizedRole is set, this means that this objects does not have a corresponding
97 // QAccessibleInterface, but it is synthesized by the cocoa plugin in order to meet the
98 // NSAccessibility requirements.
99 // The ownership is controlled by the parent object identified with the axid member variable.
100 // (Therefore, if this member is set, this objects axid member is the same as the parents axid
101 // member)
102 NSString *synthesizedRole;
103}
104
105- (instancetype)initWithId:(QAccessible::Id)anId
106{
107 return [self initWithId:anId role:nil];
108}
109
110- (instancetype)initWithId:(QAccessible::Id)anId role:(NSAccessibilityRole)role
111{
112 Q_ASSERT((int)anId < 0);
113 self = [super init];
114 if (self) {
115 axid = anId;
116 m_rowIndex = -1;
117 m_columnIndex = -1;
118 rows = nil;
119 columns = nil;
120 synthesizedRole = role;
121 // table: if this is not created as an element managed by the table, then
122 // it's either the table itself, or an element created for an already existing
123 // cell interface (or an element that's not at all related to a table).
124 if (!synthesizedRole) {
125 if (QAccessibleInterface *iface = QAccessible::accessibleInterface(axid)) {
126 if (iface->tableInterface()) {
127 [self updateTableModel];
128 } else if (const auto *cell = iface->tableCellInterface()) {
129 // If we create an element for a table cell, initialize it with row/column
130 // and insert it into the corresponding row's columns array.
131 m_rowIndex = cell->rowIndex();
132 m_columnIndex = cell->columnIndex();
133 QAccessibleInterface *table = cell->table();
134 Q_ASSERT(table);
135 QAccessibleTableInterface *tableInterface = table->tableInterface();
136 if (tableInterface) {
137 auto *tableElement = [QMacAccessibilityElement elementWithInterface:table];
138 Q_ASSERT(tableElement);
139 if (!tableElement->rows
140 || int(tableElement->rows.count) <= m_rowIndex
141 || int(tableElement->rows.count) != tableInterface->rowCount()) {
142 qCWarning(lcAccessibilityTable)
143 << "Cell requested for row" << m_rowIndex << "is out of"
144 << "bounds for table with" << (tableElement->rows ?
145 tableElement->rows.count : tableInterface->rowCount())
146 << "rows! Resizing table model.";
147 [tableElement updateTableModel];
148 }
149
150 Q_ASSERT(tableElement->rows);
151 Q_ASSERT(int(tableElement->rows.count) > m_rowIndex);
152
153 auto *rowElement = tableElement->rows[m_rowIndex];
154 if (!rowElement->columns || int(rowElement->columns.count) != tableInterface->columnCount()) {
155 if (rowElement->columns) {
156 qCWarning(lcAccessibilityTable)
157 << "Table representation column count is out of sync:"
158 << rowElement->columns.count << "!=" << tableInterface->columnCount();
159 [rowElement->columns autorelease];
160 rowElement->columns = nil;
161 }
162 rowElement->columns = [rowElement populateTableRow:tableInterface->columnCount()];
163 [rowElement->columns retain];
164 }
165
166 qCDebug(lcAccessibilityTable) << "Creating cell representation for"
167 << m_rowIndex << m_columnIndex
168 << "in table with"
169 << tableElement->rows.count << "rows and"
170 << rowElement->columns.count << "columns";
171
172 rowElement->columns[m_columnIndex] = self;
173 }
174 }
175 }
176 }
177 }
178
179 return self;
180}
181
182/*!
183 \internal
184
185 Constructs a new element with the ID \a anId and inserts it into the cache.
186
187 Elements representing table rows, columns, and cells are created directly
188 via initWithId (in populateTableArray and populateTableRow), as they don't
189 get added to the cache until later.
190*/
191+ (instancetype)elementWithId:(QAccessible::Id)anId
192{
193 Q_ASSERT(anId);
194 if (!anId)
195 return nil;
196
197 QAccessibleCache *cache = QAccessibleCache::instance();
198
199 QMacAccessibilityElement *element = cache->elementForId(anId);
200 if (!element) {
201 Q_ASSERT(QAccessible::accessibleInterface(anId));
202 element = [[self alloc] initWithId:anId];
203 if (cache->insertElement(anId, element))
204 [element release];
205 }
206 return element;
207}
208
209+ (instancetype)elementWithInterface:(QAccessibleInterface *)iface
210{
211 Q_ASSERT(iface);
212 if (!iface)
213 return nil;
214
215 const QAccessible::Id anId = QAccessible::uniqueId(iface);
216 return [self elementWithId:anId];
217}
218
219+ (void)removeElementsFromCache:(NSArray *)array {
220 for (uint i = 0; i < array.count; ++i) {
221 QMacAccessibilityElement *cell = [array objectAtIndex:i];
222 if (cell->axid) { // it's a proper cell, remove from cache
223 QAccessibleCache::instance()->deleteInterface(cell->axid);
224 }
225 }
226}
227
228// called by QAccessibleCache::removeAccessibleElement, which also releases
229- (void)invalidate {
230 axid = 0;
231 if (rows) {
232 [QMacAccessibilityElement removeElementsFromCache:rows];
233 [rows autorelease];
234 rows = nil;
235 }
236 if (columns) {
237 [QMacAccessibilityElement removeElementsFromCache:columns];
238 [columns autorelease];
239 columns = nil;
240 }
241 synthesizedRole = nil;
242
243 NSAccessibilityPostNotification(self, NSAccessibilityUIElementDestroyedNotification);
244}
245
246/*!
247 \internal
248
249 If this element represents a table, then the rows and columns array are both
250 populated with elements representing the rows and columns. If this elements
251 represents a row, then the columns array is populated with elements
252 representing the cells. Not all of those synthesized elements might be in
253 the cache, but those that are need to be removed so that we don't end up
254 with stale representations of children when the higher-level element
255 expires.
256*/
257- (void)dealloc {
258 if (rows) {
259 [QMacAccessibilityElement removeElementsFromCache:rows];
260 [rows release]; // will also release all entries first
261 }
262 if (columns) {
263 [QMacAccessibilityElement removeElementsFromCache:columns];
264 [columns release]; // will also release all entries first
265 }
266 QAccessibleCache::instance()->deleteInterface(axid);
267 [super dealloc];
268}
269
270- (BOOL)isEqual:(id)object {
271 if ([object isKindOfClass:[QMacAccessibilityElement class]]) {
272 QMacAccessibilityElement *other = object;
273 return other->axid == axid && other->synthesizedRole == synthesizedRole;
274 } else {
275 return NO;
276 }
277}
278
279- (NSUInteger)hash {
280 return axid;
281}
282
283- (BOOL)isManagedByParent {
284 return synthesizedRole != nil;
285}
286
287- (NSMutableArray *)populateTableArray:(NSAccessibilityRole)role count:(int)count
288{
289 if (self.qtInterface) {
290 auto *array = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:count];
291 Q_ASSERT(array);
292 for (int n = 0; n < count; ++n) {
293 // columns will have same axid as table (but not inserted in cache)
294 QMacAccessibilityElement *element =
295 [[QMacAccessibilityElement alloc] initWithId:axid role:role];
296 if (element) {
297 if (role == NSAccessibilityRowRole)
298 element->m_rowIndex = n;
299 else if (role == NSAccessibilityColumnRole)
300 element->m_columnIndex = n;
301 [array addObject:element];
302 [element release];
303 } else {
304 qWarning("QCocoaAccessibility: invalid child");
305 }
306 }
307 return array;
308 }
309 return nil;
310}
311
312- (NSMutableArray *)populateTableRow:(int)count
313{
314 Q_ASSERT(synthesizedRole == NSAccessibilityRowRole);
315 qCDebug(lcAccessibilityTable) << "Populating table row" << m_rowIndex
316 << "with" << count << "placeholder cells";
317 // When macOS asks for the children of a row, then we populate the row's column
318 // array with synthetic elements as place holders. This way, we don't have to
319 // create QAccessibleInterfaces for every cell before they are really needed.
320 // We don't add those synthetic elements into the cache, and we give them the
321 // same axid as the table. This way, we can get easily to the table, and from
322 // there to the QAccessibleInterface for the cell, when we have to eventually
323 // associate such an interface with the element (at which point it is no longer
324 // a placeholder).
325 auto *array = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:count];
326 Q_ASSERT(array);
327
328 for (int n = 0; n < count; ++n) {
329 // columns will have same axid as table (but not inserted in cache)
330 QMacAccessibilityElement *cell =
331 [[QMacAccessibilityElement alloc] initWithId:axid role:NSAccessibilityCellRole];
332 if (cell) {
333 cell->m_rowIndex = m_rowIndex;
334 cell->m_columnIndex = n;
335 [array addObject:cell];
336 [cell release];
337 }
338 }
339 return array;
340}
341
342- (void)updateTableModel
343{
344 if (QAccessibleInterface *iface = self.qtInterface) {
345 if (QAccessibleTableInterface *table = iface->tableInterface()) {
346 Q_ASSERT(!self.isManagedByParent);
347 qCDebug(lcAccessibilityTable) << "Updating table representation with"
348 << table->rowCount() << table->columnCount();
349 if (rows) {
350 [rows autorelease];
351 rows = nil;
352 }
353 rows = [self populateTableArray:NSAccessibilityRowRole count:table->rowCount()];
354 [rows retain];
355 if (columns) {
356 [columns autorelease];
357 columns = nil;
358 }
359 columns = [self populateTableArray:NSAccessibilityColumnRole count:table->columnCount()];
360 [columns retain];
361 }
362 }
363}
364
365- (QAccessibleInterface *)qtInterface
366{
367 QAccessibleInterface *iface = QAccessible::accessibleInterface(axid);
368 if (!iface || !iface->isValid())
369 return nullptr;
370
371 // If this is a placeholder element for a table cell, associate it with the
372 // cell interface (which will be created now, if needed). The current axid is
373 // for the table to which the cell belongs, so iface is pointing at the table.
374 if (synthesizedRole == NSAccessibilityCellRole) {
375 // get the cell interface - there must be a valid one
376 QAccessibleTableInterface *table = iface->tableInterface();
377 Q_ASSERT(table);
378 QAccessibleInterface *cell = table->cellAt(m_rowIndex, m_columnIndex);
379 if (!cell)
380 return nullptr;
381 Q_ASSERT(cell->isValid());
382 iface = cell;
383
384 // no longer a placeholder
385 axid = QAccessible::uniqueId(cell);
386 synthesizedRole = nil;
387
388 QAccessibleCache *cache = QAccessibleCache::instance();
389 if (QMacAccessibilityElement *cellElement = cache->elementForId(axid)) {
390 // there already is another, non-placeholder element in the cache
391 Q_ASSERT(cellElement->synthesizedRole == nil);
392 // we have to release it if it's not us
393 if (cellElement != self) {
394 // for the same cell position
395 Q_ASSERT(cellElement->m_rowIndex == m_rowIndex && cellElement->m_columnIndex == m_columnIndex);
396 }
397 }
398
399 cache->insertElement(axid, self);
400 }
401 return iface;
402}
403
404//
405// accessibility protocol
406//
407
408- (BOOL)isAccessibilityFocused
409{
410 // Just check if the app thinks we're focused.
411 id focusedElement = NSApp.accessibilityApplicationFocusedUIElement;
412 return [focusedElement isEqual:self];
413}
414
415// attributes
416
417+ (id) lineNumberForIndex: (int)index forText:(const QString &)text
418{
419 auto textBefore = QStringView(text).left(index);
420 qsizetype newlines = textBefore.count(u'\n');
421 return @(newlines);
422}
423
424- (BOOL) accessibilityNotifiesWhenDestroyed {
425 return YES;
426}
427
428- (NSString *) accessibilityRole {
429 // shortcut for cells, rows, and columns in a table
430 if (synthesizedRole)
431 return synthesizedRole;
432 if (QAccessibleInterface *iface = self.qtInterface)
433 return QCocoaAccessible::macRole(iface);
434 return NSAccessibilityUnknownRole;
435}
436
437- (NSString *) accessibilitySubRole {
438 if (QAccessibleInterface *iface = self.qtInterface)
439 return QCocoaAccessible::macSubrole(iface);
440 return NSAccessibilityUnknownRole;
441}
442
443- (NSString *) accessibilityRoleDescription {
444 if (self.qtInterface)
445 return NSAccessibilityRoleDescription(self.accessibilityRole, self.accessibilitySubRole);
446 return NSAccessibilityUnknownRole;
447}
448
449- (NSArray *) accessibilityChildren {
450 // shortcut for cells
451 if (synthesizedRole == NSAccessibilityCellRole)
452 return nil;
453
454 QAccessibleInterface *iface = self.qtInterface;
455 if (!iface)
456 return nil;
457 if (QAccessibleTableInterface *table = iface->tableInterface()) {
458 // either a table or table rows/columns
459 if (!synthesizedRole) {
460 // This is the table element, parent of all rows and columns
461 /*
462 * Typical 2x2 table hierarchy as can be observed in a table found under
463 * Apple -> System Settings -> General -> Login Items (macOS 13)
464 *
465 * (AXTable)
466 * | Columns: NSArray* (2 items)
467 * | Rows: NSArray* (2 items)
468 * | Visible Columns: NSArray* (2 items)
469 * | Visible Rows: NSArray* (2 items)
470 * | Children: NSArray* (5 items)
471 +----<--| Header: (AXGroup)
472 | * +-- (AXRow)
473 | * | +-- (AXText)
474 | * | +-- (AXTextField)
475 | * +-- (AXRow)
476 | * | +-- (AXText)
477 | * | +-- (AXTextField)
478 | * +-- (AXColumn)
479 | * | Header: "Item" (sort button)
480 | * | Index: 0
481 | * | Rows: NSArray* (2 items)
482 | * | Visible Rows: NSArray* (2 items)
483 | * +-- (AXColumn)
484 | * | Header: "Kind" (sort button)
485 | * | Index: 1
486 | * | Rows: NSArray* (2 items)
487 | * | Visible Rows: NSArray* (2 items)
488 +----> +-- (AXGroup)
489 * +-- (AXButton/AXSortButton) Item [NSAccessibilityTableHeaderCellProxy]
490 * +-- (AXButton/AXSortButton) Kind [NSAccessibilityTableHeaderCellProxy]
491 */
492 NSArray *rs = [self accessibilityRows];
493 NSArray *cs = [self accessibilityColumns];
494 const int rCount = int([rs count]);
495 const int cCount = int([cs count]);
496 int childCount = rCount + cCount;
497 NSMutableArray<QMacAccessibilityElement *> *tableChildren =
498 [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:childCount];
499 for (int i = 0; i < rCount; ++i) {
500 [tableChildren addObject:[rs objectAtIndex:i]];
501 }
502 for (int i = 0; i < cCount; ++i) {
503 [tableChildren addObject:[cs objectAtIndex:i]];
504 }
505 return NSAccessibilityUnignoredChildren(tableChildren);
506 } else if (synthesizedRole == NSAccessibilityColumnRole) {
507 return nil;
508 } else if (synthesizedRole == NSAccessibilityRowRole) {
509 // axid matches the parent table axid so that we can easily find the parent table
510 // children of row are cell/any items
511 Q_ASSERT(m_rowIndex >= 0);
512 Q_ASSERT(rows == nil);
513 const unsigned int numColumns = table->columnCount();
514 if (!columns || columns.count != numColumns) {
515 if (columns) {
516 [columns autorelease];
517 columns = nil;
518 }
519 columns = [self populateTableRow:numColumns];
520 [columns retain];
521 }
522 return NSAccessibilityUnignoredChildren(columns);
523 }
524 }
525 return QCocoaAccessible::unignoredChildren(iface);
526}
527
528- (NSArray *) accessibilitySelectedChildren {
529 QAccessibleInterface *iface = QAccessible::accessibleInterface(axid);
530 if (!iface || !iface->isValid())
531 return nil;
532
533 QAccessibleSelectionInterface *selection = iface->selectionInterface();
534 if (!selection)
535 return nil;
536
537 const QList<QAccessibleInterface *> selectedList = selection->selectedItems();
538 const qsizetype numSelected = selectedList.size();
539 NSMutableArray<QMacAccessibilityElement *> *selectedChildren =
540 [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:numSelected];
541 for (QAccessibleInterface *selectedChild : selectedList) {
542 if (selectedChild && selectedChild->isValid()) {
543 QAccessible::Id id = QAccessible::uniqueId(selectedChild);
544 QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId:id];
545 if (element)
546 [selectedChildren addObject:element];
547 }
548 }
549 return NSAccessibilityUnignoredChildren(selectedChildren);
550}
551
552- (id) accessibilityWindow {
553 // Go up until we find a parent that is a window
554 NSAccessibilityElement *parent = self.accessibilityParent;
555 if (parent && parent.accessibilityRole == NSAccessibilityWindowRole)
556 return parent;
557 return [parent accessibilityWindow];
558}
559
560- (id) accessibilityTopLevelUIElementAttribute {
561 // We're in the same top level element as our parent.
562 return [self.accessibilityParent accessibilityTopLevelUIElementAttribute];
563}
564
565- (NSString *) accessibilityTitle {
566 if (QAccessibleInterface *iface = self.qtInterface) {
567 if (iface->role() == QAccessible::StaticText)
568 return nil;
569 if (self.isManagedByParent)
570 return nil;
571 return iface->text(QAccessible::Name).toNSString();
572 }
573 return nil;
574}
575
576- (id) accessibilityTitleUIElement {
577 QAccessibleInterface *iface = self.qtInterface;
578 if (!iface)
579 return nil;
580
581 const auto labelRelations = iface->relations(QAccessible::Label);
582 if (labelRelations.empty())
583 return nil;
584
585 QAccessibleInterface *label = labelRelations.first().first;
586 if (!label)
587 return nil;
588
589 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:label];
590 if (!accessibleElement)
591 return nil;
592
593 return NSAccessibilityUnignoredAncestor(accessibleElement);
594}
595
596- (NSString*) accessibilityIdentifier {
597 if (QAccessibleInterface *iface = self.qtInterface)
598 return QAccessibleBridgeUtils::accessibleId(iface).toNSString();
599 return nil;
600}
601
602- (BOOL) isAccessibilityEnabled {
603 if (QAccessibleInterface *iface = self.qtInterface)
604 return !iface->state().disabled;
605 return false;
606}
607
608- (id)accessibilityParent {
609 if (synthesizedRole == NSAccessibilityCellRole) {
610 // a synthetic cell without interface - shortcut to the row
611 QMacAccessibilityElement *tableElement =
612 [QMacAccessibilityElement elementWithId:axid];
613 Q_ASSERT(tableElement && tableElement->rows);
614 Q_ASSERT(int(tableElement->rows.count) > m_rowIndex);
615 QMacAccessibilityElement *rowElement = tableElement->rows[m_rowIndex];
616 return rowElement;
617 }
618
619 QAccessibleInterface *iface = self.qtInterface;
620 if (!iface)
621 return nil;
622
623 if (self.isManagedByParent) {
624 // axid is the same for the parent element
625 return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithId:axid]);
626 }
627
628 if (QAccessibleInterface *parent = iface->parent()) {
629 if (parent->tableInterface()) {
630 QMacAccessibilityElement *tableElement =
631 [QMacAccessibilityElement elementWithInterface:parent];
632
633 // parent of cell should be row
634 int rowIndex = -1;
635 if (m_rowIndex >= 0 && m_columnIndex >= 0)
636 rowIndex = m_rowIndex;
637 else if (QAccessibleTableCellInterface *cell = iface->tableCellInterface())
638 rowIndex = cell->rowIndex();
639 Q_ASSERT(tableElement->rows);
640 if (rowIndex > int([tableElement->rows count]) || rowIndex == -1)
641 return nil;
642 QMacAccessibilityElement *rowElement = tableElement->rows[rowIndex];
643 return NSAccessibilityUnignoredAncestor(rowElement);
644 }
645
646 // If AppKit reports the parent for us, fall through to the native
647 // view below, whose unignored ancestor is the NSWindow.
648 if (!QCocoaAccessible::isRepresentedByAppKit(parent))
649 return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithInterface: parent]);
650 }
651
652 if (QWindow *window = iface->window()) {
653 QPlatformWindow *platformWindow = window->handle();
654 if (platformWindow) {
655 QCocoaWindow *win = static_cast<QCocoaWindow*>(platformWindow);
656 return NSAccessibilityUnignoredAncestor(qnsview_cast(win->view()));
657 }
658 }
659 return nil;
660}
661
662- (NSRect)accessibilityFrame {
663 QAccessibleInterface *iface = self.qtInterface;
664 if (!iface)
665 return NSZeroRect;
666
667 QRect rect;
668 if (self.isManagedByParent) {
669 if (QAccessibleTableInterface *table = iface->tableInterface()) {
670 // Construct the geometry of the Row/Column by looking at the individual table cells
671 // ### Assumes that cells logical coordinates have spatial ordering (e.g finds the
672 // rows width by taking the union between the leftmost item and the rightmost item in
673 // a row).
674 // Otherwise, we have to iterate over *all* cells in a row/columns to
675 // find out the Row/Column geometry
676 const bool isRow = synthesizedRole == NSAccessibilityRowRole;
677 QPoint cellPos;
678 int &row = isRow ? cellPos.ry() : cellPos.rx();
679 int &col = isRow ? cellPos.rx() : cellPos.ry();
680
681 NSUInteger trackIndex = self.accessibilityIndex;
682 if (trackIndex != NSNotFound) {
683 row = int(trackIndex);
684 if (QAccessibleInterface *firstCell = table->cellAt(cellPos.y(), cellPos.x())) {
685 rect = firstCell->rect();
686 col = isRow ? table->columnCount() : table->rowCount();
687 if (col > 1) {
688 --col;
689 if (QAccessibleInterface *lastCell =
690 table->cellAt(cellPos.y(), cellPos.x()))
691 rect = rect.united(lastCell->rect());
692 }
693 }
694 }
695 }
696 } else {
697 rect = iface->rect();
698 }
699
700 return QCocoaScreen::mapToNative(rect);
701}
702
703- (NSString*)accessibilityLabel {
704 if (QAccessibleInterface *iface = self.qtInterface)
705 return iface->text(QAccessible::Description).toNSString();
706 qWarning() << "Called accessibilityLabel on invalid object: " << axid;
707 return nil;
708}
709
710- (void)setAccessibilityLabel:(NSString*)label{
711 if (QAccessibleInterface *iface = self.qtInterface)
712 iface->setText(QAccessible::Description, QString::fromNSString(label));
713}
714
715- (NSAccessibilityOrientation)accessibilityOrientation {
716 QAccessibleInterface *iface = self.qtInterface;
717 if (!iface)
718 return NSAccessibilityOrientationUnknown;
719
720 NSAccessibilityOrientation nsOrientation = NSAccessibilityOrientationUnknown;
721 if (QAccessibleAttributesInterface *attributesIface = iface->attributesInterface()) {
722 const QVariant orientationVariant =
723 attributesIface->attributeValue(QAccessible::Attribute::Orientation);
724 if (orientationVariant.isValid()) {
725 Q_ASSERT(orientationVariant.canConvert<Qt::Orientation>());
726 const Qt::Orientation orientation = orientationVariant.value<Qt::Orientation>();
727 nsOrientation = orientation == Qt::Horizontal ? NSAccessibilityOrientationHorizontal
728 : NSAccessibilityOrientationVertical;
729 }
730 }
731 return nsOrientation;
732}
733
734- (id) accessibilityValue {
735 if (QAccessibleInterface *iface = self.qtInterface) {
736 // VoiceOver asks for the value attribute for all elements. Return nil
737 // if we don't want the element to have a value attribute.
738 if (QCocoaAccessible::hasValueAttribute(iface))
739 return QCocoaAccessible::getValueAttribute(iface);
740 }
741 return nil;
742}
743
744
745- (id) accessibilityMinValue {
746 if (QAccessibleInterface *iface = self.qtInterface) {
747 if (iface->valueInterface()) {
748 return iface->valueInterface()->minimumValue().toString().toNSString();
749 }
750 }
751 return nil;
752}
753
754
755- (id) accessibilityMaxValue {
756 if (QAccessibleInterface *iface = self.qtInterface) {
757 if (iface->valueInterface()) {
758 return iface->valueInterface()->maximumValue().toString().toNSString();
759 }
760 }
761 return nil;
762}
763
764- (NSInteger) accessibilityNumberOfCharacters {
765 if (QAccessibleInterface *iface = self.qtInterface) {
766 if (QAccessibleTextInterface *text = iface->textInterface())
767 return text->characterCount();
768 }
769 return 0;
770}
771
772- (NSString *) accessibilitySelectedText {
773 if (QAccessibleInterface *iface = self.qtInterface) {
774 if (QAccessibleTextInterface *text = iface->textInterface()) {
775 int start = 0;
776 int end = 0;
777 text->selection(0, &start, &end);
778 return text->text(start, end).toNSString();
779 }
780 }
781 return nil;
782}
783
784- (NSRange) accessibilitySelectedTextRange {
785 QAccessibleInterface *iface = self.qtInterface;
786 if (!iface)
787 return NSRange();
788 if (QAccessibleTextInterface *text = iface->textInterface()) {
789 int start = 0;
790 int end = 0;
791 if (text->selectionCount() > 0) {
792 text->selection(0, &start, &end);
793 } else {
794 start = text->cursorPosition();
795 end = start;
796 }
797 return NSMakeRange(quint32(start), quint32(end - start));
798 }
799 return NSMakeRange(0, 0);
800}
801
802- (NSInteger)accessibilityLineForIndex:(NSInteger)index {
803 QAccessibleInterface *iface = self.qtInterface;
804 if (!iface)
805 return 0;
806 if (QAccessibleTextInterface *text = iface->textInterface()) {
807 QString textToPos = text->text(0, index);
808 return textToPos.count('\n');
809 }
810 return 0;
811}
812
813- (NSRange)accessibilityVisibleCharacterRange {
814 QAccessibleInterface *iface = self.qtInterface;
815 if (!iface)
816 return NSRange();
817 // FIXME This is not correct and may impact performance for big texts
818 if (QAccessibleTextInterface *text = iface->textInterface())
819 return NSMakeRange(0, static_cast<uint>(text->characterCount()));
820 return NSMakeRange(0, static_cast<uint>(iface->text(QAccessible::Name).length()));
821}
822
823- (NSInteger) accessibilityInsertionPointLineNumber {
824 QAccessibleInterface *iface = self.qtInterface;
825 if (!iface)
826 return 0;
827 if (QAccessibleTextInterface *text = iface->textInterface()) {
828 int position = text->cursorPosition();
829 return [self accessibilityLineForIndex:position];
830 }
831 return 0;
832}
833
834- (NSArray *)accessibilityAttributeNames {
835 NSMutableArray *attributes = [[NSMutableArray new] autorelease];
836 QAccessibleInterface *iface = self.qtInterface;
837
838#if QT_APPLE_SDK_EQUAL_OR_ABOVE(MACOS(26))
839 if (@available(macOS 26, *)) {
840 [attributes addObject:NSAccessibilityLanguageAttribute];
841
842 if (iface && iface->role() == QAccessible::Heading) {
843 QAccessibleAttributesInterface *attributesIface = iface->attributesInterface();
844 if (attributesIface
845 && attributesIface->attributeKeys().contains(QAccessible::Attribute::Level))
846 [attributes addObject:NSAccessibilityHeadingLevelAttribute];
847 }
848 }
849#endif
850
851 if (iface && iface->state().expandable)
852 [attributes addObject:NSAccessibilityExpandedAttribute];
853
854 return attributes;
855}
856
857- (id)accessibilityAttributeValue:(NSString *)attribute {
858 QAccessibleInterface *iface = self.qtInterface;
859 if (!iface) {
860 qWarning() << "Called attribute on invalid object: " << axid;
861 return nil;
862 }
863
864#if QT_APPLE_SDK_EQUAL_OR_ABOVE(MACOS(26))
865 if (@available(macOS 26, *)) {
866 if ([attribute isEqualToString:NSAccessibilityLanguageAttribute]) {
867 QAccessibleAttributesInterface *attributesIface = iface->attributesInterface();
868 if (!attributesIface
869 || !attributesIface->attributeKeys().contains(QAccessible::Attribute::Locale))
870 return nil;
871
872 const auto &localeVariant =
873 attributesIface->attributeValue(QAccessible::Attribute::Locale);
874 return localeVariant.toLocale().bcp47Name().toNSString();
875 }
876
877 if ([attribute isEqualToString:NSAccessibilityHeadingLevelAttribute]) {
878 if (iface->role() != QAccessible::Heading)
879 return @(0);
880
881 auto attributesIface = iface->attributesInterface();
882 if (!attributesIface)
883 return @(0);
884
885 const QVariant levelVariant =
886 attributesIface->attributeValue(QAccessible::Attribute::Level);
887 if (!levelVariant.isValid() || !levelVariant.canConvert<int>())
888 return @(0);
889
890 const int level = levelVariant.toInt();
891 if (level < 1)
892 return @(0);
893
894 return @(level);
895 }
896 }
897#endif
898
899 if ([attribute isEqualToString:NSAccessibilityExpandedAttribute])
900 return iface->state().expanded ? @YES : @NO;
901
902 return nil;
903}
904
905- (NSArray *)accessibilityParameterizedAttributeNames {
906
907 QAccessibleInterface *iface = self.qtInterface;
908 if (!iface) {
909 qWarning() << "Called attribute on invalid object: " << axid;
910 return nil;
911 }
912
913 if (iface->textInterface()) {
914 return @[
915 NSAccessibilityStringForRangeParameterizedAttribute,
916 NSAccessibilityLineForIndexParameterizedAttribute,
917 NSAccessibilityRangeForLineParameterizedAttribute,
918 NSAccessibilityRangeForPositionParameterizedAttribute,
919// NSAccessibilityRangeForIndexParameterizedAttribute,
920 NSAccessibilityBoundsForRangeParameterizedAttribute,
921// NSAccessibilityRTFForRangeParameterizedAttribute,
922 NSAccessibilityStyleRangeForIndexParameterizedAttribute,
923 NSAccessibilityAttributedStringForRangeParameterizedAttribute
924 ];
925 }
926
927 return nil;
928}
929
930- (id)accessibilityAttributeValue:(NSString *)attribute forParameter:(id)parameter {
931 QAccessibleInterface *iface = self.qtInterface;
932 if (!iface) {
933 qWarning() << "Called attribute on invalid object: " << axid;
934 return nil;
935 }
936
937 if (!iface->textInterface())
938 return nil;
939
940 if ([attribute isEqualToString: NSAccessibilityStringForRangeParameterizedAttribute]) {
941 NSRange range = [parameter rangeValue];
942 QString text = iface->textInterface()->text(range.location, range.location + range.length);
943 return text.toNSString();
944 }
945 if ([attribute isEqualToString: NSAccessibilityLineForIndexParameterizedAttribute]) {
946 int index = [parameter intValue];
947 if (index < 0 || index > iface->textInterface()->characterCount())
948 return nil;
949 int line = 0; // true for all single line edits
950 if (iface->state().multiLine) {
951 line = -1;
952 convertLineOffset(iface->textInterface(), &line, &index);
953 }
954 return @(line);
955 }
956 if ([attribute isEqualToString: NSAccessibilityRangeForLineParameterizedAttribute]) {
957 int line = [parameter intValue];
958 if (line < 0)
959 return nil;
960 int lineOffset = -1;
961 NSUInteger startOffset = 0;
962 NSUInteger endOffset = 0;
963 convertLineOffset(iface->textInterface(), &line, &lineOffset, &startOffset, &endOffset);
964 return [NSValue valueWithRange:NSMakeRange(startOffset, endOffset - startOffset)];
965 }
966 if ([attribute isEqualToString: NSAccessibilityBoundsForRangeParameterizedAttribute]) {
967 NSRange range = [parameter rangeValue];
968 QRect firstRect = iface->textInterface()->characterRect(range.location);
969 QRectF rect;
970 if (range.length > 0) {
971 NSUInteger position = range.location + range.length - 1;
972 if (position > range.location && iface->textInterface()->text(position, position + 1) == "\n"_L1)
973 --position;
974 QRect lastRect = iface->textInterface()->characterRect(position);
975 rect = firstRect.united(lastRect);
976 } else {
977 rect = firstRect;
978 rect.setWidth(1);
979 }
980 return [NSValue valueWithRect:QCocoaScreen::mapToNative(rect)];
981 }
982 if ([attribute isEqualToString: NSAccessibilityAttributedStringForRangeParameterizedAttribute]) {
983 NSRange range = [parameter rangeValue];
984 QString text = iface->textInterface()->text(range.location, range.location + range.length);
985 return [[NSAttributedString alloc] initWithString:text.toNSString()];
986 } else if ([attribute isEqualToString: NSAccessibilityRangeForPositionParameterizedAttribute]) {
987 QPoint point = QCocoaScreen::mapFromNative([parameter pointValue]).toPoint();
988 int offset = iface->textInterface()->offsetAtPoint(point);
989 return [NSValue valueWithRange:NSMakeRange(static_cast<NSUInteger>(offset), 1)];
990 } else if ([attribute isEqualToString: NSAccessibilityStyleRangeForIndexParameterizedAttribute]) {
991 int start = 0;
992 int end = 0;
993 iface->textInterface()->attributes([parameter intValue], &start, &end);
994 return [NSValue valueWithRange:NSMakeRange(static_cast<NSUInteger>(start), static_cast<NSUInteger>(end - start))];
995 }
996 return nil;
997}
998
999- (BOOL)accessibilityIsAttributeSettable:(NSString *)attribute {
1000 QAccessibleInterface *iface = self.qtInterface;
1001 if (!iface)
1002 return NO;
1003
1004 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
1005 return iface->state().focusable ? YES : NO;
1006 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
1007 if (iface->textInterface() && iface->state().editable)
1008 return YES;
1009 if (iface->valueInterface())
1010 return YES;
1011 return NO;
1012 } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
1013 return iface->textInterface() ? YES : NO;
1014 }
1015 return NO;
1016}
1017
1018- (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attribute {
1019 QAccessibleInterface *iface = self.qtInterface;
1020 if (!iface)
1021 return;
1022 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
1023 if (QAccessibleActionInterface *action = iface->actionInterface())
1024 action->doAction(QAccessibleActionInterface::setFocusAction());
1025 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
1026 if (iface->textInterface()) {
1027 QString text = QString::fromNSString((NSString *)value);
1028 iface->setText(QAccessible::Value, text);
1029 } else if (QAccessibleValueInterface *valueIface = iface->valueInterface()) {
1030 double val = [value doubleValue];
1031 valueIface->setCurrentValue(val);
1032 }
1033 } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
1034 if (QAccessibleTextInterface *text = iface->textInterface()) {
1035 NSRange range = [value rangeValue];
1036 if (range.length > 0)
1037 text->setSelection(0, range.location, range.location + range.length);
1038 else
1039 text->setCursorPosition(range.location);
1040 }
1041 }
1042}
1043
1044// actions
1045
1046- (NSArray *)accessibilityActionNames {
1047 NSMutableArray *nsActions = [[NSMutableArray new] autorelease];
1048 QAccessibleInterface *iface = self.qtInterface;
1049 if (!iface)
1050 return nsActions;
1051
1052 const QStringList &supportedActionNames = QAccessibleBridgeUtils::effectiveActionNames(iface);
1053 for (const QString &qtAction : supportedActionNames) {
1054 NSString *nsAction = QCocoaAccessible::getTranslatedAction(qtAction);
1055 if (nsAction)
1056 [nsActions addObject : nsAction];
1057 }
1058
1059 return nsActions;
1060}
1061
1062- (NSString *)accessibilityActionDescription:(NSString *)action {
1063 QAccessibleInterface *iface = self.qtInterface;
1064 if (!iface)
1065 return nil; // FIXME is that the right return type??
1066 QString qtAction = QCocoaAccessible::translateAction(action, iface);
1067 QString description;
1068 // Return a description from the action interface if this action is not known to the OS.
1069 if (qtAction.isEmpty()) {
1070 if (QAccessibleActionInterface *actionInterface = iface->actionInterface()) {
1071 qtAction = QString::fromNSString((NSString *)action);
1072 description = actionInterface->localizedActionDescription(qtAction);
1073 }
1074 } else {
1075 description = qAccessibleLocalizedActionDescription(qtAction);
1076 }
1077 return description.toNSString();
1078}
1079
1080- (void)accessibilityPerformAction:(NSString *)action {
1081 if (QAccessibleInterface *iface = self.qtInterface) {
1082 const QString qtAction = QCocoaAccessible::translateAction(action, iface);
1083 QAccessibleBridgeUtils::performEffectiveAction(iface, qtAction);
1084 }
1085}
1086
1087// misc
1088
1089- (BOOL)accessibilityIsIgnored {
1090 // Short-cut for placeholders and synthesized elements. Working around a bug
1091 // that corrups lists returned by NSAccessibilityUnignoredChildren, otherwise
1092 // we could ignore rows and columns that are outside the table.
1093 if (self.isManagedByParent)
1094 return false;
1095
1096 if (QAccessibleInterface *iface = self.qtInterface)
1097 return QCocoaAccessible::shouldBeIgnored(iface);
1098 return true;
1099}
1100
1101- (id)accessibilityHitTest:(NSPoint)point
1102{
1103 QAccessibleInterface *iface = self.qtInterface;
1104 if (!iface)
1105 return NSAccessibilityUnignoredAncestor(self);
1106
1107 QPointF screenPoint = QCocoaScreen::mapFromNative(point);
1108 QWindow *window = QAccessibleBridgeUtils::windowFor(iface);
1109
1110 // Find the deepest child at the point, stopping where a child lives in a
1111 // window of its own, as that window's view answers for its own content.
1112 QAccessibleInterface *childInterface = iface;
1113 while (QAccessibleInterface *child = childInterface->childAt(screenPoint.x(), screenPoint.y())) {
1114 if (!child->isValid())
1115 break;
1116
1117 QWindow *childWindow = QAccessibleBridgeUtils::windowFor(child, childInterface, window);
1118 if (childWindow != window) {
1119 if (auto *view = QCocoaAccessible::accessibleViewFor(childWindow))
1120 return [view accessibilityHitTest:point];
1121 }
1122
1123 childInterface = child;
1124 }
1125
1126 // No child found, meaning we hit this element.
1127 if (childInterface == iface)
1128 return NSAccessibilityUnignoredAncestor(self);
1129
1130 // hit a child, forward to child accessible interface.
1131 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface];
1132 if (accessibleElement)
1133 return NSAccessibilityUnignoredAncestor(accessibleElement);
1134 return NSAccessibilityUnignoredAncestor(self);
1135}
1136
1137- (id)accessibilityFocusedUIElement {
1138 QAccessibleInterface *iface = self.qtInterface;
1139 if (!iface) {
1140 qWarning("FocusedUIElement for INVALID");
1141 return nil;
1142 }
1143
1144 QAccessibleInterface *focusInterface = iface->focusChild();
1145 if (focusInterface && focusInterface->isValid()) {
1146 // The focus may have moved into a window of its own, whose view answers
1147 // for its own content. QAccessibleWindow::focusChild() reports
1148 // the child window holding the focus, so this is the ordinary way for
1149 // the focus to leave our window. Note that focusChild() answers with
1150 // the deepest focused interface, which may be any depth below us, and
1151 // need not report a window of its own.
1152 QWindow *window = QAccessibleBridgeUtils::windowFor(iface);
1153 QWindow *focusWindow = QAccessibleBridgeUtils::windowFor(focusInterface, iface, window);
1154 if (focusWindow != window) {
1155 if (auto *view = QCocoaAccessible::accessibleViewFor(focusWindow))
1156 return [view accessibilityFocusedUIElement];
1157 }
1158
1159 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:focusInterface];
1160 return NSAccessibilityUnignoredAncestor(accessibleElement);
1161 }
1162
1163 return NSAccessibilityUnignoredAncestor(self);
1164}
1165
1166- (NSString *) accessibilityHelp {
1167 if (QAccessibleInterface *iface = self.qtInterface) {
1168 const QString helpText = iface->text(QAccessible::Help);
1169 if (!helpText.isEmpty())
1170 return helpText.toNSString();
1171 }
1172 return nil;
1173}
1174
1175/*
1176 * Support for table
1177 */
1178- (NSInteger) accessibilityIndex {
1179 NSInteger index = 0;
1180 if (synthesizedRole == NSAccessibilityCellRole)
1181 return m_columnIndex;
1182 if (QAccessibleInterface *iface = self.qtInterface) {
1183 if (self.isManagedByParent) {
1184 // axid matches the parent table axid so that we can easily find the parent table
1185 // children of row are cell/any items
1186 if (iface->tableInterface()) {
1187 if (m_rowIndex >= 0)
1188 index = NSInteger(m_rowIndex);
1189 else if (m_columnIndex >= 0)
1190 index = NSInteger(m_columnIndex);
1191 }
1192 }
1193 }
1194 return index;
1195}
1196
1197- (NSArray *) accessibilityRows {
1198 if (!synthesizedRole && rows) {
1199 QAccessibleInterface *iface = self.qtInterface;
1200 QAccessibleTableInterface *tableInterface = iface ? iface->tableInterface() : nullptr;
1201 if (tableInterface) {
1202 const unsigned int rowCount = tableInterface->rowCount();
1203 if (rows.count != rowCount) {
1204 qCDebug(lcAccessibilityTable) << "Updating table rows with" << rowCount << "rows";
1205 if (rows) {
1206 [rows autorelease];
1207 rows = nil;
1208 }
1209 rows = [self populateTableArray:NSAccessibilityRowRole
1210 count:rowCount];
1211 [rows retain];
1212 }
1213 return NSAccessibilityUnignoredChildren(rows);
1214 }
1215 }
1216 return nil;
1217}
1218
1219- (NSArray *) accessibilityColumns {
1220 // we only implement this for a table, not for rows
1221 if (!synthesizedRole && columns) {
1222 QAccessibleInterface *iface = self.qtInterface;
1223 if (iface && iface->tableInterface())
1224 return NSAccessibilityUnignoredChildren(columns);
1225 }
1226 return nil;
1227}
1228
1229// tabs
1230
1231- (NSArray *) accessibilityTabs {
1232 QAccessibleInterface *iface = self.qtInterface;
1233 if (iface && iface->role() == QAccessible::PageTabList) {
1234 return QCocoaAccessible::unignoredChildren(iface, [](QAccessibleInterface *child){
1235 return QCocoaAccessible::defaultUnignored(child)
1236 && child->role() == QAccessible::PageTab;
1237 });
1238 }
1239 return nil;
1240}
1241
1242@end
1243
1244#endif // QT_CONFIG(accessibility)