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