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