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- (id) accessibilityValue {
718 if (QAccessibleInterface *iface = self.qtInterface) {
719 // VoiceOver asks for the value attribute for all elements. Return nil
720 // if we don't want the element to have a value attribute.
721 if (QCocoaAccessible::hasValueAttribute(iface))
722 return QCocoaAccessible::getValueAttribute(iface);
723 }
724 return nil;
725}
726
727
728- (id) accessibilityMinValue {
729 if (QAccessibleInterface *iface = self.qtInterface) {
730 if (iface->valueInterface()) {
731 return iface->valueInterface()->minimumValue().toString().toNSString();
732 }
733 }
734 return nil;
735}
736
737
738- (id) accessibilityMaxValue {
739 if (QAccessibleInterface *iface = self.qtInterface) {
740 if (iface->valueInterface()) {
741 return iface->valueInterface()->maximumValue().toString().toNSString();
742 }
743 }
744 return nil;
745}
746
747- (NSInteger) accessibilityNumberOfCharacters {
748 if (QAccessibleInterface *iface = self.qtInterface) {
749 if (QAccessibleTextInterface *text = iface->textInterface())
750 return text->characterCount();
751 }
752 return 0;
753}
754
755- (NSString *) accessibilitySelectedText {
756 if (QAccessibleInterface *iface = self.qtInterface) {
757 if (QAccessibleTextInterface *text = iface->textInterface()) {
758 int start = 0;
759 int end = 0;
760 text->selection(0, &start, &end);
761 return text->text(start, end).toNSString();
762 }
763 }
764 return nil;
765}
766
767- (NSRange) accessibilitySelectedTextRange {
768 QAccessibleInterface *iface = self.qtInterface;
769 if (!iface)
770 return NSRange();
771 if (QAccessibleTextInterface *text = iface->textInterface()) {
772 int start = 0;
773 int end = 0;
774 if (text->selectionCount() > 0) {
775 text->selection(0, &start, &end);
776 } else {
777 start = text->cursorPosition();
778 end = start;
779 }
780 return NSMakeRange(quint32(start), quint32(end - start));
781 }
782 return NSMakeRange(0, 0);
783}
784
785- (NSInteger)accessibilityLineForIndex:(NSInteger)index {
786 QAccessibleInterface *iface = self.qtInterface;
787 if (!iface)
788 return 0;
789 if (QAccessibleTextInterface *text = iface->textInterface()) {
790 QString textToPos = text->text(0, index);
791 return textToPos.count('\n');
792 }
793 return 0;
794}
795
796- (NSRange)accessibilityVisibleCharacterRange {
797 QAccessibleInterface *iface = self.qtInterface;
798 if (!iface)
799 return NSRange();
800 // FIXME This is not correct and may impact performance for big texts
801 if (QAccessibleTextInterface *text = iface->textInterface())
802 return NSMakeRange(0, static_cast<uint>(text->characterCount()));
803 return NSMakeRange(0, static_cast<uint>(iface->text(QAccessible::Name).length()));
804}
805
806- (NSInteger) accessibilityInsertionPointLineNumber {
807 QAccessibleInterface *iface = self.qtInterface;
808 if (!iface)
809 return 0;
810 if (QAccessibleTextInterface *text = iface->textInterface()) {
811 int position = text->cursorPosition();
812 return [self accessibilityLineForIndex:position];
813 }
814 return 0;
815}
816
817- (NSArray *)accessibilityParameterizedAttributeNames {
818
819 QAccessibleInterface *iface = self.qtInterface;
820 if (!iface) {
821 qWarning() << "Called attribute on invalid object: " << axid;
822 return nil;
823 }
824
825 if (iface->textInterface()) {
826 return @[
827 NSAccessibilityStringForRangeParameterizedAttribute,
828 NSAccessibilityLineForIndexParameterizedAttribute,
829 NSAccessibilityRangeForLineParameterizedAttribute,
830 NSAccessibilityRangeForPositionParameterizedAttribute,
831// NSAccessibilityRangeForIndexParameterizedAttribute,
832 NSAccessibilityBoundsForRangeParameterizedAttribute,
833// NSAccessibilityRTFForRangeParameterizedAttribute,
834 NSAccessibilityStyleRangeForIndexParameterizedAttribute,
835 NSAccessibilityAttributedStringForRangeParameterizedAttribute
836 ];
837 }
838
839 return nil;
840}
841
842- (id)accessibilityAttributeValue:(NSString *)attribute forParameter:(id)parameter {
843 QAccessibleInterface *iface = self.qtInterface;
844 if (!iface) {
845 qWarning() << "Called attribute on invalid object: " << axid;
846 return nil;
847 }
848
849 if (!iface->textInterface())
850 return nil;
851
852 if ([attribute isEqualToString: NSAccessibilityStringForRangeParameterizedAttribute]) {
853 NSRange range = [parameter rangeValue];
854 QString text = iface->textInterface()->text(range.location, range.location + range.length);
855 return text.toNSString();
856 }
857 if ([attribute isEqualToString: NSAccessibilityLineForIndexParameterizedAttribute]) {
858 int index = [parameter intValue];
859 if (index < 0 || index > iface->textInterface()->characterCount())
860 return nil;
861 int line = 0; // true for all single line edits
862 if (iface->state().multiLine) {
863 line = -1;
864 convertLineOffset(iface->textInterface(), &line, &index);
865 }
866 return @(line);
867 }
868 if ([attribute isEqualToString: NSAccessibilityRangeForLineParameterizedAttribute]) {
869 int line = [parameter intValue];
870 if (line < 0)
871 return nil;
872 int lineOffset = -1;
873 NSUInteger startOffset = 0;
874 NSUInteger endOffset = 0;
875 convertLineOffset(iface->textInterface(), &line, &lineOffset, &startOffset, &endOffset);
876 return [NSValue valueWithRange:NSMakeRange(startOffset, endOffset - startOffset)];
877 }
878 if ([attribute isEqualToString: NSAccessibilityBoundsForRangeParameterizedAttribute]) {
879 NSRange range = [parameter rangeValue];
880 QRect firstRect = iface->textInterface()->characterRect(range.location);
881 QRectF rect;
882 if (range.length > 0) {
883 NSUInteger position = range.location + range.length - 1;
884 if (position > range.location && iface->textInterface()->text(position, position + 1) == "\n"_L1)
885 --position;
886 QRect lastRect = iface->textInterface()->characterRect(position);
887 rect = firstRect.united(lastRect);
888 } else {
889 rect = firstRect;
890 rect.setWidth(1);
891 }
892 return [NSValue valueWithRect:QCocoaScreen::mapToNative(rect)];
893 }
894 if ([attribute isEqualToString: NSAccessibilityAttributedStringForRangeParameterizedAttribute]) {
895 NSRange range = [parameter rangeValue];
896 QString text = iface->textInterface()->text(range.location, range.location + range.length);
897 return [[NSAttributedString alloc] initWithString:text.toNSString()];
898 } else if ([attribute isEqualToString: NSAccessibilityRangeForPositionParameterizedAttribute]) {
899 QPoint point = QCocoaScreen::mapFromNative([parameter pointValue]).toPoint();
900 int offset = iface->textInterface()->offsetAtPoint(point);
901 return [NSValue valueWithRange:NSMakeRange(static_cast<NSUInteger>(offset), 1)];
902 } else if ([attribute isEqualToString: NSAccessibilityStyleRangeForIndexParameterizedAttribute]) {
903 int start = 0;
904 int end = 0;
905 iface->textInterface()->attributes([parameter intValue], &start, &end);
906 return [NSValue valueWithRange:NSMakeRange(static_cast<NSUInteger>(start), static_cast<NSUInteger>(end - start))];
907 }
908 return nil;
909}
910
911- (BOOL)accessibilityIsAttributeSettable:(NSString *)attribute {
912 QAccessibleInterface *iface = self.qtInterface;
913 if (!iface)
914 return NO;
915
916 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
917 return iface->state().focusable ? YES : NO;
918 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
919 if (iface->textInterface() && iface->state().editable)
920 return YES;
921 if (iface->valueInterface())
922 return YES;
923 return NO;
924 } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
925 return iface->textInterface() ? YES : NO;
926 }
927 return NO;
928}
929
930- (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attribute {
931 QAccessibleInterface *iface = self.qtInterface;
932 if (!iface)
933 return;
934 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
935 if (QAccessibleActionInterface *action = iface->actionInterface())
936 action->doAction(QAccessibleActionInterface::setFocusAction());
937 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
938 if (iface->textInterface()) {
939 QString text = QString::fromNSString((NSString *)value);
940 iface->setText(QAccessible::Value, text);
941 } else if (QAccessibleValueInterface *valueIface = iface->valueInterface()) {
942 double val = [value doubleValue];
943 valueIface->setCurrentValue(val);
944 }
945 } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
946 if (QAccessibleTextInterface *text = iface->textInterface()) {
947 NSRange range = [value rangeValue];
948 if (range.length > 0)
949 text->setSelection(0, range.location, range.location + range.length);
950 else
951 text->setCursorPosition(range.location);
952 }
953 }
954}
955
956// actions
957
958- (NSArray *)accessibilityActionNames {
959 NSMutableArray *nsActions = [[NSMutableArray new] autorelease];
960 QAccessibleInterface *iface = self.qtInterface;
961 if (!iface)
962 return nsActions;
963
964 const QStringList &supportedActionNames = QAccessibleBridgeUtils::effectiveActionNames(iface);
965 for (const QString &qtAction : supportedActionNames) {
966 NSString *nsAction = QCocoaAccessible::getTranslatedAction(qtAction);
967 if (nsAction)
968 [nsActions addObject : nsAction];
969 }
970
971 return nsActions;
972}
973
974- (NSString *)accessibilityActionDescription:(NSString *)action {
975 QAccessibleInterface *iface = self.qtInterface;
976 if (!iface)
977 return nil; // FIXME is that the right return type??
978 QString qtAction = QCocoaAccessible::translateAction(action, iface);
979 QString description;
980 // Return a description from the action interface if this action is not known to the OS.
981 if (qtAction.isEmpty()) {
982 if (QAccessibleActionInterface *actionInterface = iface->actionInterface()) {
983 qtAction = QString::fromNSString((NSString *)action);
984 description = actionInterface->localizedActionDescription(qtAction);
985 }
986 } else {
987 description = qAccessibleLocalizedActionDescription(qtAction);
988 }
989 return description.toNSString();
990}
991
992- (void)accessibilityPerformAction:(NSString *)action {
993 if (QAccessibleInterface *iface = self.qtInterface) {
994 const QString qtAction = QCocoaAccessible::translateAction(action, iface);
995 QAccessibleBridgeUtils::performEffectiveAction(iface, qtAction);
996 }
997}
998
999// misc
1000
1001- (BOOL)accessibilityIsIgnored {
1002 // Short-cut for placeholders and synthesized elements. Working around a bug
1003 // that corrups lists returned by NSAccessibilityUnignoredChildren, otherwise
1004 // we could ignore rows and columns that are outside the table.
1005 if (self.isManagedByParent)
1006 return false;
1007
1008 if (QAccessibleInterface *iface = self.qtInterface)
1009 return QCocoaAccessible::shouldBeIgnored(iface);
1010 return true;
1011}
1012
1013- (id)accessibilityHitTest:(NSPoint)point {
1014 QAccessibleInterface *iface = self.qtInterface;
1015 if (!iface) {
1016// qDebug("Hit test: INVALID");
1017 return NSAccessibilityUnignoredAncestor(self);
1018 }
1019
1020 QPointF screenPoint = QCocoaScreen::mapFromNative(point);
1021 QAccessibleInterface *childInterface = iface->childAt(screenPoint.x(), screenPoint.y());
1022 // No child found, meaning we hit this element.
1023 if (!childInterface || !childInterface->isValid())
1024 return NSAccessibilityUnignoredAncestor(self);
1025
1026 // find the deepest child at the point
1027 QAccessibleInterface *childOfChildInterface = nullptr;
1028 do {
1029 childOfChildInterface = childInterface->childAt(screenPoint.x(), screenPoint.y());
1030 if (childOfChildInterface && childOfChildInterface->isValid())
1031 childInterface = childOfChildInterface;
1032 } while (childOfChildInterface && childOfChildInterface->isValid());
1033
1034 // hit a child, forward to child accessible interface.
1035 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface];
1036 if (accessibleElement)
1037 return NSAccessibilityUnignoredAncestor(accessibleElement);
1038 return NSAccessibilityUnignoredAncestor(self);
1039}
1040
1041- (id)accessibilityFocusedUIElement {
1042 QAccessibleInterface *iface = self.qtInterface;
1043 if (!iface) {
1044 qWarning("FocusedUIElement for INVALID");
1045 return nil;
1046 }
1047
1048 QAccessibleInterface *childInterface = iface->focusChild();
1049 if (childInterface && childInterface->isValid()) {
1050 QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface];
1051 return NSAccessibilityUnignoredAncestor(accessibleElement);
1052 }
1053
1054 return NSAccessibilityUnignoredAncestor(self);
1055}
1056
1057- (NSString *) accessibilityHelp {
1058 if (QAccessibleInterface *iface = self.qtInterface) {
1059 const QString helpText = iface->text(QAccessible::Help);
1060 if (!helpText.isEmpty())
1061 return helpText.toNSString();
1062 }
1063 return nil;
1064}
1065
1066/*
1067 * Support for table
1068 */
1069- (NSInteger) accessibilityIndex {
1070 NSInteger index = 0;
1071 if (synthesizedRole == NSAccessibilityCellRole)
1072 return m_columnIndex;
1073 if (QAccessibleInterface *iface = self.qtInterface) {
1074 if (self.isManagedByParent) {
1075 // axid matches the parent table axid so that we can easily find the parent table
1076 // children of row are cell/any items
1077 if (iface->tableInterface()) {
1078 if (m_rowIndex >= 0)
1079 index = NSInteger(m_rowIndex);
1080 else if (m_columnIndex >= 0)
1081 index = NSInteger(m_columnIndex);
1082 }
1083 }
1084 }
1085 return index;
1086}
1087
1088- (NSArray *) accessibilityRows {
1089 if (!synthesizedRole && rows) {
1090 QAccessibleInterface *iface = self.qtInterface;
1091 QAccessibleTableInterface *tableInterface = iface ? iface->tableInterface() : nullptr;
1092 if (tableInterface) {
1093 const unsigned int rowCount = tableInterface->rowCount();
1094 if (rows.count != rowCount) {
1095 qCDebug(lcAccessibilityTable) << "Updating table rows with" << rowCount << "rows";
1096 if (rows) {
1097 [rows autorelease];
1098 rows = nil;
1099 }
1100 rows = [self populateTableArray:NSAccessibilityRowRole
1101 count:rowCount];
1102 [rows retain];
1103 }
1104 return NSAccessibilityUnignoredChildren(rows);
1105 }
1106 }
1107 return nil;
1108}
1109
1110- (NSArray *) accessibilityColumns {
1111 // we only implement this for a table, not for rows
1112 if (!synthesizedRole && columns) {
1113 QAccessibleInterface *iface = self.qtInterface;
1114 if (iface && iface->tableInterface())
1115 return NSAccessibilityUnignoredChildren(columns);
1116 }
1117 return nil;
1118}
1119
1120// tabs
1121
1122- (NSArray *) accessibilityTabs {
1123 QAccessibleInterface *iface = self.qtInterface;
1124 if (iface && iface->role() == QAccessible::PageTabList) {
1125 return QCocoaAccessible::unignoredChildren(iface, [](QAccessibleInterface *child){
1126 return QCocoaAccessible::defaultUnignored(child)
1127 && child->role() == QAccessible::PageTab;
1128 });
1129 }
1130 return nil;
1131}
1132
1133@end
1134
1135#endif // QT_CONFIG(accessibility)