// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include #include "qcocoaaccessibilityelement.h" #include "qcocoaaccessibility.h" #include "qcocoahelpers.h" #include "qcocoawindow.h" #include "qcocoascreen.h" #include #include #include #include QT_USE_NAMESPACE Q_LOGGING_CATEGORY(lcAccessibilityTable, "qt.accessibility.table") using namespace Qt::Literals::StringLiterals; #if QT_CONFIG(accessibility) /** * Converts between absolute character offsets and line numbers of a * QAccessibleTextInterface. Works in exactly one of two modes: * * - Pass *line == -1 in order to get a line containing character at the given * *offset * - Pass *offset == -1 in order to get the offset of first character of the * given *line * * You can optionally also pass non-NULL `start` and `end`, which will in both * modes be filled with the offset of the first and last characters of the * relevant line. */ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *offset, NSUInteger *start = 0, NSUInteger *end = 0) { Q_ASSERT(*line == -1 || *offset == -1); Q_ASSERT(*line != -1 || *offset != -1); Q_ASSERT(*offset <= text->characterCount()); int curLine = -1; int curStart = 0, curEnd = 0; do { curStart = curEnd; text->textAtOffset(curStart, QAccessible::LineBoundary, &curStart, &curEnd); // If the text is empty then we just return if (curStart == -1 || curEnd == -1) { if (start) *start = 0; if (end) *end = 0; return; } ++curLine; { // check for a case where a single word longer than the text edit's width and gets wrapped // in the middle of the word; in this case curEnd will be an offset belonging to the next line // and therefore nextEnd will not be equal to curEnd int nextStart; int nextEnd; text->textAtOffset(curEnd, QAccessible::LineBoundary, &nextStart, &nextEnd); if (nextEnd == curEnd) ++curEnd; } } while ((*line == -1 || curLine < *line) && (*offset == -1 || (curEnd <= *offset)) && curEnd <= text->characterCount()); curEnd = qMin(curEnd, text->characterCount()); if (*line == -1) *line = curLine; if (*offset == -1) *offset = curStart; Q_ASSERT(curStart >= 0); Q_ASSERT(curEnd >= 0); if (start) *start = curStart; if (end) *end = curEnd; } @implementation QMacAccessibilityElement { QAccessible::Id axid; int m_rowIndex; int m_columnIndex; // used by NSAccessibilityTable NSMutableArray *rows; // corresponds to accessibilityRows NSMutableArray *columns; // corresponds to accessibilityColumns // If synthesizedRole is set, this means that this objects does not have a corresponding // QAccessibleInterface, but it is synthesized by the cocoa plugin in order to meet the // NSAccessibility requirements. // The ownership is controlled by the parent object identified with the axid member variable. // (Therefore, if this member is set, this objects axid member is the same as the parents axid // member) NSString *synthesizedRole; } - (instancetype)initWithId:(QAccessible::Id)anId { return [self initWithId:anId role:nil]; } - (instancetype)initWithId:(QAccessible::Id)anId role:(NSAccessibilityRole)role { Q_ASSERT((int)anId < 0); self = [super init]; if (self) { axid = anId; m_rowIndex = -1; m_columnIndex = -1; rows = nil; columns = nil; synthesizedRole = role; // table: if this is not created as an element managed by the table, then // it's either the table itself, or an element created for an already existing // cell interface (or an element that's not at all related to a table). if (!synthesizedRole) { if (QAccessibleInterface *iface = QAccessible::accessibleInterface(axid)) { if (iface->tableInterface()) { [self updateTableModel]; } else if (const auto *cell = iface->tableCellInterface()) { // If we create an element for a table cell, initialize it with row/column // and insert it into the corresponding row's columns array. m_rowIndex = cell->rowIndex(); m_columnIndex = cell->columnIndex(); QAccessibleInterface *table = cell->table(); Q_ASSERT(table); QAccessibleTableInterface *tableInterface = table->tableInterface(); if (tableInterface) { auto *tableElement = [QMacAccessibilityElement elementWithInterface:table]; Q_ASSERT(tableElement); if (!tableElement->rows || int(tableElement->rows.count) <= m_rowIndex || int(tableElement->rows.count) != tableInterface->rowCount()) { qCWarning(lcAccessibilityTable) << "Cell requested for row" << m_rowIndex << "is out of" << "bounds for table with" << (tableElement->rows ? tableElement->rows.count : tableInterface->rowCount()) << "rows! Resizing table model."; [tableElement updateTableModel]; } Q_ASSERT(tableElement->rows); Q_ASSERT(int(tableElement->rows.count) > m_rowIndex); auto *rowElement = tableElement->rows[m_rowIndex]; if (!rowElement->columns || int(rowElement->columns.count) != tableInterface->columnCount()) { if (rowElement->columns) { qCWarning(lcAccessibilityTable) << "Table representation column count is out of sync:" << rowElement->columns.count << "!=" << tableInterface->columnCount(); } rowElement->columns = [rowElement populateTableRow:rowElement->columns count:tableInterface->columnCount()]; } qCDebug(lcAccessibilityTable) << "Creating cell representation for" << m_rowIndex << m_columnIndex << "in table with" << tableElement->rows.count << "rows and" << rowElement->columns.count << "columns"; rowElement->columns[m_columnIndex] = self; } } } } } return self; } + (instancetype)elementWithId:(QAccessible::Id)anId { Q_ASSERT(anId); if (!anId) return nil; QAccessibleCache *cache = QAccessibleCache::instance(); QMacAccessibilityElement *element = cache->elementForId(anId); if (!element) { Q_ASSERT(QAccessible::accessibleInterface(anId)); element = [[self alloc] initWithId:anId]; cache->insertElement(anId, element); } return element; } + (instancetype)elementWithInterface:(QAccessibleInterface *)iface { Q_ASSERT(iface); if (!iface) return nil; const QAccessible::Id anId = QAccessible::uniqueId(iface); return [self elementWithId:anId]; } - (void)invalidate { axid = 0; rows = nil; columns = nil; synthesizedRole = nil; NSAccessibilityPostNotification(self, NSAccessibilityUIElementDestroyedNotification); [self release]; } - (void)dealloc { if (rows) [rows release]; // will also release all entries first if (columns) [columns release]; // will also release all entries first [super dealloc]; } - (BOOL)isEqual:(id)object { if ([object isKindOfClass:[QMacAccessibilityElement class]]) { QMacAccessibilityElement *other = object; return other->axid == axid && other->synthesizedRole == synthesizedRole; } else { return NO; } } - (NSUInteger)hash { return axid; } - (BOOL)isManagedByParent { return synthesizedRole != nil; } - (NSMutableArray *)populateTableArray:(NSMutableArray *)array role:(NSAccessibilityRole)role count:(int)count { if (QAccessibleInterface *iface = self.qtInterface) { if (array && int(array.count) != count) { [array release]; array = nil; } if (!array) { array = [NSMutableArray arrayWithCapacity:count]; [array retain]; } else { [array removeAllObjects]; } Q_ASSERT(array); for (int n = 0; n < count; ++n) { // columns will have same axid as table (but not inserted in cache) QMacAccessibilityElement *element = [[QMacAccessibilityElement alloc] initWithId:axid role:role]; if (element) { if (role == NSAccessibilityRowRole) element->m_rowIndex = n; else if (role == NSAccessibilityColumnRole) element->m_columnIndex = n; [array addObject:element]; [element release]; } else { qWarning("QCocoaAccessibility: invalid child"); } } return array; } return nil; } - (NSMutableArray *)populateTableRow:(NSMutableArray *)array count:(int)count { Q_ASSERT(synthesizedRole == NSAccessibilityRowRole); if (array && int(array.count) != count) { [array release]; array = nil; } if (!array) { array = [NSMutableArray arrayWithCapacity:count]; [array retain]; // When macOS asks for the children of a row, then we populate the row's column // array with synthetic elements as place holders. This way, we don't have to // create QAccessibleInterfaces for every cell before they are really needed. // We don't add those synthetic elements into the cache, and we give them the // same axid as the table. This way, we can get easily to the table, and from // there to the QAccessibleInterface for the cell, when we have to eventually // associate such an interface with the element (at which point it is no longer // a placeholder). for (int n = 0; n < count; ++n) { // columns will have same axid as table (but not inserted in cache) QMacAccessibilityElement *cell = [[QMacAccessibilityElement alloc] initWithId:axid role:NSAccessibilityCellRole]; if (cell) { cell->m_rowIndex = m_rowIndex; cell->m_columnIndex = n; [array addObject:cell]; } } } Q_ASSERT(array); return array; } - (void)updateTableModel { if (QAccessibleInterface *iface = self.qtInterface) { if (QAccessibleTableInterface *table = iface->tableInterface()) { Q_ASSERT(!self.isManagedByParent); qCDebug(lcAccessibilityTable) << "Updating table representation with" << table->rowCount() << table->columnCount(); rows = [self populateTableArray:rows role:NSAccessibilityRowRole count:table->rowCount()]; columns = [self populateTableArray:columns role:NSAccessibilityColumnRole count:table->columnCount()]; } } } - (QAccessibleInterface *)qtInterface { QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); if (!iface || !iface->isValid()) return nullptr; // If this is a placeholder element for a table cell, associate it with the // cell interface (which will be created now, if needed). The current axid is // for the table to which the cell belongs, so iface is pointing at the table. if (synthesizedRole == NSAccessibilityCellRole) { // get the cell interface - there must be a valid one QAccessibleTableInterface *table = iface->tableInterface(); Q_ASSERT(table); QAccessibleInterface *cell = table->cellAt(m_rowIndex, m_columnIndex); if (!cell) return nullptr; Q_ASSERT(cell->isValid()); iface = cell; // no longer a placeholder axid = QAccessible::uniqueId(cell); synthesizedRole = nil; QAccessibleCache *cache = QAccessibleCache::instance(); if (QMacAccessibilityElement *cellElement = cache->elementForId(axid)) { // there already is another, non-placeholder element in the cache Q_ASSERT(cellElement->synthesizedRole == nil); // we have to release it if it's not us if (cellElement != self) { // for the same cell position Q_ASSERT(cellElement->m_rowIndex == m_rowIndex && cellElement->m_columnIndex == m_columnIndex); [cellElement release]; } } cache->insertElement(axid, self); } return iface; } // // accessibility protocol // - (BOOL)isAccessibilityFocused { // Just check if the app thinks we're focused. id focusedElement = NSApp.accessibilityApplicationFocusedUIElement; return [focusedElement isEqual:self]; } // attributes + (id) lineNumberForIndex: (int)index forText:(const QString &)text { auto textBefore = QStringView(text).left(index); qsizetype newlines = textBefore.count(u'\n'); return @(newlines); } - (BOOL) accessibilityNotifiesWhenDestroyed { return YES; } - (NSString *) accessibilityRole { // shortcut for cells, rows, and columns in a table if (synthesizedRole) return synthesizedRole; if (QAccessibleInterface *iface = self.qtInterface) return QCocoaAccessible::macRole(iface); return NSAccessibilityUnknownRole; } - (NSString *) accessibilitySubRole { if (QAccessibleInterface *iface = self.qtInterface) return QCocoaAccessible::macSubrole(iface); return NSAccessibilityUnknownRole; } - (NSString *) accessibilityRoleDescription { if (QAccessibleInterface *iface = self.qtInterface) return NSAccessibilityRoleDescription(self.accessibilityRole, self.accessibilitySubRole); return NSAccessibilityUnknownRole; } - (NSArray *) accessibilityChildren { // shortcut for cells if (synthesizedRole == NSAccessibilityCellRole) return nil; QAccessibleInterface *iface = self.qtInterface; if (!iface) return nil; if (QAccessibleTableInterface *table = iface->tableInterface()) { // either a table or table rows/columns if (!synthesizedRole) { // This is the table element, parent of all rows and columns /* * Typical 2x2 table hierarchy as can be observed in a table found under * Apple -> System Settings -> General -> Login Items (macOS 13) * * (AXTable) * | Columns: NSArray* (2 items) * | Rows: NSArray* (2 items) * | Visible Columns: NSArray* (2 items) * | Visible Rows: NSArray* (2 items) * | Children: NSArray* (5 items) +----<--| Header: (AXGroup) | * +-- (AXRow) | * | +-- (AXText) | * | +-- (AXTextField) | * +-- (AXRow) | * | +-- (AXText) | * | +-- (AXTextField) | * +-- (AXColumn) | * | Header: "Item" (sort button) | * | Index: 0 | * | Rows: NSArray* (2 items) | * | Visible Rows: NSArray* (2 items) | * +-- (AXColumn) | * | Header: "Kind" (sort button) | * | Index: 1 | * | Rows: NSArray* (2 items) | * | Visible Rows: NSArray* (2 items) +----> +-- (AXGroup) * +-- (AXButton/AXSortButton) Item [NSAccessibilityTableHeaderCellProxy] * +-- (AXButton/AXSortButton) Kind [NSAccessibilityTableHeaderCellProxy] */ NSArray *rs = [self accessibilityRows]; NSArray *cs = [self accessibilityColumns]; const int rCount = int([rs count]); const int cCount = int([cs count]); int childCount = rCount + cCount; NSMutableArray *tableChildren = [NSMutableArray arrayWithCapacity:childCount]; for (int i = 0; i < rCount; ++i) { [tableChildren addObject:[rs objectAtIndex:i]]; } for (int i = 0; i < cCount; ++i) { [tableChildren addObject:[cs objectAtIndex:i]]; } return NSAccessibilityUnignoredChildren(tableChildren); } else if (synthesizedRole == NSAccessibilityColumnRole) { return nil; } else if (synthesizedRole == NSAccessibilityRowRole) { // axid matches the parent table axid so that we can easily find the parent table // children of row are cell/any items Q_ASSERT(m_rowIndex >= 0); const int numColumns = table->columnCount(); columns = [self populateTableRow:columns count:numColumns]; return NSAccessibilityUnignoredChildren(columns); } } return QCocoaAccessible::unignoredChildren(iface); } - (NSArray *) accessibilitySelectedChildren { QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); if (!iface || !iface->isValid()) return nil; QAccessibleSelectionInterface *selection = iface->selectionInterface(); if (!selection) return nil; const QList selectedList = selection->selectedItems(); const qsizetype numSelected = selectedList.size(); NSMutableArray *selectedChildren = [NSMutableArray arrayWithCapacity:numSelected]; for (QAccessibleInterface *selectedChild : selectedList) { if (selectedChild && selectedChild->isValid()) { QAccessible::Id id = QAccessible::uniqueId(selectedChild); QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId:id]; if (element) [selectedChildren addObject:element]; } } return NSAccessibilityUnignoredChildren(selectedChildren); } - (id) accessibilityWindow { // We're in the same window as our parent. return [self.accessibilityParent accessibilityWindow]; } - (id) accessibilityTopLevelUIElementAttribute { // We're in the same top level element as our parent. return [self.accessibilityParent accessibilityTopLevelUIElementAttribute]; } - (NSString *) accessibilityTitle { if (QAccessibleInterface *iface = self.qtInterface) { if (iface->role() == QAccessible::StaticText) return nil; if (self.isManagedByParent) return nil; return iface->text(QAccessible::Name).toNSString(); } return nil; } - (BOOL) isAccessibilityEnabled { if (QAccessibleInterface *iface = self.qtInterface) return !iface->state().disabled; return false; } - (id)accessibilityParent { if (synthesizedRole == NSAccessibilityCellRole) { // a synthetic cell without interface - shortcut to the row QMacAccessibilityElement *tableElement = [QMacAccessibilityElement elementWithId:axid]; Q_ASSERT(tableElement && tableElement->rows); Q_ASSERT(int(tableElement->rows.count) > m_rowIndex); QMacAccessibilityElement *rowElement = tableElement->rows[m_rowIndex]; return rowElement; } QAccessibleInterface *iface = self.qtInterface; if (!iface) return nil; if (self.isManagedByParent) { // axid is the same for the parent element return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithId:axid]); } // macOS expects that the hierarchy is: // App -> Window -> Children // We don't actually have the window reflected properly in QAccessibility. // Check if the parent is the application and then instead return the native window. if (QAccessibleInterface *parent = iface->parent()) { if (parent->tableInterface()) { QMacAccessibilityElement *tableElement = [QMacAccessibilityElement elementWithInterface:parent]; // parent of cell should be row int rowIndex = -1; if (m_rowIndex >= 0 && m_columnIndex >= 0) rowIndex = m_rowIndex; else if (QAccessibleTableCellInterface *cell = iface->tableCellInterface()) rowIndex = cell->rowIndex(); Q_ASSERT(tableElement->rows); if (rowIndex > int([tableElement->rows count]) || rowIndex == -1) return nil; QMacAccessibilityElement *rowElement = tableElement->rows[rowIndex]; return NSAccessibilityUnignoredAncestor(rowElement); } if (parent->role() != QAccessible::Application) return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithInterface: parent]); } if (QWindow *window = iface->window()) { QPlatformWindow *platformWindow = window->handle(); if (platformWindow) { QCocoaWindow *win = static_cast(platformWindow); return NSAccessibilityUnignoredAncestor(qnsview_cast(win->view())); } } return nil; } - (NSRect)accessibilityFrame { QAccessibleInterface *iface = self.qtInterface; if (!iface) return NSZeroRect; QRect rect; if (self.isManagedByParent) { if (QAccessibleTableInterface *table = iface->tableInterface()) { // Construct the geometry of the Row/Column by looking at the individual table cells // ### Assumes that cells logical coordinates have spatial ordering (e.g finds the // rows width by taking the union between the leftmost item and the rightmost item in // a row). // Otherwise, we have to iterate over *all* cells in a row/columns to // find out the Row/Column geometry const bool isRow = synthesizedRole == NSAccessibilityRowRole; QPoint cellPos; int &row = isRow ? cellPos.ry() : cellPos.rx(); int &col = isRow ? cellPos.rx() : cellPos.ry(); NSUInteger trackIndex = self.accessibilityIndex; if (trackIndex != NSNotFound) { row = int(trackIndex); if (QAccessibleInterface *firstCell = table->cellAt(cellPos.y(), cellPos.x())) { rect = firstCell->rect(); col = isRow ? table->columnCount() : table->rowCount(); if (col > 1) { --col; if (QAccessibleInterface *lastCell = table->cellAt(cellPos.y(), cellPos.x())) rect = rect.united(lastCell->rect()); } } } } } else { rect = iface->rect(); } return QCocoaScreen::mapToNative(rect); } - (NSString*)accessibilityLabel { if (QAccessibleInterface *iface = self.qtInterface) return iface->text(QAccessible::Description).toNSString(); qWarning() << "Called accessibilityLabel on invalid object: " << axid; return nil; } - (void)setAccessibilityLabel:(NSString*)label{ if (QAccessibleInterface *iface = self.qtInterface) iface->setText(QAccessible::Description, QString::fromNSString(label)); } - (id) accessibilityValue { if (QAccessibleInterface *iface = self.qtInterface) { // VoiceOver asks for the value attribute for all elements. Return nil // if we don't want the element to have a value attribute. if (QCocoaAccessible::hasValueAttribute(iface)) return QCocoaAccessible::getValueAttribute(iface); } return nil; } - (NSInteger) accessibilityNumberOfCharacters { if (QAccessibleInterface *iface = self.qtInterface) { if (QAccessibleTextInterface *text = iface->textInterface()) return text->characterCount(); } return 0; } - (NSString *) accessibilitySelectedText { if (QAccessibleInterface *iface = self.qtInterface) { if (QAccessibleTextInterface *text = iface->textInterface()) { int start = 0; int end = 0; text->selection(0, &start, &end); return text->text(start, end).toNSString(); } } return nil; } - (NSRange) accessibilitySelectedTextRange { QAccessibleInterface *iface = self.qtInterface; if (!iface) return NSRange(); if (QAccessibleTextInterface *text = iface->textInterface()) { int start = 0; int end = 0; if (text->selectionCount() > 0) { text->selection(0, &start, &end); } else { start = text->cursorPosition(); end = start; } return NSMakeRange(quint32(start), quint32(end - start)); } return NSMakeRange(0, 0); } - (NSInteger)accessibilityLineForIndex:(NSInteger)index { QAccessibleInterface *iface = self.qtInterface; if (!iface) return 0; if (QAccessibleTextInterface *text = iface->textInterface()) { QString textToPos = text->text(0, index); return textToPos.count('\n'); } return 0; } - (NSRange)accessibilityVisibleCharacterRange { QAccessibleInterface *iface = self.qtInterface; if (!iface) return NSRange(); // FIXME This is not correct and may impact performance for big texts if (QAccessibleTextInterface *text = iface->textInterface()) return NSMakeRange(0, static_cast(text->characterCount())); return NSMakeRange(0, static_cast(iface->text(QAccessible::Name).length())); } - (NSInteger) accessibilityInsertionPointLineNumber { QAccessibleInterface *iface = self.qtInterface; if (!iface) return 0; if (QAccessibleTextInterface *text = iface->textInterface()) { int position = text->cursorPosition(); return [self accessibilityLineForIndex:position]; } return 0; } - (NSArray *)accessibilityParameterizedAttributeNames { QAccessibleInterface *iface = self.qtInterface; if (!iface) { qWarning() << "Called attribute on invalid object: " << axid; return nil; } if (iface->textInterface()) { return @[ NSAccessibilityStringForRangeParameterizedAttribute, NSAccessibilityLineForIndexParameterizedAttribute, NSAccessibilityRangeForLineParameterizedAttribute, NSAccessibilityRangeForPositionParameterizedAttribute, // NSAccessibilityRangeForIndexParameterizedAttribute, NSAccessibilityBoundsForRangeParameterizedAttribute, // NSAccessibilityRTFForRangeParameterizedAttribute, NSAccessibilityStyleRangeForIndexParameterizedAttribute, NSAccessibilityAttributedStringForRangeParameterizedAttribute ]; } return nil; } - (id)accessibilityAttributeValue:(NSString *)attribute forParameter:(id)parameter { QAccessibleInterface *iface = self.qtInterface; if (!iface) { qWarning() << "Called attribute on invalid object: " << axid; return nil; } if (!iface->textInterface()) return nil; if ([attribute isEqualToString: NSAccessibilityStringForRangeParameterizedAttribute]) { NSRange range = [parameter rangeValue]; QString text = iface->textInterface()->text(range.location, range.location + range.length); return text.toNSString(); } if ([attribute isEqualToString: NSAccessibilityLineForIndexParameterizedAttribute]) { int index = [parameter intValue]; if (index < 0 || index > iface->textInterface()->characterCount()) return nil; int line = 0; // true for all single line edits if (iface->state().multiLine) { line = -1; convertLineOffset(iface->textInterface(), &line, &index); } return @(line); } if ([attribute isEqualToString: NSAccessibilityRangeForLineParameterizedAttribute]) { int line = [parameter intValue]; if (line < 0) return nil; int lineOffset = -1; NSUInteger startOffset = 0; NSUInteger endOffset = 0; convertLineOffset(iface->textInterface(), &line, &lineOffset, &startOffset, &endOffset); return [NSValue valueWithRange:NSMakeRange(startOffset, endOffset - startOffset)]; } if ([attribute isEqualToString: NSAccessibilityBoundsForRangeParameterizedAttribute]) { NSRange range = [parameter rangeValue]; QRect firstRect = iface->textInterface()->characterRect(range.location); QRectF rect; if (range.length > 0) { NSUInteger position = range.location + range.length - 1; if (position > range.location && iface->textInterface()->text(position, position + 1) == "\n"_L1) --position; QRect lastRect = iface->textInterface()->characterRect(position); rect = firstRect.united(lastRect); } else { rect = firstRect; rect.setWidth(1); } return [NSValue valueWithRect:QCocoaScreen::mapToNative(rect)]; } if ([attribute isEqualToString: NSAccessibilityAttributedStringForRangeParameterizedAttribute]) { NSRange range = [parameter rangeValue]; QString text = iface->textInterface()->text(range.location, range.location + range.length); return [[NSAttributedString alloc] initWithString:text.toNSString()]; } else if ([attribute isEqualToString: NSAccessibilityRangeForPositionParameterizedAttribute]) { QPoint point = QCocoaScreen::mapFromNative([parameter pointValue]).toPoint(); int offset = iface->textInterface()->offsetAtPoint(point); return [NSValue valueWithRange:NSMakeRange(static_cast(offset), 1)]; } else if ([attribute isEqualToString: NSAccessibilityStyleRangeForIndexParameterizedAttribute]) { int start = 0; int end = 0; iface->textInterface()->attributes([parameter intValue], &start, &end); return [NSValue valueWithRange:NSMakeRange(static_cast(start), static_cast(end - start))]; } return nil; } - (BOOL)accessibilityIsAttributeSettable:(NSString *)attribute { QAccessibleInterface *iface = self.qtInterface; if (!iface) return NO; if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { return iface->state().focusable ? YES : NO; } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { if (iface->textInterface() && iface->state().editable) return YES; if (iface->valueInterface()) return YES; return NO; } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) { return iface->textInterface() ? YES : NO; } return NO; } - (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attribute { QAccessibleInterface *iface = self.qtInterface; if (!iface) return; if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { if (QAccessibleActionInterface *action = iface->actionInterface()) action->doAction(QAccessibleActionInterface::setFocusAction()); } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { if (iface->textInterface()) { QString text = QString::fromNSString((NSString *)value); iface->setText(QAccessible::Value, text); } else if (QAccessibleValueInterface *valueIface = iface->valueInterface()) { double val = [value doubleValue]; valueIface->setCurrentValue(val); } } else if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) { if (QAccessibleTextInterface *text = iface->textInterface()) { NSRange range = [value rangeValue]; if (range.length > 0) text->setSelection(0, range.location, range.location + range.length); else text->setCursorPosition(range.location); } } } // actions - (NSArray *)accessibilityActionNames { NSMutableArray *nsActions = [[NSMutableArray new] autorelease]; QAccessibleInterface *iface = self.qtInterface; if (!iface) return nsActions; const QStringList &supportedActionNames = QAccessibleBridgeUtils::effectiveActionNames(iface); for (const QString &qtAction : supportedActionNames) { NSString *nsAction = QCocoaAccessible::getTranslatedAction(qtAction); if (nsAction) [nsActions addObject : nsAction]; } return nsActions; } - (NSString *)accessibilityActionDescription:(NSString *)action { QAccessibleInterface *iface = self.qtInterface; if (!iface) return nil; // FIXME is that the right return type?? QString qtAction = QCocoaAccessible::translateAction(action, iface); QString description; // Return a description from the action interface if this action is not known to the OS. if (qtAction.isEmpty()) { if (QAccessibleActionInterface *actionInterface = iface->actionInterface()) { qtAction = QString::fromNSString((NSString *)action); description = actionInterface->localizedActionDescription(qtAction); } } else { description = qAccessibleLocalizedActionDescription(qtAction); } return description.toNSString(); } - (void)accessibilityPerformAction:(NSString *)action { if (QAccessibleInterface *iface = self.qtInterface) { const QString qtAction = QCocoaAccessible::translateAction(action, iface); QAccessibleBridgeUtils::performEffectiveAction(iface, qtAction); } } // misc - (BOOL)accessibilityIsIgnored { // Short-cut for placeholders and synthesized elements. Working around a bug // that corrups lists returned by NSAccessibilityUnignoredChildren, otherwise // we could ignore rows and columns that are outside the table. if (self.isManagedByParent) return false; if (QAccessibleInterface *iface = self.qtInterface) return QCocoaAccessible::shouldBeIgnored(iface); return true; } - (id)accessibilityHitTest:(NSPoint)point { QAccessibleInterface *iface = self.qtInterface; if (!iface) { // qDebug("Hit test: INVALID"); return NSAccessibilityUnignoredAncestor(self); } QPointF screenPoint = QCocoaScreen::mapFromNative(point); QAccessibleInterface *childInterface = iface->childAt(screenPoint.x(), screenPoint.y()); // No child found, meaning we hit this element. if (!childInterface || !childInterface->isValid()) return NSAccessibilityUnignoredAncestor(self); // find the deepest child at the point QAccessibleInterface *childOfChildInterface = nullptr; do { childOfChildInterface = childInterface->childAt(screenPoint.x(), screenPoint.y()); if (childOfChildInterface && childOfChildInterface->isValid()) childInterface = childOfChildInterface; } while (childOfChildInterface && childOfChildInterface->isValid()); // hit a child, forward to child accessible interface. QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface]; if (accessibleElement) return NSAccessibilityUnignoredAncestor(accessibleElement); return NSAccessibilityUnignoredAncestor(self); } - (id)accessibilityFocusedUIElement { QAccessibleInterface *iface = self.qtInterface; if (!iface) { qWarning("FocusedUIElement for INVALID"); return nil; } QAccessibleInterface *childInterface = iface->focusChild(); if (childInterface && childInterface->isValid()) { QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface]; return NSAccessibilityUnignoredAncestor(accessibleElement); } return NSAccessibilityUnignoredAncestor(self); } - (NSString *) accessibilityHelp { if (QAccessibleInterface *iface = self.qtInterface) { const QString helpText = iface->text(QAccessible::Help); if (!helpText.isEmpty()) return helpText.toNSString(); } return nil; } /* * Support for table */ - (NSInteger) accessibilityIndex { NSInteger index = 0; if (synthesizedRole == NSAccessibilityCellRole) return m_columnIndex; if (QAccessibleInterface *iface = self.qtInterface) { if (self.isManagedByParent) { // axid matches the parent table axid so that we can easily find the parent table // children of row are cell/any items if (QAccessibleTableInterface *table = iface->tableInterface()) { if (m_rowIndex >= 0) index = NSInteger(m_rowIndex); else if (m_columnIndex >= 0) index = NSInteger(m_columnIndex); } } } return index; } - (NSArray *) accessibilityRows { if (!synthesizedRole && rows) { QAccessibleInterface *iface = self.qtInterface; if (iface && iface->tableInterface()) return NSAccessibilityUnignoredChildren(rows); } return nil; } - (NSArray *) accessibilityColumns { if (!synthesizedRole && columns) { QAccessibleInterface *iface = self.qtInterface; if (iface && iface->tableInterface()) return NSAccessibilityUnignoredChildren(columns); } return nil; } @end #endif // QT_CONFIG(accessibility)