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 // macOS expects that the hierarchy is:
646 // App -> Window -> Children
647 // We don't actually have the window reflected properly in QAccessibility;
648 // the native framework does that for us. Check if the parent is the
649 // Application or a window, and if so return the native NSView instead.
650 if (parent->role() != QAccessible::Application && parent->role() != QAccessible::Window)
651 return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithInterface: parent]);
652 }
653
654 if (QWindow *window = iface->window()) {
655 QPlatformWindow *platformWindow = window->handle();
656 if (platformWindow) {
657 QCocoaWindow *win = static_cast<QCocoaWindow*>(platformWindow);
658 return NSAccessibilityUnignoredAncestor(qnsview_cast(win->view()));
659 }
660 }
661 return nil;
662}
663
664- (NSRect)accessibilityFrame {
665 QAccessibleInterface *iface = self.qtInterface;
666 if (!iface)
667 return NSZeroRect;
668
669 QRect rect;
670 if (self.isManagedByParent) {
671 if (QAccessibleTableInterface *table = iface->tableInterface()) {
672 // Construct the geometry of the Row/Column by looking at the individual table cells
673 // ### Assumes that cells logical coordinates have spatial ordering (e.g finds the
674 // rows width by taking the union between the leftmost item and the rightmost item in
675 // a row).
676 // Otherwise, we have to iterate over *all* cells in a row/columns to
677 // find out the Row/Column geometry
678 const bool isRow = synthesizedRole == NSAccessibilityRowRole;
679 QPoint cellPos;
680 int &row = isRow ? cellPos.ry() : cellPos.rx();
681 int &col = isRow ? cellPos.rx() : cellPos.ry();
682
683 NSUInteger trackIndex = self.accessibilityIndex;
684 if (trackIndex != NSNotFound) {
685 row = int(trackIndex);
686 if (QAccessibleInterface *firstCell = table->cellAt(cellPos.y(), cellPos.x())) {
687 rect = firstCell->rect();
688 col = isRow ? table->columnCount() : table->rowCount();
689 if (col > 1) {
690 --col;
691 if (QAccessibleInterface *lastCell =
692 table->cellAt(cellPos.y(), cellPos.x()))
693 rect = rect.united(lastCell->rect());
694 }
695 }
696 }
697 }
698 } else {
699 rect = iface->rect();
700 }
701
702 return QCocoaScreen::mapToNative(rect);
703}
704
705- (NSString*)accessibilityLabel {
706 if (QAccessibleInterface *iface = self.qtInterface)
707 return iface->text(QAccessible::Description).toNSString();
708 qWarning() << "Called accessibilityLabel on invalid object: " << axid;
709 return nil;
710}
711
712- (void)setAccessibilityLabel:(NSString*)label{
713 if (QAccessibleInterface *iface = self.qtInterface)
714 iface->setText(QAccessible::Description, QString::fromNSString(label));
715}
716
717- (NSAccessibilityOrientation)accessibilityOrientation {
718 QAccessibleInterface *iface = self.qtInterface;
719 if (!iface)
720 return NSAccessibilityOrientationUnknown;
721
722 NSAccessibilityOrientation nsOrientation = NSAccessibilityOrientationUnknown;
723 if (QAccessibleAttributesInterface *attributesIface = iface->attributesInterface()) {
724 const QVariant orientationVariant =
725 attributesIface->attributeValue(QAccessible::Attribute::Orientation);
726 if (orientationVariant.isValid()) {
727 Q_ASSERT(orientationVariant.canConvert<Qt::Orientation>());
728 const Qt::Orientation orientation = orientationVariant.value<Qt::Orientation>();
729 nsOrientation = orientation == Qt::Horizontal ? NSAccessibilityOrientationHorizontal
730 : NSAccessibilityOrientationVertical;
731 }
732 }
733 return nsOrientation;
734}
735
736- (id) accessibilityValue {
737 if (QAccessibleInterface *iface = self.qtInterface) {
738 // VoiceOver asks for the value attribute for all elements. Return nil
739 // if we don't want the element to have a value attribute.
740 if (QCocoaAccessible::hasValueAttribute(iface))
741 return QCocoaAccessible::getValueAttribute(iface);
742 }
743 return nil;
744}
745
746
747- (id) accessibilityMinValue {
748 if (QAccessibleInterface *iface = self.qtInterface) {
749 if (iface->valueInterface()) {
750 return iface->valueInterface()->minimumValue().toString().toNSString();
751 }
752 }
753 return nil;
754}
755
756
757- (id) accessibilityMaxValue {
758 if (QAccessibleInterface *iface = self.qtInterface) {
759 if (iface->valueInterface()) {
760 return iface->valueInterface()->maximumValue().toString().toNSString();
761 }
762 }
763 return nil;
764}
765
766- (NSInteger) accessibilityNumberOfCharacters {
767 if (QAccessibleInterface *iface = self.qtInterface) {
768 if (QAccessibleTextInterface *text = iface->textInterface())
769 return text->characterCount();
770 }
771 return 0;
772}
773
774- (NSString *) accessibilitySelectedText {
775 if (QAccessibleInterface *iface = self.qtInterface) {
776 if (QAccessibleTextInterface *text = iface->textInterface()) {
777 int start = 0;
778 int end = 0;
779 text->selection(0, &start, &end);
780 return text->text(start, end).toNSString();
781 }
782 }
783 return nil;
784}
785
786- (NSRange) accessibilitySelectedTextRange {
787 QAccessibleInterface *iface = self.qtInterface;
788 if (!iface)
789 return NSRange();
790 if (QAccessibleTextInterface *text = iface->textInterface()) {
791 int start = 0;
792 int end = 0;
793 if (text->selectionCount() > 0) {
794 text->selection(0, &start, &end);
795 } else {
796 start = text->cursorPosition();
797 end = start;
798 }
799 return NSMakeRange(quint32(start), quint32(end - start));
800 }
801 return NSMakeRange(0, 0);
802}
803
804- (NSInteger)accessibilityLineForIndex:(NSInteger)index {
805 QAccessibleInterface *iface = self.qtInterface;
806 if (!iface)
807 return 0;
808 if (QAccessibleTextInterface *text = iface->textInterface()) {
809 QString textToPos = text->text(0, index);
810 return textToPos.count('\n');
811 }
812 return 0;
813}
814
815- (NSRange)accessibilityVisibleCharacterRange {
816 QAccessibleInterface *iface = self.qtInterface;
817 if (!iface)
818 return NSRange();
819 // FIXME This is not correct and may impact performance for big texts
820 if (QAccessibleTextInterface *text = iface->textInterface())
821 return NSMakeRange(0, static_cast<uint>(text->characterCount()));
822 return NSMakeRange(0, static_cast<uint>(iface->text(QAccessible::Name).length()));
823}
824
825- (NSInteger) accessibilityInsertionPointLineNumber {
826 QAccessibleInterface *iface = self.qtInterface;
827 if (!iface)
828 return 0;
829 if (QAccessibleTextInterface *text = iface->textInterface()) {
830 int position = text->cursorPosition();
831 return [self accessibilityLineForIndex:position];
832 }
833 return 0;
834}
835
836- (NSArray *)accessibilityAttributeNames {
837 NSMutableArray *attributes = [[NSMutableArray new] autorelease];
838
839#if QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(260000)
840 if (@available(macOS 26, *))
841 [attributes addObject:NSAccessibilityLanguageAttribute];
842#endif
843
844 QAccessibleInterface *iface = self.qtInterface;
845 if (iface && iface->state().expandable)
846 [attributes addObject:NSAccessibilityExpandedAttribute];
847
848 return attributes;
849}
850
851- (id)accessibilityAttributeValue:(NSString *)attribute {
852 QAccessibleInterface *iface = self.qtInterface;
853 if (!iface) {
854 qWarning() << "Called attribute on invalid object: " << axid;
855 return nil;
856 }
857
858#if QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(260000)
859 if (@available(macOS 26, *)) {
860 if ([attribute isEqualToString:NSAccessibilityLanguageAttribute]) {
861 QAccessibleAttributesInterface *attributesIface = iface->attributesInterface();
862 if (!attributesIface
863 || !attributesIface->attributeKeys().contains(QAccessible::Attribute::Locale))
864 return nil;
865
866 const auto &localeVariant =
867 attributesIface->attributeValue(QAccessible::Attribute::Locale);
868 return localeVariant.toLocale().bcp47Name().toNSString();
869 }
870 }
871#endif
872
873 if ([attribute isEqualToString:NSAccessibilityExpandedAttribute])
874 return iface->state().expanded ? @YES : @NO;
875
876 return nil;
877}
878
879- (NSArray *)accessibilityParameterizedAttributeNames {
880
881 QAccessibleInterface *iface = self.qtInterface;
882 if (!iface) {
883 qWarning() << "Called attribute on invalid object: " << axid;
884 return nil;
885 }
886
887 if (iface->textInterface()) {
888 return @[
889 NSAccessibilityStringForRangeParameterizedAttribute,
890 NSAccessibilityLineForIndexParameterizedAttribute,
891 NSAccessibilityRangeForLineParameterizedAttribute,
892 NSAccessibilityRangeForPositionParameterizedAttribute,
893// NSAccessibilityRangeForIndexParameterizedAttribute,
894 NSAccessibilityBoundsForRangeParameterizedAttribute,
895// NSAccessibilityRTFForRangeParameterizedAttribute,
896 NSAccessibilityStyleRangeForIndexParameterizedAttribute,
897 NSAccessibilityAttributedStringForRangeParameterizedAttribute
898 ];
899 }
900
901 return nil;
902}
903
904- (id)accessibilityAttributeValue:(NSString *)attribute forParameter:(id)parameter {
905 QAccessibleInterface *iface = self.qtInterface;
906 if (!iface) {
907 qWarning() << "Called attribute on invalid object: " << axid;
908 return nil;
909 }
910
911 if (!iface->textInterface())
912 return nil;
913
914 if ([attribute isEqualToString: NSAccessibilityStringForRangeParameterizedAttribute]) {
915 NSRange range = [parameter rangeValue];
916 QString text = iface->textInterface()->text(range.location, range.location + range.length);
917 return text.toNSString();
918 }
919 if ([attribute isEqualToString: NSAccessibilityLineForIndexParameterizedAttribute]) {
920 int index = [parameter intValue];
921 if (index < 0 || index > iface->textInterface()->characterCount())
922 return nil;
923 int line = 0; // true for all single line edits
924 if (iface->state().multiLine) {
925 line = -1;
926 convertLineOffset(iface->textInterface(), &line, &index);
927 }
928 return @(line);
929 }
930 if ([attribute isEqualToString: NSAccessibilityRangeForLineParameterizedAttribute]) {
931 int line = [parameter intValue];
932 if (line < 0)
933 return nil;
934 int lineOffset = -1;
935 NSUInteger startOffset = 0;
936 NSUInteger endOffset = 0;
937 convertLineOffset(iface->textInterface(), &line, &lineOffset, &startOffset, &endOffset);
938 return [NSValue valueWithRange:NSMakeRange(startOffset, endOffset - startOffset)];
939 }
940 if ([attribute isEqualToString: NSAccessibilityBoundsForRangeParameterizedAttribute]) {
941 NSRange range = [parameter rangeValue];
942 QRect firstRect = iface->textInterface()->characterRect(range.location);
943 QRectF rect;
944 if (range.length > 0) {
945 NSUInteger position = range.location + range.length - 1;
946 if (position > range.location && iface->textInterface()->text(position, position + 1) == "\n"_L1)
947 --position;
948 QRect lastRect = iface->textInterface()->characterRect(position);
949 rect = firstRect.united(lastRect);
950 } else {
951 rect = firstRect;
952 rect.setWidth(1);
953 }
954 return [NSValue valueWithRect:QCocoaScreen::mapToNative(rect)];
955 }
956 if ([attribute isEqualToString: NSAccessibilityAttributedStringForRangeParameterizedAttribute]) {
957 NSRange range = [parameter rangeValue];
958 QString text = iface->textInterface()->text(range.location, range.location + range.length);
959 return [[NSAttributedString alloc] initWithString:text.toNSString()];
960 } else if ([attribute isEqualToString: NSAccessibilityRangeForPositionParameterizedAttribute]) {
961 QPoint point = QCocoaScreen::mapFromNative([parameter pointValue]).toPoint();
962 int offset = iface->textInterface()->offsetAtPoint(point);
963 return [NSValue valueWithRange:NSMakeRange(static_cast<NSUInteger>(offset), 1)];
964 } else if ([attribute isEqualToString: NSAccessibilityStyleRangeForIndexParameterizedAttribute]) {
965 int start = 0;
966 int end = 0;
967 iface->textInterface()->attributes([parameter intValue], &start, &end);
968 return [NSValue valueWithRange:NSMakeRange(static_cast<NSUInteger>(start), static_cast<NSUInteger>(end - start))];
969 }
970 return nil;
971}
972
973- (BOOL)accessibilityIsAttributeSettable:(NSString *)attribute {
974 QAccessibleInterface *iface = self.qtInterface;
975 if (!iface)
976 return NO;
977
978 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
979 return iface->state().focusable ? YES : NO;
980 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
981 if (iface->textInterface() && iface->state().editable)
982 return YES;
983 if (iface->valueInterface())
984 return YES;
985 return NO;
986 } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
987 return iface->textInterface() ? YES : NO;
988 }
989 return NO;
990}
991
992- (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attribute {
993 QAccessibleInterface *iface = self.qtInterface;
994 if (!iface)
995 return;
996 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
997 if (QAccessibleActionInterface *action = iface->actionInterface())
998 action->doAction(QAccessibleActionInterface::setFocusAction());
999 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
1000 if (iface->textInterface()) {
1001 QString text = QString::fromNSString((NSString *)value);
1002 iface->setText(QAccessible::Value, text);
1003 } else if (QAccessibleValueInterface *valueIface = iface->valueInterface()) {
1004 double val = [value doubleValue];
1005 valueIface->setCurrentValue(val);
1006 }
1007 } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
1008 if (QAccessibleTextInterface *text = iface->textInterface()) {
1009 NSRange range = [value rangeValue];
1010 if (range.length > 0)
1011 text->setSelection(0, range.location, range.location + range.length);
1012 else
1013 text->setCursorPosition(range.location);
1014 }
1015 }
1016}
1017
1018// actions
1019
1020- (NSArray *)accessibilityActionNames {
1021 NSMutableArray *nsActions = [[NSMutableArray new] autorelease];
1022 QAccessibleInterface *iface = self.qtInterface;
1023 if (!iface)
1024 return nsActions;
1025
1026 const QStringList &supportedActionNames = QAccessibleBridgeUtils::effectiveActionNames(iface);
1027 for (const QString &qtAction : supportedActionNames) {
1028 NSString *nsAction = QCocoaAccessible::getTranslatedAction(qtAction);
1029 if (nsAction)
1030 [nsActions addObject : nsAction];
1031 }
1032
1033 return nsActions;
1034}
1035
1036- (NSString *)accessibilityActionDescription:(NSString *)action {
1037 QAccessibleInterface *iface = self.qtInterface;
1038 if (!iface)
1039 return nil; // FIXME is that the right return type??
1040 QString qtAction = QCocoaAccessible::translateAction(action, iface);
1041 QString description;
1042 // Return a description from the action interface if this action is not known to the OS.
1043 if (qtAction.isEmpty()) {
1044 if (QAccessibleActionInterface *actionInterface = iface->actionInterface()) {
1045 qtAction = QString::fromNSString((NSString *)action);
1046 description = actionInterface->localizedActionDescription(qtAction);
1047 }
1048 } else {
1049 description = qAccessibleLocalizedActionDescription(qtAction);
1050 }
1051 return description.toNSString();
1052}
1053
1054- (void)accessibilityPerformAction:(NSString *)action {
1055 if (QAccessibleInterface *iface = self.qtInterface) {
1056 const QString qtAction = QCocoaAccessible::translateAction(action, iface);
1057 QAccessibleBridgeUtils::performEffectiveAction(iface, qtAction);
1058 }
1059}
1060
1061// misc
1062
1063- (BOOL)accessibilityIsIgnored {
1064 // Short-cut for placeholders and synthesized elements. Working around a bug
1065 // that corrups lists returned by NSAccessibilityUnignoredChildren, otherwise
1066 // we could ignore rows and columns that are outside the table.
1067 if (self.isManagedByParent)
1068 return false;
1069
1070 if (QAccessibleInterface *iface = self.qtInterface)
1071 return QCocoaAccessible::shouldBeIgnored(iface);
1072 return true;
1073}
1074
1075- (id)accessibilityHitTest:(NSPoint)point {
1076 QAccessibleInterface *iface = self.qtInterface;
1077 if (!iface) {
1078// qDebug("Hit test: INVALID");
1079 return NSAccessibilityUnignoredAncestor(self);
1080 }
1081
1082 QPointF screenPoint = QCocoaScreen::mapFromNative(point);
1083 QAccessibleInterface *childInterface = iface->childAt(screenPoint.x(), screenPoint.y());
1084 // No child found, meaning we hit this element.
1085 if (!childInterface || !childInterface->isValid())
1086 return NSAccessibilityUnignoredAncestor(self);
1087
1088 // find the deepest child at the point
1089 QAccessibleInterface *childOfChildInterface = nullptr;
1090 do {
1091 childOfChildInterface = childInterface->childAt(screenPoint.x(), screenPoint.y());
1092 if (childOfChildInterface && childOfChildInterface->isValid())
1093 childInterface = childOfChildInterface;
1094 } while (childOfChildInterface && childOfChildInterface->isValid());
1095
1096 // hit a child, forward to child accessible interface.
1097 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface];
1098 if (accessibleElement)
1099 return NSAccessibilityUnignoredAncestor(accessibleElement);
1100 return NSAccessibilityUnignoredAncestor(self);
1101}
1102
1103- (id)accessibilityFocusedUIElement {
1104 QAccessibleInterface *iface = self.qtInterface;
1105 if (!iface) {
1106 qWarning("FocusedUIElement for INVALID");
1107 return nil;
1108 }
1109
1110 QAccessibleInterface *childInterface = iface->focusChild();
1111 if (childInterface && childInterface->isValid()) {
1112 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface];
1113 return NSAccessibilityUnignoredAncestor(accessibleElement);
1114 }
1115
1116 return NSAccessibilityUnignoredAncestor(self);
1117}
1118
1119- (NSString *) accessibilityHelp {
1120 if (QAccessibleInterface *iface = self.qtInterface) {
1121 const QString helpText = iface->text(QAccessible::Help);
1122 if (!helpText.isEmpty())
1123 return helpText.toNSString();
1124 }
1125 return nil;
1126}
1127
1128/*
1129 * Support for table
1130 */
1131- (NSInteger) accessibilityIndex {
1132 NSInteger index = 0;
1133 if (synthesizedRole == NSAccessibilityCellRole)
1134 return m_columnIndex;
1135 if (QAccessibleInterface *iface = self.qtInterface) {
1136 if (self.isManagedByParent) {
1137 // axid matches the parent table axid so that we can easily find the parent table
1138 // children of row are cell/any items
1139 if (iface->tableInterface()) {
1140 if (m_rowIndex >= 0)
1141 index = NSInteger(m_rowIndex);
1142 else if (m_columnIndex >= 0)
1143 index = NSInteger(m_columnIndex);
1144 }
1145 }
1146 }
1147 return index;
1148}
1149
1150- (NSArray *) accessibilityRows {
1151 if (!synthesizedRole && rows) {
1152 QAccessibleInterface *iface = self.qtInterface;
1153 QAccessibleTableInterface *tableInterface = iface ? iface->tableInterface() : nullptr;
1154 if (tableInterface) {
1155 const unsigned int rowCount = tableInterface->rowCount();
1156 if (rows.count != rowCount) {
1157 qCDebug(lcAccessibilityTable) << "Updating table rows with" << rowCount << "rows";
1158 if (rows) {
1159 [rows autorelease];
1160 rows = nil;
1161 }
1162 rows = [self populateTableArray:NSAccessibilityRowRole
1163 count:rowCount];
1164 [rows retain];
1165 }
1166 return NSAccessibilityUnignoredChildren(rows);
1167 }
1168 }
1169 return nil;
1170}
1171
1172- (NSArray *) accessibilityColumns {
1173 // we only implement this for a table, not for rows
1174 if (!synthesizedRole && columns) {
1175 QAccessibleInterface *iface = self.qtInterface;
1176 if (iface && iface->tableInterface())
1177 return NSAccessibilityUnignoredChildren(columns);
1178 }
1179 return nil;
1180}
1181
1182// tabs
1183
1184- (NSArray *) accessibilityTabs {
1185 QAccessibleInterface *iface = self.qtInterface;
1186 if (iface && iface->role() == QAccessible::PageTabList) {
1187 return QCocoaAccessible::unignoredChildren(iface, [](QAccessibleInterface *child){
1188 return QCocoaAccessible::defaultUnignored(child)
1189 && child->role() == QAccessible::PageTab;
1190 });
1191 }
1192 return nil;
1193}
1194
1195@end
1196
1197#endif // QT_CONFIG(accessibility)