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// called by QAccessibleCache after removing the element, which also releases
220- (void)invalidate {
221 axid = 0;
222 if (rows) {
223 [rows autorelease];
224 rows = nil;
225 }
226 if (columns) {
227 [columns autorelease];
228 columns = nil;
229 }
230 synthesizedRole = nil;
231
232 NSAccessibilityPostNotification(self, NSAccessibilityUIElementDestroyedNotification);
233}
234
235// The view and accessibility cache own the interfaces. Native element teardown
236// must not delete interfaces that the table still references.
237- (void)dealloc {
238 if (axid)
239 NSAccessibilityPostNotification(self, NSAccessibilityUIElementDestroyedNotification);
240 if (rows)
241 [rows release]; // will also release all entries first
242 if (columns)
243 [columns release]; // will also release all entries first
244 [super dealloc];
245}
246
247- (BOOL)isEqual:(id)object {
248 if ([object isKindOfClass:[QMacAccessibilityElement class]]) {
249 QMacAccessibilityElement *other = object;
250 // invalidated elements are only equal to themselves
251 if (!axid)
252 return other == self;
253 // synthesized rows, columns, and cells share the table's axid
254 return other->axid == axid && other->synthesizedRole == synthesizedRole
255 && (!synthesizedRole
256 || (other->m_rowIndex == m_rowIndex && other->m_columnIndex == m_columnIndex));
257 } else {
258 return NO;
259 }
260}
261
262- (NSUInteger)hash {
263 return axid;
264}
265
266- (BOOL)isManagedByParent {
267 return synthesizedRole != nil;
268}
269
270- (NSMutableArray *)populateTableArray:(NSAccessibilityRole)role count:(int)count
271{
272 if (self.qtInterface) {
273 auto *array = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:count];
274 Q_ASSERT(array);
275 for (int n = 0; n < count; ++n) {
276 // columns will have same axid as table (but not inserted in cache)
277 QMacAccessibilityElement *element =
278 [[QMacAccessibilityElement alloc] initWithId:axid role:role];
279 if (element) {
280 if (role == NSAccessibilityRowRole)
281 element->m_rowIndex = n;
282 else if (role == NSAccessibilityColumnRole)
283 element->m_columnIndex = n;
284 [array addObject:element];
285 [element release];
286 } else {
287 qWarning("QCocoaAccessibility: invalid child");
288 }
289 }
290 return array;
291 }
292 return nil;
293}
294
295- (NSMutableArray *)populateTableRow:(int)count
296{
297 Q_ASSERT(synthesizedRole == NSAccessibilityRowRole);
298 qCDebug(lcAccessibilityTable) << "Populating table row" << m_rowIndex
299 << "with" << count << "placeholder cells";
300 // When macOS asks for the children of a row, then we populate the row's column
301 // array with synthetic elements as place holders. This way, we don't have to
302 // create QAccessibleInterfaces for every cell before they are really needed.
303 // We don't add those synthetic elements into the cache, and we give them the
304 // same axid as the table. This way, we can get easily to the table, and from
305 // there to the QAccessibleInterface for the cell, when we have to eventually
306 // associate such an interface with the element (at which point it is no longer
307 // a placeholder).
308 auto *array = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:count];
309 Q_ASSERT(array);
310
311 for (int n = 0; n < count; ++n) {
312 // columns will have same axid as table (but not inserted in cache)
313 QMacAccessibilityElement *cell =
314 [[QMacAccessibilityElement alloc] initWithId:axid role:NSAccessibilityCellRole];
315 if (cell) {
316 cell->m_rowIndex = m_rowIndex;
317 cell->m_columnIndex = n;
318 [array addObject:cell];
319 [cell release];
320 }
321 }
322 return array;
323}
324
325- (void)updateTableModel
326{
327 if (QAccessibleInterface *iface = self.qtInterface) {
328 if (QAccessibleTableInterface *table = iface->tableInterface()) {
329 Q_ASSERT(!self.isManagedByParent);
330 qCDebug(lcAccessibilityTable) << "Updating table representation with"
331 << table->rowCount() << table->columnCount();
332 if (rows) {
333 [rows autorelease];
334 rows = nil;
335 }
336 rows = [self populateTableArray:NSAccessibilityRowRole count:table->rowCount()];
337 [rows retain];
338 if (columns) {
339 [columns autorelease];
340 columns = nil;
341 }
342 columns = [self populateTableArray:NSAccessibilityColumnRole count:table->columnCount()];
343 [columns retain];
344 }
345 }
346}
347
348- (QAccessibleInterface *)qtInterface
349{
350 QAccessibleInterface *iface = QAccessible::accessibleInterface(axid);
351 if (!iface || !iface->isValid())
352 return nullptr;
353
354 // If this is a placeholder element for a table cell, associate it with the
355 // cell interface (which will be created now, if needed). The current axid is
356 // for the table to which the cell belongs, so iface is pointing at the table.
357 if (synthesizedRole == NSAccessibilityCellRole) {
358 // get the cell interface - there must be a valid one
359 QAccessibleTableInterface *table = iface->tableInterface();
360 Q_ASSERT(table);
361 QAccessibleInterface *cell = table->cellAt(m_rowIndex, m_columnIndex);
362 if (!cell)
363 return nullptr;
364 Q_ASSERT(cell->isValid());
365 iface = cell;
366
367 // no longer a placeholder
368 axid = QAccessible::uniqueId(cell);
369 synthesizedRole = nil;
370
371 // If there already is another element for this cell in the cache, then
372 // the cache invalidates and releases it. That element might have been
373 // created for a different position, if rows or columns moved since.
374 QAccessibleCache *cache = QAccessibleCache::instance();
375 Q_ASSERT(!cache->elementForId(axid) || !cache->elementForId(axid)->synthesizedRole);
376 cache->insertElement(axid, self);
377 }
378 return iface;
379}
380
381//
382// accessibility protocol
383//
384
385- (BOOL)isAccessibilityFocused
386{
387 // Just check if the app thinks we're focused.
388 id focusedElement = NSApp.accessibilityApplicationFocusedUIElement;
389 return [focusedElement isEqual:self];
390}
391
392// attributes
393
394+ (id) lineNumberForIndex: (int)index forText:(const QString &)text
395{
396 auto textBefore = QStringView(text).left(index);
397 qsizetype newlines = textBefore.count(u'\n');
398 return @(newlines);
399}
400
401- (BOOL) accessibilityNotifiesWhenDestroyed {
402 return YES;
403}
404
405- (NSString *) accessibilityRole {
406 // shortcut for cells, rows, and columns in a table
407 if (synthesizedRole)
408 return synthesizedRole;
409 if (QAccessibleInterface *iface = self.qtInterface)
410 return QCocoaAccessible::macRole(iface);
411 return NSAccessibilityUnknownRole;
412}
413
414- (NSString *) accessibilitySubRole {
415 if (QAccessibleInterface *iface = self.qtInterface)
416 return QCocoaAccessible::macSubrole(iface);
417 return NSAccessibilityUnknownRole;
418}
419
420- (NSString *) accessibilityRoleDescription {
421 if (self.qtInterface)
422 return NSAccessibilityRoleDescription(self.accessibilityRole, self.accessibilitySubRole);
423 return NSAccessibilityUnknownRole;
424}
425
426- (NSArray *) accessibilityChildren {
427 // shortcut for cells
428 if (synthesizedRole == NSAccessibilityCellRole)
429 return nil;
430
431 QAccessibleInterface *iface = self.qtInterface;
432 if (!iface)
433 return nil;
434 if (QAccessibleTableInterface *table = iface->tableInterface()) {
435 // either a table or table rows/columns
436 if (!synthesizedRole) {
437 // This is the table element, parent of all rows and columns
438 /*
439 * Typical 2x2 table hierarchy as can be observed in a table found under
440 * Apple -> System Settings -> General -> Login Items (macOS 13)
441 *
442 * (AXTable)
443 * | Columns: NSArray* (2 items)
444 * | Rows: NSArray* (2 items)
445 * | Visible Columns: NSArray* (2 items)
446 * | Visible Rows: NSArray* (2 items)
447 * | Children: NSArray* (5 items)
448 +----<--| Header: (AXGroup)
449 | * +-- (AXRow)
450 | * | +-- (AXText)
451 | * | +-- (AXTextField)
452 | * +-- (AXRow)
453 | * | +-- (AXText)
454 | * | +-- (AXTextField)
455 | * +-- (AXColumn)
456 | * | Header: "Item" (sort button)
457 | * | Index: 0
458 | * | Rows: NSArray* (2 items)
459 | * | Visible Rows: NSArray* (2 items)
460 | * +-- (AXColumn)
461 | * | Header: "Kind" (sort button)
462 | * | Index: 1
463 | * | Rows: NSArray* (2 items)
464 | * | Visible Rows: NSArray* (2 items)
465 +----> +-- (AXGroup)
466 * +-- (AXButton/AXSortButton) Item [NSAccessibilityTableHeaderCellProxy]
467 * +-- (AXButton/AXSortButton) Kind [NSAccessibilityTableHeaderCellProxy]
468 */
469 NSArray *rs = [self accessibilityRows];
470 NSArray *cs = [self accessibilityColumns];
471 const int rCount = int([rs count]);
472 const int cCount = int([cs count]);
473 int childCount = rCount + cCount;
474 NSMutableArray<QMacAccessibilityElement *> *tableChildren =
475 [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:childCount];
476 for (int i = 0; i < rCount; ++i) {
477 [tableChildren addObject:[rs objectAtIndex:i]];
478 }
479 for (int i = 0; i < cCount; ++i) {
480 [tableChildren addObject:[cs objectAtIndex:i]];
481 }
482 return NSAccessibilityUnignoredChildren(tableChildren);
483 } else if (synthesizedRole == NSAccessibilityColumnRole) {
484 return nil;
485 } else if (synthesizedRole == NSAccessibilityRowRole) {
486 // axid matches the parent table axid so that we can easily find the parent table
487 // children of row are cell/any items
488 Q_ASSERT(m_rowIndex >= 0);
489 Q_ASSERT(rows == nil);
490 const unsigned int numColumns = table->columnCount();
491 if (!columns || columns.count != numColumns) {
492 if (columns) {
493 [columns autorelease];
494 columns = nil;
495 }
496 columns = [self populateTableRow:numColumns];
497 [columns retain];
498 }
499 return NSAccessibilityUnignoredChildren(columns);
500 }
501 }
502 return QCocoaAccessible::unignoredChildren(iface);
503}
504
505- (NSArray *) accessibilitySelectedChildren {
506 QAccessibleInterface *iface = QAccessible::accessibleInterface(axid);
507 if (!iface || !iface->isValid())
508 return nil;
509
510 QAccessibleSelectionInterface *selection = iface->selectionInterface();
511 if (!selection)
512 return nil;
513
514 const QList<QAccessibleInterface *> selectedList = selection->selectedItems();
515 const qsizetype numSelected = selectedList.size();
516 NSMutableArray<QMacAccessibilityElement *> *selectedChildren =
517 [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:numSelected];
518 for (QAccessibleInterface *selectedChild : selectedList) {
519 if (selectedChild && selectedChild->isValid()) {
520 QAccessible::Id id = QAccessible::uniqueId(selectedChild);
521 QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId:id];
522 if (element)
523 [selectedChildren addObject:element];
524 }
525 }
526 return NSAccessibilityUnignoredChildren(selectedChildren);
527}
528
529- (id) accessibilityWindow {
530 // Go up until we find a parent that is a window
531 NSAccessibilityElement *parent = self.accessibilityParent;
532 if (parent && parent.accessibilityRole == NSAccessibilityWindowRole)
533 return parent;
534 return [parent accessibilityWindow];
535}
536
537- (id) accessibilityTopLevelUIElementAttribute {
538 // We're in the same top level element as our parent.
539 return [self.accessibilityParent accessibilityTopLevelUIElementAttribute];
540}
541
542- (NSString *) accessibilityTitle {
543 if (QAccessibleInterface *iface = self.qtInterface) {
544 if (iface->role() == QAccessible::StaticText)
545 return nil;
546 if (self.isManagedByParent)
547 return nil;
548 return iface->text(QAccessible::Name).toNSString();
549 }
550 return nil;
551}
552
553- (id) accessibilityTitleUIElement {
554 QAccessibleInterface *iface = self.qtInterface;
555 if (!iface)
556 return nil;
557
558 const auto labelRelations = iface->relations(QAccessible::Label);
559 if (labelRelations.empty())
560 return nil;
561
562 QAccessibleInterface *label = labelRelations.first().first;
563 if (!label)
564 return nil;
565
566 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:label];
567 if (!accessibleElement)
568 return nil;
569
570 return NSAccessibilityUnignoredAncestor(accessibleElement);
571}
572
573- (NSString*) accessibilityIdentifier {
574 if (QAccessibleInterface *iface = self.qtInterface)
575 return QAccessibleBridgeUtils::accessibleId(iface).toNSString();
576 return nil;
577}
578
579- (BOOL) isAccessibilityEnabled {
580 if (QAccessibleInterface *iface = self.qtInterface)
581 return !iface->state().disabled;
582 return false;
583}
584
585- (id)accessibilityParent {
586 if (synthesizedRole == NSAccessibilityCellRole) {
587 QAccessibleInterface *tableInterface = QAccessible::accessibleInterface(axid);
588 if (!tableInterface || !tableInterface->isValid())
589 return nil;
590 QMacAccessibilityElement *tableElement =
591 [QMacAccessibilityElement elementWithId:axid];
592 if (!tableElement->rows || m_rowIndex < 0
593 || m_rowIndex >= int(tableElement->rows.count))
594 return nil;
595 QMacAccessibilityElement *rowElement = tableElement->rows[m_rowIndex];
596 return rowElement;
597 }
598
599 QAccessibleInterface *iface = self.qtInterface;
600 if (!iface)
601 return nil;
602
603 if (self.isManagedByParent) {
604 // axid is the same for the parent element
605 return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithId:axid]);
606 }
607
608 if (QAccessibleInterface *parent = iface->parent()) {
609 if (parent->tableInterface()) {
610 QMacAccessibilityElement *tableElement =
611 [QMacAccessibilityElement elementWithInterface:parent];
612
613 // parent of cell should be row; the row index we were created with
614 // is outdated if rows got inserted or removed since.
615 int rowIndex = -1;
616 if (QAccessibleTableCellInterface *cell = iface->tableCellInterface())
617 rowIndex = cell->rowIndex();
618 else if (m_rowIndex >= 0 && m_columnIndex >= 0)
619 rowIndex = m_rowIndex;
620 if (!tableElement->rows || rowIndex < 0 || rowIndex >= int(tableElement->rows.count))
621 return nil;
622 QMacAccessibilityElement *rowElement = tableElement->rows[rowIndex];
623 return NSAccessibilityUnignoredAncestor(rowElement);
624 }
625
626 // If AppKit reports the parent for us, fall through to the native
627 // view below, whose unignored ancestor is the NSWindow.
628 if (!QCocoaAccessible::isRepresentedByAppKit(parent))
629 return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithInterface: parent]);
630 }
631
632 if (QWindow *window = iface->window()) {
633 QPlatformWindow *platformWindow = window->handle();
634 if (platformWindow) {
635 QCocoaWindow *win = static_cast<QCocoaWindow*>(platformWindow);
636 return NSAccessibilityUnignoredAncestor(qnsview_cast(win->view()));
637 }
638 }
639 return nil;
640}
641
642- (NSRect)accessibilityFrame {
643 QAccessibleInterface *iface = self.qtInterface;
644 if (!iface)
645 return NSZeroRect;
646
647 QRect rect;
648 if (self.isManagedByParent) {
649 if (QAccessibleTableInterface *table = iface->tableInterface()) {
650 // Construct the geometry of the Row/Column by looking at the individual table cells
651 // ### Assumes that cells logical coordinates have spatial ordering (e.g finds the
652 // rows width by taking the union between the leftmost item and the rightmost item in
653 // a row).
654 // Otherwise, we have to iterate over *all* cells in a row/columns to
655 // find out the Row/Column geometry
656 const bool isRow = synthesizedRole == NSAccessibilityRowRole;
657 QPoint cellPos;
658 int &row = isRow ? cellPos.ry() : cellPos.rx();
659 int &col = isRow ? cellPos.rx() : cellPos.ry();
660
661 NSUInteger trackIndex = self.accessibilityIndex;
662 if (trackIndex != NSNotFound) {
663 row = int(trackIndex);
664 if (QAccessibleInterface *firstCell = table->cellAt(cellPos.y(), cellPos.x())) {
665 rect = firstCell->rect();
666 col = isRow ? table->columnCount() : table->rowCount();
667 if (col > 1) {
668 --col;
669 if (QAccessibleInterface *lastCell =
670 table->cellAt(cellPos.y(), cellPos.x()))
671 rect = rect.united(lastCell->rect());
672 }
673 }
674 }
675 }
676 } else {
677 rect = iface->rect();
678 }
679
680 return QCocoaScreen::mapToNative(rect);
681}
682
683- (NSString*)accessibilityLabel {
684 if (QAccessibleInterface *iface = self.qtInterface)
685 return iface->text(QAccessible::Description).toNSString();
686 qWarning() << "Called accessibilityLabel on invalid object: " << axid;
687 return nil;
688}
689
690- (void)setAccessibilityLabel:(NSString*)label{
691 if (QAccessibleInterface *iface = self.qtInterface)
692 iface->setText(QAccessible::Description, QString::fromNSString(label));
693}
694
695- (NSAccessibilityOrientation)accessibilityOrientation {
696 QAccessibleInterface *iface = self.qtInterface;
697 if (!iface)
698 return NSAccessibilityOrientationUnknown;
699
700 NSAccessibilityOrientation nsOrientation = NSAccessibilityOrientationUnknown;
701 if (QAccessibleAttributesInterface *attributesIface = iface->attributesInterface()) {
702 const QVariant orientationVariant =
703 attributesIface->attributeValue(QAccessible::Attribute::Orientation);
704 if (orientationVariant.isValid()) {
705 Q_ASSERT(orientationVariant.canConvert<Qt::Orientation>());
706 const Qt::Orientation orientation = orientationVariant.value<Qt::Orientation>();
707 nsOrientation = orientation == Qt::Horizontal ? NSAccessibilityOrientationHorizontal
708 : NSAccessibilityOrientationVertical;
709 }
710 }
711 return nsOrientation;
712}
713
714- (id) accessibilityValue {
715 if (QAccessibleInterface *iface = self.qtInterface) {
716 // VoiceOver asks for the value attribute for all elements. Return nil
717 // if we don't want the element to have a value attribute.
718 if (QCocoaAccessible::hasValueAttribute(iface))
719 return QCocoaAccessible::getValueAttribute(iface);
720 }
721 return nil;
722}
723
724
725- (id) accessibilityMinValue {
726 if (QAccessibleInterface *iface = self.qtInterface) {
727 if (iface->valueInterface()) {
728 return iface->valueInterface()->minimumValue().toString().toNSString();
729 }
730 }
731 return nil;
732}
733
734
735- (id) accessibilityMaxValue {
736 if (QAccessibleInterface *iface = self.qtInterface) {
737 if (iface->valueInterface()) {
738 return iface->valueInterface()->maximumValue().toString().toNSString();
739 }
740 }
741 return nil;
742}
743
744- (NSInteger) accessibilityNumberOfCharacters {
745 if (QAccessibleInterface *iface = self.qtInterface) {
746 if (QAccessibleTextInterface *text = iface->textInterface())
747 return text->characterCount();
748 }
749 return 0;
750}
751
752- (NSString *) accessibilitySelectedText {
753 if (QAccessibleInterface *iface = self.qtInterface) {
754 if (QAccessibleTextInterface *text = iface->textInterface()) {
755 int start = 0;
756 int end = 0;
757 text->selection(0, &start, &end);
758 return text->text(start, end).toNSString();
759 }
760 }
761 return nil;
762}
763
764- (NSRange) accessibilitySelectedTextRange {
765 QAccessibleInterface *iface = self.qtInterface;
766 if (!iface)
767 return NSRange();
768 if (QAccessibleTextInterface *text = iface->textInterface()) {
769 int start = 0;
770 int end = 0;
771 if (text->selectionCount() > 0) {
772 text->selection(0, &start, &end);
773 } else {
774 start = text->cursorPosition();
775 end = start;
776 }
777 return NSMakeRange(quint32(start), quint32(end - start));
778 }
779 return NSMakeRange(0, 0);
780}
781
782- (NSInteger)accessibilityLineForIndex:(NSInteger)index {
783 QAccessibleInterface *iface = self.qtInterface;
784 if (!iface)
785 return 0;
786 if (QAccessibleTextInterface *text = iface->textInterface()) {
787 QString textToPos = text->text(0, index);
788 return textToPos.count('\n');
789 }
790 return 0;
791}
792
793- (NSRange)accessibilityVisibleCharacterRange {
794 QAccessibleInterface *iface = self.qtInterface;
795 if (!iface)
796 return NSRange();
797 // FIXME This is not correct and may impact performance for big texts
798 if (QAccessibleTextInterface *text = iface->textInterface())
799 return NSMakeRange(0, static_cast<uint>(text->characterCount()));
800 return NSMakeRange(0, static_cast<uint>(iface->text(QAccessible::Name).length()));
801}
802
803- (NSInteger) accessibilityInsertionPointLineNumber {
804 QAccessibleInterface *iface = self.qtInterface;
805 if (!iface)
806 return 0;
807 if (QAccessibleTextInterface *text = iface->textInterface()) {
808 int position = text->cursorPosition();
809 return [self accessibilityLineForIndex:position];
810 }
811 return 0;
812}
813
814- (NSArray *)accessibilityAttributeNames {
815 NSMutableArray *attributes = [[NSMutableArray new] autorelease];
816 QAccessibleInterface *iface = self.qtInterface;
817
818#if QT_APPLE_SDK_EQUAL_OR_ABOVE(MACOS(26))
819 if (@available(macOS 26, *)) {
820 [attributes addObject:NSAccessibilityLanguageAttribute];
821
822 if (iface && iface->role() == QAccessible::Heading) {
823 QAccessibleAttributesInterface *attributesIface = iface->attributesInterface();
824 if (attributesIface
825 && attributesIface->attributeKeys().contains(QAccessible::Attribute::Level))
826 [attributes addObject:NSAccessibilityHeadingLevelAttribute];
827 }
828 }
829#endif
830
831 if (iface && iface->state().expandable)
832 [attributes addObject:NSAccessibilityExpandedAttribute];
833
834 return attributes;
835}
836
837- (id)accessibilityAttributeValue:(NSString *)attribute {
838 QAccessibleInterface *iface = self.qtInterface;
839 if (!iface) {
840 qWarning() << "Called attribute on invalid object: " << axid;
841 return nil;
842 }
843
844#if QT_APPLE_SDK_EQUAL_OR_ABOVE(MACOS(26))
845 if (@available(macOS 26, *)) {
846 if ([attribute isEqualToString:NSAccessibilityLanguageAttribute]) {
847 QAccessibleAttributesInterface *attributesIface = iface->attributesInterface();
848 if (!attributesIface
849 || !attributesIface->attributeKeys().contains(QAccessible::Attribute::Locale))
850 return nil;
851
852 const auto &localeVariant =
853 attributesIface->attributeValue(QAccessible::Attribute::Locale);
854 return localeVariant.toLocale().bcp47Name().toNSString();
855 }
856
857 if ([attribute isEqualToString:NSAccessibilityHeadingLevelAttribute]) {
858 if (iface->role() != QAccessible::Heading)
859 return @(0);
860
861 auto attributesIface = iface->attributesInterface();
862 if (!attributesIface)
863 return @(0);
864
865 const QVariant levelVariant =
866 attributesIface->attributeValue(QAccessible::Attribute::Level);
867 if (!levelVariant.isValid() || !levelVariant.canConvert<int>())
868 return @(0);
869
870 const int level = levelVariant.toInt();
871 if (level < 1)
872 return @(0);
873
874 return @(level);
875 }
876 }
877#endif
878
879 if ([attribute isEqualToString:NSAccessibilityExpandedAttribute])
880 return iface->state().expanded ? @YES : @NO;
881
882 return nil;
883}
884
885- (NSArray *)accessibilityParameterizedAttributeNames {
886
887 QAccessibleInterface *iface = self.qtInterface;
888 if (!iface) {
889 qWarning() << "Called attribute on invalid object: " << axid;
890 return nil;
891 }
892
893 if (iface->textInterface()) {
894 return @[
895 NSAccessibilityStringForRangeParameterizedAttribute,
896 NSAccessibilityLineForIndexParameterizedAttribute,
897 NSAccessibilityRangeForLineParameterizedAttribute,
898 NSAccessibilityRangeForPositionParameterizedAttribute,
899// NSAccessibilityRangeForIndexParameterizedAttribute,
900 NSAccessibilityBoundsForRangeParameterizedAttribute,
901// NSAccessibilityRTFForRangeParameterizedAttribute,
902 NSAccessibilityStyleRangeForIndexParameterizedAttribute,
903 NSAccessibilityAttributedStringForRangeParameterizedAttribute
904 ];
905 }
906
907 return nil;
908}
909
910- (id)accessibilityAttributeValue:(NSString *)attribute forParameter:(id)parameter {
911 QAccessibleInterface *iface = self.qtInterface;
912 if (!iface) {
913 qWarning() << "Called attribute on invalid object: " << axid;
914 return nil;
915 }
916
917 if (!iface->textInterface())
918 return nil;
919
920 if ([attribute isEqualToString: NSAccessibilityStringForRangeParameterizedAttribute]) {
921 NSRange range = [parameter rangeValue];
922 QString text = iface->textInterface()->text(range.location, range.location + range.length);
923 return text.toNSString();
924 }
925 if ([attribute isEqualToString: NSAccessibilityLineForIndexParameterizedAttribute]) {
926 int index = [parameter intValue];
927 if (index < 0 || index > iface->textInterface()->characterCount())
928 return nil;
929 int line = 0; // true for all single line edits
930 if (iface->state().multiLine) {
931 line = -1;
932 convertLineOffset(iface->textInterface(), &line, &index);
933 }
934 return @(line);
935 }
936 if ([attribute isEqualToString: NSAccessibilityRangeForLineParameterizedAttribute]) {
937 int line = [parameter intValue];
938 if (line < 0)
939 return nil;
940 int lineOffset = -1;
941 NSUInteger startOffset = 0;
942 NSUInteger endOffset = 0;
943 convertLineOffset(iface->textInterface(), &line, &lineOffset, &startOffset, &endOffset);
944 return [NSValue valueWithRange:NSMakeRange(startOffset, endOffset - startOffset)];
945 }
946 if ([attribute isEqualToString: NSAccessibilityBoundsForRangeParameterizedAttribute]) {
947 NSRange range = [parameter rangeValue];
948 QRect firstRect = iface->textInterface()->characterRect(range.location);
949 QRectF rect;
950 if (range.length > 0) {
951 NSUInteger position = range.location + range.length - 1;
952 if (position > range.location && iface->textInterface()->text(position, position + 1) == "\n"_L1)
953 --position;
954 QRect lastRect = iface->textInterface()->characterRect(position);
955 rect = firstRect.united(lastRect);
956 } else {
957 rect = firstRect;
958 rect.setWidth(1);
959 }
960 return [NSValue valueWithRect:QCocoaScreen::mapToNative(rect)];
961 }
962 if ([attribute isEqualToString: NSAccessibilityAttributedStringForRangeParameterizedAttribute]) {
963 NSRange range = [parameter rangeValue];
964 QString text = iface->textInterface()->text(range.location, range.location + range.length);
965 return [[NSAttributedString alloc] initWithString:text.toNSString()];
966 } else if ([attribute isEqualToString: NSAccessibilityRangeForPositionParameterizedAttribute]) {
967 QPoint point = QCocoaScreen::mapFromNative([parameter pointValue]).toPoint();
968 int offset = iface->textInterface()->offsetAtPoint(point);
969 return [NSValue valueWithRange:NSMakeRange(static_cast<NSUInteger>(offset), 1)];
970 } else if ([attribute isEqualToString: NSAccessibilityStyleRangeForIndexParameterizedAttribute]) {
971 int start = 0;
972 int end = 0;
973 iface->textInterface()->attributes([parameter intValue], &start, &end);
974 return [NSValue valueWithRange:NSMakeRange(static_cast<NSUInteger>(start), static_cast<NSUInteger>(end - start))];
975 }
976 return nil;
977}
978
979- (BOOL)accessibilityIsAttributeSettable:(NSString *)attribute {
980 QAccessibleInterface *iface = self.qtInterface;
981 if (!iface)
982 return NO;
983
984 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
985 return iface->state().focusable ? YES : NO;
986 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
987 if (iface->textInterface() && iface->state().editable)
988 return YES;
989 if (iface->valueInterface())
990 return YES;
991 return NO;
992 } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
993 return iface->textInterface() ? YES : NO;
994 }
995 return NO;
996}
997
998- (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attribute {
999 QAccessibleInterface *iface = self.qtInterface;
1000 if (!iface)
1001 return;
1002 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
1003 if (QAccessibleActionInterface *action = iface->actionInterface())
1004 action->doAction(QAccessibleActionInterface::setFocusAction());
1005 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
1006 if (iface->textInterface()) {
1007 QString text = QString::fromNSString((NSString *)value);
1008 iface->setText(QAccessible::Value, text);
1009 } else if (QAccessibleValueInterface *valueIface = iface->valueInterface()) {
1010 double val = [value doubleValue];
1011 valueIface->setCurrentValue(val);
1012 }
1013 } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
1014 if (QAccessibleTextInterface *text = iface->textInterface()) {
1015 NSRange range = [value rangeValue];
1016 if (range.length > 0)
1017 text->setSelection(0, range.location, range.location + range.length);
1018 else
1019 text->setCursorPosition(range.location);
1020 }
1021 }
1022}
1023
1024// actions
1025
1026- (NSArray *)accessibilityActionNames {
1027 NSMutableArray *nsActions = [[NSMutableArray new] autorelease];
1028 QAccessibleInterface *iface = self.qtInterface;
1029 if (!iface)
1030 return nsActions;
1031
1032 const QStringList &supportedActionNames = QAccessibleBridgeUtils::effectiveActionNames(iface);
1033 for (const QString &qtAction : supportedActionNames) {
1034 NSString *nsAction = QCocoaAccessible::getTranslatedAction(qtAction);
1035 if (nsAction)
1036 [nsActions addObject : nsAction];
1037 }
1038
1039 return nsActions;
1040}
1041
1042- (NSString *)accessibilityActionDescription:(NSString *)action {
1043 QAccessibleInterface *iface = self.qtInterface;
1044 if (!iface)
1045 return nil; // FIXME is that the right return type??
1046 QString qtAction = QCocoaAccessible::translateAction(action, iface);
1047 QString description;
1048 // Return a description from the action interface if this action is not known to the OS.
1049 if (qtAction.isEmpty()) {
1050 if (QAccessibleActionInterface *actionInterface = iface->actionInterface()) {
1051 qtAction = QString::fromNSString((NSString *)action);
1052 description = actionInterface->localizedActionDescription(qtAction);
1053 }
1054 } else {
1055 description = qAccessibleLocalizedActionDescription(qtAction);
1056 }
1057 return description.toNSString();
1058}
1059
1060- (void)accessibilityPerformAction:(NSString *)action {
1061 if (QAccessibleInterface *iface = self.qtInterface) {
1062 const QString qtAction = QCocoaAccessible::translateAction(action, iface);
1063 QAccessibleBridgeUtils::performEffectiveAction(iface, qtAction);
1064 }
1065}
1066
1067// misc
1068
1069- (BOOL)accessibilityIsIgnored {
1070 // Short-cut for placeholders and synthesized elements. Working around a bug
1071 // that corrups lists returned by NSAccessibilityUnignoredChildren, otherwise
1072 // we could ignore rows and columns that are outside the table.
1073 if (self.isManagedByParent)
1074 return false;
1075
1076 if (QAccessibleInterface *iface = self.qtInterface)
1077 return QCocoaAccessible::shouldBeIgnored(iface);
1078 return true;
1079}
1080
1081- (id)accessibilityHitTest:(NSPoint)point
1082{
1083 QAccessibleInterface *iface = self.qtInterface;
1084 if (!iface)
1085 return NSAccessibilityUnignoredAncestor(self);
1086
1087 QPointF screenPoint = QCocoaScreen::mapFromNative(point);
1088 QWindow *window = QAccessibleBridgeUtils::windowFor(iface);
1089
1090 // Find the deepest child at the point, stopping where a child lives in a
1091 // window of its own, as that window's view answers for its own content.
1092 QAccessibleInterface *childInterface = iface;
1093 while (QAccessibleInterface *child = childInterface->childAt(screenPoint.x(), screenPoint.y())) {
1094 if (!child->isValid())
1095 break;
1096
1097 QWindow *childWindow = QAccessibleBridgeUtils::windowFor(child, childInterface, window);
1098 if (childWindow != window) {
1099 if (auto *view = QCocoaAccessible::accessibleViewFor(childWindow))
1100 return [view accessibilityHitTest:point];
1101 }
1102
1103 childInterface = child;
1104 }
1105
1106 // No child found, meaning we hit this element.
1107 if (childInterface == iface)
1108 return NSAccessibilityUnignoredAncestor(self);
1109
1110 // hit a child, forward to child accessible interface.
1111 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface];
1112 if (accessibleElement)
1113 return NSAccessibilityUnignoredAncestor(accessibleElement);
1114 return NSAccessibilityUnignoredAncestor(self);
1115}
1116
1117- (id)accessibilityFocusedUIElement {
1118 QAccessibleInterface *iface = self.qtInterface;
1119 if (!iface) {
1120 qWarning("FocusedUIElement for INVALID");
1121 return nil;
1122 }
1123
1124 QAccessibleInterface *focusInterface = iface->focusChild();
1125 if (focusInterface && focusInterface->isValid()) {
1126 // The focus may have moved into a window of its own, whose view answers
1127 // for its own content. QAccessibleWindow::focusChild() reports
1128 // the child window holding the focus, so this is the ordinary way for
1129 // the focus to leave our window. Note that focusChild() answers with
1130 // the deepest focused interface, which may be any depth below us, and
1131 // need not report a window of its own.
1132 QWindow *window = QAccessibleBridgeUtils::windowFor(iface);
1133 QWindow *focusWindow = QAccessibleBridgeUtils::windowFor(focusInterface, iface, window);
1134 if (focusWindow != window) {
1135 if (auto *view = QCocoaAccessible::accessibleViewFor(focusWindow))
1136 return [view accessibilityFocusedUIElement];
1137 }
1138
1139 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:focusInterface];
1140 return NSAccessibilityUnignoredAncestor(accessibleElement);
1141 }
1142
1143 return NSAccessibilityUnignoredAncestor(self);
1144}
1145
1146- (NSString *) accessibilityHelp {
1147 if (QAccessibleInterface *iface = self.qtInterface) {
1148 const QString helpText = iface->text(QAccessible::Help);
1149 if (!helpText.isEmpty())
1150 return helpText.toNSString();
1151 }
1152 return nil;
1153}
1154
1155/*
1156 * Support for table
1157 */
1158- (NSInteger) accessibilityIndex {
1159 NSInteger index = 0;
1160 if (synthesizedRole == NSAccessibilityCellRole)
1161 return m_columnIndex;
1162 if (QAccessibleInterface *iface = self.qtInterface) {
1163 if (self.isManagedByParent) {
1164 // axid matches the parent table axid so that we can easily find the parent table
1165 // children of row are cell/any items
1166 if (iface->tableInterface()) {
1167 if (m_rowIndex >= 0)
1168 index = NSInteger(m_rowIndex);
1169 else if (m_columnIndex >= 0)
1170 index = NSInteger(m_columnIndex);
1171 }
1172 }
1173 }
1174 return index;
1175}
1176
1177- (NSArray *) accessibilityRows {
1178 if (!synthesizedRole && rows) {
1179 QAccessibleInterface *iface = self.qtInterface;
1180 QAccessibleTableInterface *tableInterface = iface ? iface->tableInterface() : nullptr;
1181 if (tableInterface) {
1182 const unsigned int rowCount = tableInterface->rowCount();
1183 if (rows.count != rowCount) {
1184 qCDebug(lcAccessibilityTable) << "Updating table rows with" << rowCount << "rows";
1185 if (rows) {
1186 [rows autorelease];
1187 rows = nil;
1188 }
1189 rows = [self populateTableArray:NSAccessibilityRowRole
1190 count:rowCount];
1191 [rows retain];
1192 }
1193 return NSAccessibilityUnignoredChildren(rows);
1194 }
1195 }
1196 return nil;
1197}
1198
1199- (NSArray *) accessibilityColumns {
1200 // we only implement this for a table, not for rows
1201 if (!synthesizedRole && columns) {
1202 QAccessibleInterface *iface = self.qtInterface;
1203 if (iface && iface->tableInterface())
1204 return NSAccessibilityUnignoredChildren(columns);
1205 }
1206 return nil;
1207}
1208
1209// tabs
1210
1211- (NSArray *) accessibilityTabs {
1212 QAccessibleInterface *iface = self.qtInterface;
1213 if (iface && iface->role() == QAccessible::PageTabList) {
1214 return QCocoaAccessible::unignoredChildren(iface, [](QAccessibleInterface *child){
1215 return QCocoaAccessible::defaultUnignored(child)
1216 && child->role() == QAccessible::PageTab;
1217 });
1218 }
1219 return nil;
1220}
1221
1222@end
1223
1224#endif // QT_CONFIG(accessibility)