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
837#if QT_APPLE_SDK_EQUAL_OR_ABOVE(MACOS(26))
838 if (@available(macOS 26, *))
839 [attributes addObject:NSAccessibilityLanguageAttribute];
840#endif
841
842 QAccessibleInterface *iface = self.qtInterface;
843 if (iface && iface->state().expandable)
844 [attributes addObject:NSAccessibilityExpandedAttribute];
845
846 return attributes;
847}
848
849- (id)accessibilityAttributeValue:(NSString *)attribute {
850 QAccessibleInterface *iface = self.qtInterface;
851 if (!iface) {
852 qWarning() << "Called attribute on invalid object: " << axid;
853 return nil;
854 }
855
856#if QT_APPLE_SDK_EQUAL_OR_ABOVE(MACOS(26))
857 if (@available(macOS 26, *)) {
858 if ([attribute isEqualToString:NSAccessibilityLanguageAttribute]) {
859 QAccessibleAttributesInterface *attributesIface = iface->attributesInterface();
860 if (!attributesIface
861 || !attributesIface->attributeKeys().contains(QAccessible::Attribute::Locale))
862 return nil;
863
864 const auto &localeVariant =
865 attributesIface->attributeValue(QAccessible::Attribute::Locale);
866 return localeVariant.toLocale().bcp47Name().toNSString();
867 }
868 }
869#endif
870
871 if ([attribute isEqualToString:NSAccessibilityExpandedAttribute])
872 return iface->state().expanded ? @YES : @NO;
873
874 return nil;
875}
876
877- (NSArray *)accessibilityParameterizedAttributeNames {
878
879 QAccessibleInterface *iface = self.qtInterface;
880 if (!iface) {
881 qWarning() << "Called attribute on invalid object: " << axid;
882 return nil;
883 }
884
885 if (iface->textInterface()) {
886 return @[
887 NSAccessibilityStringForRangeParameterizedAttribute,
888 NSAccessibilityLineForIndexParameterizedAttribute,
889 NSAccessibilityRangeForLineParameterizedAttribute,
890 NSAccessibilityRangeForPositionParameterizedAttribute,
891// NSAccessibilityRangeForIndexParameterizedAttribute,
892 NSAccessibilityBoundsForRangeParameterizedAttribute,
893// NSAccessibilityRTFForRangeParameterizedAttribute,
894 NSAccessibilityStyleRangeForIndexParameterizedAttribute,
895 NSAccessibilityAttributedStringForRangeParameterizedAttribute
896 ];
897 }
898
899 return nil;
900}
901
902- (id)accessibilityAttributeValue:(NSString *)attribute forParameter:(id)parameter {
903 QAccessibleInterface *iface = self.qtInterface;
904 if (!iface) {
905 qWarning() << "Called attribute on invalid object: " << axid;
906 return nil;
907 }
908
909 if (!iface->textInterface())
910 return nil;
911
912 if ([attribute isEqualToString: NSAccessibilityStringForRangeParameterizedAttribute]) {
913 NSRange range = [parameter rangeValue];
914 QString text = iface->textInterface()->text(range.location, range.location + range.length);
915 return text.toNSString();
916 }
917 if ([attribute isEqualToString: NSAccessibilityLineForIndexParameterizedAttribute]) {
918 int index = [parameter intValue];
919 if (index < 0 || index > iface->textInterface()->characterCount())
920 return nil;
921 int line = 0; // true for all single line edits
922 if (iface->state().multiLine) {
923 line = -1;
924 convertLineOffset(iface->textInterface(), &line, &index);
925 }
926 return @(line);
927 }
928 if ([attribute isEqualToString: NSAccessibilityRangeForLineParameterizedAttribute]) {
929 int line = [parameter intValue];
930 if (line < 0)
931 return nil;
932 int lineOffset = -1;
933 NSUInteger startOffset = 0;
934 NSUInteger endOffset = 0;
935 convertLineOffset(iface->textInterface(), &line, &lineOffset, &startOffset, &endOffset);
936 return [NSValue valueWithRange:NSMakeRange(startOffset, endOffset - startOffset)];
937 }
938 if ([attribute isEqualToString: NSAccessibilityBoundsForRangeParameterizedAttribute]) {
939 NSRange range = [parameter rangeValue];
940 QRect firstRect = iface->textInterface()->characterRect(range.location);
941 QRectF rect;
942 if (range.length > 0) {
943 NSUInteger position = range.location + range.length - 1;
944 if (position > range.location && iface->textInterface()->text(position, position + 1) == "\n"_L1)
945 --position;
946 QRect lastRect = iface->textInterface()->characterRect(position);
947 rect = firstRect.united(lastRect);
948 } else {
949 rect = firstRect;
950 rect.setWidth(1);
951 }
952 return [NSValue valueWithRect:QCocoaScreen::mapToNative(rect)];
953 }
954 if ([attribute isEqualToString: NSAccessibilityAttributedStringForRangeParameterizedAttribute]) {
955 NSRange range = [parameter rangeValue];
956 QString text = iface->textInterface()->text(range.location, range.location + range.length);
957 return [[NSAttributedString alloc] initWithString:text.toNSString()];
958 } else if ([attribute isEqualToString: NSAccessibilityRangeForPositionParameterizedAttribute]) {
959 QPoint point = QCocoaScreen::mapFromNative([parameter pointValue]).toPoint();
960 int offset = iface->textInterface()->offsetAtPoint(point);
961 return [NSValue valueWithRange:NSMakeRange(static_cast<NSUInteger>(offset), 1)];
962 } else if ([attribute isEqualToString: NSAccessibilityStyleRangeForIndexParameterizedAttribute]) {
963 int start = 0;
964 int end = 0;
965 iface->textInterface()->attributes([parameter intValue], &start, &end);
966 return [NSValue valueWithRange:NSMakeRange(static_cast<NSUInteger>(start), static_cast<NSUInteger>(end - start))];
967 }
968 return nil;
969}
970
971- (BOOL)accessibilityIsAttributeSettable:(NSString *)attribute {
972 QAccessibleInterface *iface = self.qtInterface;
973 if (!iface)
974 return NO;
975
976 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
977 return iface->state().focusable ? YES : NO;
978 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
979 if (iface->textInterface() && iface->state().editable)
980 return YES;
981 if (iface->valueInterface())
982 return YES;
983 return NO;
984 } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
985 return iface->textInterface() ? YES : NO;
986 }
987 return NO;
988}
989
990- (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attribute {
991 QAccessibleInterface *iface = self.qtInterface;
992 if (!iface)
993 return;
994 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
995 if (QAccessibleActionInterface *action = iface->actionInterface())
996 action->doAction(QAccessibleActionInterface::setFocusAction());
997 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
998 if (iface->textInterface()) {
999 QString text = QString::fromNSString((NSString *)value);
1000 iface->setText(QAccessible::Value, text);
1001 } else if (QAccessibleValueInterface *valueIface = iface->valueInterface()) {
1002 double val = [value doubleValue];
1003 valueIface->setCurrentValue(val);
1004 }
1005 } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
1006 if (QAccessibleTextInterface *text = iface->textInterface()) {
1007 NSRange range = [value rangeValue];
1008 if (range.length > 0)
1009 text->setSelection(0, range.location, range.location + range.length);
1010 else
1011 text->setCursorPosition(range.location);
1012 }
1013 }
1014}
1015
1016// actions
1017
1018- (NSArray *)accessibilityActionNames {
1019 NSMutableArray *nsActions = [[NSMutableArray new] autorelease];
1020 QAccessibleInterface *iface = self.qtInterface;
1021 if (!iface)
1022 return nsActions;
1023
1024 const QStringList &supportedActionNames = QAccessibleBridgeUtils::effectiveActionNames(iface);
1025 for (const QString &qtAction : supportedActionNames) {
1026 NSString *nsAction = QCocoaAccessible::getTranslatedAction(qtAction);
1027 if (nsAction)
1028 [nsActions addObject : nsAction];
1029 }
1030
1031 return nsActions;
1032}
1033
1034- (NSString *)accessibilityActionDescription:(NSString *)action {
1035 QAccessibleInterface *iface = self.qtInterface;
1036 if (!iface)
1037 return nil; // FIXME is that the right return type??
1038 QString qtAction = QCocoaAccessible::translateAction(action, iface);
1039 QString description;
1040 // Return a description from the action interface if this action is not known to the OS.
1041 if (qtAction.isEmpty()) {
1042 if (QAccessibleActionInterface *actionInterface = iface->actionInterface()) {
1043 qtAction = QString::fromNSString((NSString *)action);
1044 description = actionInterface->localizedActionDescription(qtAction);
1045 }
1046 } else {
1047 description = qAccessibleLocalizedActionDescription(qtAction);
1048 }
1049 return description.toNSString();
1050}
1051
1052- (void)accessibilityPerformAction:(NSString *)action {
1053 if (QAccessibleInterface *iface = self.qtInterface) {
1054 const QString qtAction = QCocoaAccessible::translateAction(action, iface);
1055 QAccessibleBridgeUtils::performEffectiveAction(iface, qtAction);
1056 }
1057}
1058
1059// misc
1060
1061- (BOOL)accessibilityIsIgnored {
1062 // Short-cut for placeholders and synthesized elements. Working around a bug
1063 // that corrups lists returned by NSAccessibilityUnignoredChildren, otherwise
1064 // we could ignore rows and columns that are outside the table.
1065 if (self.isManagedByParent)
1066 return false;
1067
1068 if (QAccessibleInterface *iface = self.qtInterface)
1069 return QCocoaAccessible::shouldBeIgnored(iface);
1070 return true;
1071}
1072
1073- (id)accessibilityHitTest:(NSPoint)point
1074{
1075 QAccessibleInterface *iface = self.qtInterface;
1076 if (!iface)
1077 return NSAccessibilityUnignoredAncestor(self);
1078
1079 QPointF screenPoint = QCocoaScreen::mapFromNative(point);
1080 QWindow *window = QAccessibleBridgeUtils::windowFor(iface);
1081
1082 // Find the deepest child at the point, stopping where a child lives in a
1083 // window of its own, as that window's view answers for its own content.
1084 QAccessibleInterface *childInterface = iface;
1085 while (QAccessibleInterface *child = childInterface->childAt(screenPoint.x(), screenPoint.y())) {
1086 if (!child->isValid())
1087 break;
1088
1089 QWindow *childWindow = QAccessibleBridgeUtils::windowFor(child, childInterface, window);
1090 if (childWindow != window) {
1091 if (auto *view = QCocoaAccessible::accessibleViewFor(childWindow))
1092 return [view accessibilityHitTest:point];
1093 }
1094
1095 childInterface = child;
1096 }
1097
1098 // No child found, meaning we hit this element.
1099 if (childInterface == iface)
1100 return NSAccessibilityUnignoredAncestor(self);
1101
1102 // hit a child, forward to child accessible interface.
1103 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface];
1104 if (accessibleElement)
1105 return NSAccessibilityUnignoredAncestor(accessibleElement);
1106 return NSAccessibilityUnignoredAncestor(self);
1107}
1108
1109- (id)accessibilityFocusedUIElement {
1110 QAccessibleInterface *iface = self.qtInterface;
1111 if (!iface) {
1112 qWarning("FocusedUIElement for INVALID");
1113 return nil;
1114 }
1115
1116 QAccessibleInterface *focusInterface = iface->focusChild();
1117 if (focusInterface && focusInterface->isValid()) {
1118 // The focus may have moved into a window of its own, whose view answers
1119 // for its own content. QAccessibleWindow::focusChild() reports
1120 // the child window holding the focus, so this is the ordinary way for
1121 // the focus to leave our window. Note that focusChild() answers with
1122 // the deepest focused interface, which may be any depth below us, and
1123 // need not report a window of its own.
1124 QWindow *window = QAccessibleBridgeUtils::windowFor(iface);
1125 QWindow *focusWindow = QAccessibleBridgeUtils::windowFor(focusInterface, iface, window);
1126 if (focusWindow != window) {
1127 if (auto *view = QCocoaAccessible::accessibleViewFor(focusWindow))
1128 return [view accessibilityFocusedUIElement];
1129 }
1130
1131 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:focusInterface];
1132 return NSAccessibilityUnignoredAncestor(accessibleElement);
1133 }
1134
1135 return NSAccessibilityUnignoredAncestor(self);
1136}
1137
1138- (NSString *) accessibilityHelp {
1139 if (QAccessibleInterface *iface = self.qtInterface) {
1140 const QString helpText = iface->text(QAccessible::Help);
1141 if (!helpText.isEmpty())
1142 return helpText.toNSString();
1143 }
1144 return nil;
1145}
1146
1147/*
1148 * Support for table
1149 */
1150- (NSInteger) accessibilityIndex {
1151 NSInteger index = 0;
1152 if (synthesizedRole == NSAccessibilityCellRole)
1153 return m_columnIndex;
1154 if (QAccessibleInterface *iface = self.qtInterface) {
1155 if (self.isManagedByParent) {
1156 // axid matches the parent table axid so that we can easily find the parent table
1157 // children of row are cell/any items
1158 if (iface->tableInterface()) {
1159 if (m_rowIndex >= 0)
1160 index = NSInteger(m_rowIndex);
1161 else if (m_columnIndex >= 0)
1162 index = NSInteger(m_columnIndex);
1163 }
1164 }
1165 }
1166 return index;
1167}
1168
1169- (NSArray *) accessibilityRows {
1170 if (!synthesizedRole && rows) {
1171 QAccessibleInterface *iface = self.qtInterface;
1172 QAccessibleTableInterface *tableInterface = iface ? iface->tableInterface() : nullptr;
1173 if (tableInterface) {
1174 const unsigned int rowCount = tableInterface->rowCount();
1175 if (rows.count != rowCount) {
1176 qCDebug(lcAccessibilityTable) << "Updating table rows with" << rowCount << "rows";
1177 if (rows) {
1178 [rows autorelease];
1179 rows = nil;
1180 }
1181 rows = [self populateTableArray:NSAccessibilityRowRole
1182 count:rowCount];
1183 [rows retain];
1184 }
1185 return NSAccessibilityUnignoredChildren(rows);
1186 }
1187 }
1188 return nil;
1189}
1190
1191- (NSArray *) accessibilityColumns {
1192 // we only implement this for a table, not for rows
1193 if (!synthesizedRole && columns) {
1194 QAccessibleInterface *iface = self.qtInterface;
1195 if (iface && iface->tableInterface())
1196 return NSAccessibilityUnignoredChildren(columns);
1197 }
1198 return nil;
1199}
1200
1201// tabs
1202
1203- (NSArray *) accessibilityTabs {
1204 QAccessibleInterface *iface = self.qtInterface;
1205 if (iface && iface->role() == QAccessible::PageTabList) {
1206 return QCocoaAccessible::unignoredChildren(iface, [](QAccessibleInterface *child){
1207 return QCocoaAccessible::defaultUnignored(child)
1208 && child->role() == QAccessible::PageTab;
1209 });
1210 }
1211 return nil;
1212}
1213
1214@end
1215
1216#endif // QT_CONFIG(accessibility)