123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 | // 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 <AppKit/AppKit.h> #include "qcocoaaccessibility.h" #include "qcocoaaccessibilityelement.h" #include <QtGui/qaccessible.h> #include <QtCore/qmap.h> #include <private/qcore_mac_p.h> QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; #if QT_CONFIG(accessibility) QCocoaAccessibility::QCocoaAccessibility() { } QCocoaAccessibility::~QCocoaAccessibility() { } void QCocoaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) { if (!isActive() || !event->accessibleInterface() || !event->accessibleInterface()->isValid()) return; QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId: event->uniqueId()]; if (!element) { qWarning("QCocoaAccessibility::notifyAccessibilityUpdate: invalid element"); return; } switch (event->type()) { case QAccessible::Announcement: { auto *announcementEvent = static_cast<QAccessibleAnnouncementEvent *>(event); auto priorityLevel = (announcementEvent->politeness() == QAccessible::AnnouncementPoliteness::Assertive) ? NSAccessibilityPriorityHigh : NSAccessibilityPriorityMedium; NSDictionary *announcementInfo = @{ NSAccessibilityPriorityKey: [NSNumber numberWithInt:priorityLevel], NSAccessibilityAnnouncementKey: announcementEvent->message().toNSString() }; // post event for application element, as the comment for // NSAccessibilityAnnouncementRequestedNotification in the // NSAccessibilityConstants.h header says NSAccessibilityPostNotificationWithUserInfo(NSApp, NSAccessibilityAnnouncementRequestedNotification, announcementInfo); break; } case QAccessible::Focus: { NSAccessibilityPostNotification(element, NSAccessibilityFocusedUIElementChangedNotification); break; } case QAccessible::PopupMenuStart: NSAccessibilityPostNotification(element, NSAccessibilityFocusedUIElementChangedNotification); break; case QAccessible::StateChanged: case QAccessible::ValueChanged: case QAccessible::TextInserted: case QAccessible::TextRemoved: case QAccessible::TextUpdated: NSAccessibilityPostNotification(element, NSAccessibilityValueChangedNotification); break; case QAccessible::TextCaretMoved: case QAccessible::TextSelectionChanged: NSAccessibilityPostNotification(element, NSAccessibilitySelectedTextChangedNotification); break; case QAccessible::NameChanged: NSAccessibilityPostNotification(element, NSAccessibilityTitleChangedNotification); break; case QAccessible::TableModelChanged: // ### Could NSAccessibilityRowCountChangedNotification be relevant here? [element updateTableModel]; break; default: break; } } void QCocoaAccessibility::setRootObject(QObject *o) { Q_UNUSED(o); } void QCocoaAccessibility::initialize() { } void QCocoaAccessibility::cleanup() { } namespace QCocoaAccessible { typedef QMap<QAccessible::Role, NSString *> QMacAccessibiltyRoleMap; Q_GLOBAL_STATIC(QMacAccessibiltyRoleMap, qMacAccessibiltyRoleMap); static void populateRoleMap() { QMacAccessibiltyRoleMap &roleMap = *qMacAccessibiltyRoleMap(); roleMap[QAccessible::MenuItem] = NSAccessibilityMenuItemRole; roleMap[QAccessible::MenuBar] = NSAccessibilityMenuBarRole; roleMap[QAccessible::ScrollBar] = NSAccessibilityScrollBarRole; roleMap[QAccessible::Grip] = NSAccessibilityGrowAreaRole; roleMap[QAccessible::Window] = NSAccessibilityWindowRole; roleMap[QAccessible::Dialog] = NSAccessibilityWindowRole; roleMap[QAccessible::AlertMessage] = NSAccessibilityWindowRole; roleMap[QAccessible::ToolTip] = NSAccessibilityWindowRole; roleMap[QAccessible::HelpBalloon] = NSAccessibilityWindowRole; roleMap[QAccessible::PopupMenu] = NSAccessibilityMenuRole; roleMap[QAccessible::Application] = NSAccessibilityApplicationRole; roleMap[QAccessible::Pane] = NSAccessibilityGroupRole; roleMap[QAccessible::Grouping] = NSAccessibilityGroupRole; roleMap[QAccessible::Separator] = NSAccessibilitySplitterRole; roleMap[QAccessible::ToolBar] = NSAccessibilityToolbarRole; roleMap[QAccessible::PageTab] = NSAccessibilityRadioButtonRole; roleMap[QAccessible::PageTabList] = NSAccessibilityTabGroupRole; roleMap[QAccessible::ButtonMenu] = NSAccessibilityMenuButtonRole; roleMap[QAccessible::ButtonDropDown] = NSAccessibilityPopUpButtonRole; roleMap[QAccessible::SpinBox] = NSAccessibilityIncrementorRole; roleMap[QAccessible::Slider] = NSAccessibilitySliderRole; roleMap[QAccessible::ProgressBar] = NSAccessibilityProgressIndicatorRole; roleMap[QAccessible::ComboBox] = NSAccessibilityComboBoxRole; roleMap[QAccessible::RadioButton] = NSAccessibilityRadioButtonRole; roleMap[QAccessible::CheckBox] = NSAccessibilityCheckBoxRole; roleMap[QAccessible::StaticText] = NSAccessibilityStaticTextRole; roleMap[QAccessible::Table] = NSAccessibilityTableRole; roleMap[QAccessible::StatusBar] = NSAccessibilityStaticTextRole; roleMap[QAccessible::Column] = NSAccessibilityColumnRole; roleMap[QAccessible::ColumnHeader] = NSAccessibilityColumnRole; roleMap[QAccessible::Row] = NSAccessibilityRowRole; roleMap[QAccessible::RowHeader] = NSAccessibilityRowRole; roleMap[QAccessible::Button] = NSAccessibilityButtonRole; roleMap[QAccessible::EditableText] = NSAccessibilityTextFieldRole; roleMap[QAccessible::Link] = NSAccessibilityLinkRole; roleMap[QAccessible::Indicator] = NSAccessibilityValueIndicatorRole; roleMap[QAccessible::Splitter] = NSAccessibilitySplitGroupRole; roleMap[QAccessible::List] = NSAccessibilityListRole; roleMap[QAccessible::ListItem] = NSAccessibilityStaticTextRole; roleMap[QAccessible::Cell] = NSAccessibilityCellRole; roleMap[QAccessible::Client] = NSAccessibilityGroupRole; roleMap[QAccessible::Paragraph] = NSAccessibilityGroupRole; roleMap[QAccessible::Section] = NSAccessibilityGroupRole; roleMap[QAccessible::WebDocument] = NSAccessibilityGroupRole; roleMap[QAccessible::ColorChooser] = NSAccessibilityColorWellRole; roleMap[QAccessible::Footer] = NSAccessibilityGroupRole; roleMap[QAccessible::Form] = NSAccessibilityGroupRole; roleMap[QAccessible::Heading] = @"AXHeading"; roleMap[QAccessible::Note] = NSAccessibilityGroupRole; roleMap[QAccessible::ComplementaryContent] = NSAccessibilityGroupRole; roleMap[QAccessible::Graphic] = NSAccessibilityImageRole; roleMap[QAccessible::Tree] = NSAccessibilityOutlineRole; roleMap[QAccessible::BlockQuote] = NSAccessibilityGroupRole; } /* Returns a Cocoa accessibility role for the given interface, or NSAccessibilityUnknownRole if no role mapping is found. */ NSString *macRole(QAccessibleInterface *interface) { QAccessible::Role qtRole = interface->role(); QMacAccessibiltyRoleMap &roleMap = *qMacAccessibiltyRoleMap(); if (roleMap.isEmpty()) populateRoleMap(); // MAC_ACCESSIBILTY_DEBUG() << "role for" << interface.object() << "interface role" << Qt::hex << qtRole; if (roleMap.contains(qtRole)) { // MAC_ACCESSIBILTY_DEBUG() << "return" << roleMap[qtRole]; if (roleMap[qtRole] == NSAccessibilityComboBoxRole && !interface->state().editable) return NSAccessibilityMenuButtonRole; if (roleMap[qtRole] == NSAccessibilityTextFieldRole && interface->state().multiLine) return NSAccessibilityTextAreaRole; return roleMap[qtRole]; } // Treat unknown Qt roles as generic group container items. Returning // NSAccessibilityUnknownRole is also possible but makes the screen // reader focus on the item instead of passing focus to child items. // MAC_ACCESSIBILTY_DEBUG() << "return NSAccessibilityGroupRole for unknown Qt role"; return NSAccessibilityGroupRole; } /* Returns a Cocoa sub role for the given interface. */ NSString *macSubrole(QAccessibleInterface *interface) { QAccessible::State s = interface->state(); if (s.searchEdit) return NSAccessibilitySearchFieldSubrole; if (s.passwordEdit) return NSAccessibilitySecureTextFieldSubrole; if (interface->role() == QAccessible::PageTab) return NSAccessibilityTabButtonSubrole; return nil; } /* Cocoa accessibility supports ignoring elements, which means that the elements are still present in the accessibility tree but is not used by the screen reader. */ bool shouldBeIgnored(QAccessibleInterface *interface) { // Cocoa accessibility does not have an attribute that corresponds to the Invisible/Offscreen // state. Ignore interfaces with those flags set. const QAccessible::State state = interface->state(); if (state.invisible || state.offscreen || state.invalid) return true; // Some roles are not interesting. In particular, container roles should be // ignored in order to flatten the accessibility tree as seen by the user. switch (interface->role()) { case QAccessible::Border: // QFrame case QAccessible::Application: // We use the system-provided application element. case QAccessible::ToolBar: // Access the tool buttons directly. case QAccessible::Pane: // Scroll areas. case QAccessible::Client: // The default for QWidget. case QAccessible::PopupMenu: // Access the menu items directly return true; default: break; } NSString *mac_role = macRole(interface); if (mac_role == NSAccessibilityWindowRole || // We use the system-provided window elements. mac_role == NSAccessibilityUnknownRole) { return true; } if (const QObject *object = interface->object()) { const QByteArrayView className = object->metaObject()->className(); // VoiceOver focusing on tool tips can be confusing. The contents of the // tool tip is available through the description attribute anyway, so // we disable accessibility for tool tips. if (className == "QTipLabel"_ba) return true; } return false; } bool defaultUnignored(QAccessibleInterface *child) { if (child && child->isValid()) { const auto state = child->state(); return !state.invalid && !state.invisible; } return false; } NSArray<QMacAccessibilityElement *> *unignoredChildren(QAccessibleInterface *interface, const std::function<bool(QAccessibleInterface *child)> &pred) { int numKids = interface->childCount(); NSMutableArray<QMacAccessibilityElement *> *kids = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:numKids]; for (int i = 0; i < numKids; ++i) { QAccessibleInterface *child = interface->child(i); if (!pred(child)) continue; QAccessible::Id childId = QAccessible::uniqueId(child); QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId: childId]; if (element) [kids addObject: element]; else qWarning("QCocoaAccessibility: invalid child"); } return NSAccessibilityUnignoredChildren(kids); } /* Translates a predefined QAccessibleActionInterface action to a Mac action constant. Returns 0 if the Qt Action has no mac equivalent. Ownership of the NSString is not transferred. */ NSString *getTranslatedAction(const QString &qtAction) { if (qtAction == QAccessibleActionInterface::pressAction()) return NSAccessibilityPressAction; else if (qtAction == QAccessibleActionInterface::increaseAction()) return NSAccessibilityIncrementAction; else if (qtAction == QAccessibleActionInterface::decreaseAction()) return NSAccessibilityDecrementAction; else if (qtAction == QAccessibleActionInterface::showMenuAction()) return NSAccessibilityShowMenuAction; else if (qtAction == QAccessibleActionInterface::setFocusAction()) // Not 100% sure on this one return NSAccessibilityRaiseAction; else if (qtAction == QAccessibleActionInterface::toggleAction()) return NSAccessibilityPressAction; // Not translated: // // Qt: // static const QString &checkAction(); // static const QString &uncheckAction(); // // Cocoa: // NSAccessibilityConfirmAction; // NSAccessibilityPickAction; // NSAccessibilityCancelAction; // NSAccessibilityDeleteAction; return nil; } /* Translates between a Mac action constant and a QAccessibleActionInterface action Returns an empty QString if there is no Qt predefined equivalent. */ QString translateAction(NSString *nsAction, QAccessibleInterface *interface) { if ([nsAction compare: NSAccessibilityPressAction] == NSOrderedSame) { if (interface->role() == QAccessible::CheckBox || interface->role() == QAccessible::RadioButton) return QAccessibleActionInterface::toggleAction(); return QAccessibleActionInterface::pressAction(); } else if ([nsAction compare: NSAccessibilityIncrementAction] == NSOrderedSame) return QAccessibleActionInterface::increaseAction(); else if ([nsAction compare: NSAccessibilityDecrementAction] == NSOrderedSame) return QAccessibleActionInterface::decreaseAction(); else if ([nsAction compare: NSAccessibilityShowMenuAction] == NSOrderedSame) return QAccessibleActionInterface::showMenuAction(); else if ([nsAction compare: NSAccessibilityRaiseAction] == NSOrderedSame) return QAccessibleActionInterface::setFocusAction(); // See getTranslatedAction for not matched translations. return QString(); } bool hasValueAttribute(QAccessibleInterface *interface) { Q_ASSERT(interface); const QAccessible::Role qtrole = interface->role(); if (qtrole == QAccessible::EditableText || qtrole == QAccessible::StaticText || interface->valueInterface() || interface->state().checkable) { return true; } return false; } id getValueAttribute(QAccessibleInterface *interface) { const QAccessible::Role qtrole = interface->role(); if (qtrole == QAccessible::StaticText) { return interface->text(QAccessible::Name).toNSString(); } if (qtrole == QAccessible::EditableText) { if (QAccessibleTextInterface *textInterface = interface->textInterface()) { int begin = 0; int end = textInterface->characterCount(); QString text; if (interface->state().passwordEdit) { // return round password replacement chars text = QString(end, QChar(0x2022)); } else { // VoiceOver will read out the entire text string at once when returning // text as a value. For large text edits the size of the returned string // needs to be limited and text range attributes need to be used instead. // NSTextEdit returns the first sentence as the value, Do the same here: // ### call to textAfterOffset hangs. Booo! //if (textInterface->characterCount() > 0) // textInterface->textAfterOffset(0, QAccessible2::SentenceBoundary, &begin, &end); text = textInterface->text(begin, end); } return text.toNSString(); } } if (QAccessibleValueInterface *valueInterface = interface->valueInterface()) { return valueInterface->currentValue().toString().toNSString(); } if (interface->state().checkable) { if (interface->state().checkStateMixed) return @(2); return interface->state().checked ? @(1) : @(0); } return nil; } } // namespace QCocoaAccessible #endif // QT_CONFIG(accessibility) QT_END_NAMESPACE
|