Revision control

Copy as Markdown

// 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
// Qt-Security score:significant reason:default
#include <AppKit/AppKit.h>
#include "qcocoaaccessibility.h"
#include "qcocoaaccessibilityelement.h"
#include "qcocoawindow.h"
#include <QtGui/qaccessible.h>
#include <QtGui/private/qaccessiblebridgeutils_p.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;
// AppKit has no rotary role, and its own circular NSSlider reports as a slider.
roleMap[QAccessible::Dial] = NSAccessibilitySliderRole;
roleMap[QAccessible::ProgressBar] = NSAccessibilityProgressIndicatorRole;
roleMap[QAccessible::ComboBox] = NSAccessibilityComboBoxRole;
roleMap[QAccessible::RadioButton] = NSAccessibilityRadioButtonRole;
roleMap[QAccessible::CheckBox] = NSAccessibilityCheckBoxRole;
roleMap[QAccessible::Switch] = 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;
roleMap[QAccessible::LayeredPane] = 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;
if (roleMap[qtRole] == NSAccessibilityButtonRole && interface->state().expandable)
return NSAccessibilityDisclosureTriangleRole;
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;
if (interface->role() == QAccessible::Switch)
return NSAccessibilitySwitchSubrole;
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;
}
/*
Returns whether AppKit reports \a interface on our behalf.
macOS expects the hierarchy App -> Window -> Children, and takes the first
two from NSApp and the NSWindow, so an element of ours for either of them
would double up on a node AppKit already provides.
*/
bool isRepresentedByAppKit(QAccessibleInterface *interface)
{
const QAccessible::Role role = interface->role();
return role == QAccessible::Application || role == QAccessible::Window;
}
/*
Returns the view backing \a window, with its accessible parent up to date.
A Qt view resolves its parent when asked, via [QNSView accessibilityParent],
but foreign views have no getter, so the parent has to be pushed onto it.
The parent we push is the unignored ancestor of the window container hosting
the window, which can change whenever anything above the container moves in
the hierarchy, or stops being ignored.
Rather than tracking those changes we resolve the parent here, on every path
by which a client reaches a foreign view. The parent is then correct as of
the last query that touched the view, and a query is the only thing that can
observe it.
Any stale parent relationships held by accessibility clients will either be
reflected as invalidated accessibility handles as a result of us emitting
layout changes, or be refreshed on the next walk of the a11y herarchy.
*/
NSView *accessibleViewFor(QWindow *window)
{
// Opt-out, in case this regresses some use-cases
const bool disable = qEnvironmentVariableIsSet("QT_MAC_A11Y_NO_SPLIT_ON_VIEW_BOUNDARY");
if (disable)
return nil;
auto *platformWindow = static_cast<QCocoaWindow *>(window->handle());
if (!platformWindow)
return nil;
if (platformWindow->isForeignWindow())
platformWindow->updateAccessibleParent();
return platformWindow->view();
}
NSArray<id> *unignoredChildren(QAccessibleInterface *interface,
const std::function<bool(QAccessibleInterface *child)> &pred)
{
QWindow *window = QAccessibleBridgeUtils::windowFor(interface);
int numKids = interface->childCount();
NSMutableArray<id> *kids = [NSMutableArray<id> arrayWithCapacity:numKids];
for (int i = 0; i < numKids; ++i) {
QAccessibleInterface *child = interface->child(i);
if (!pred(child))
continue;
// A child that lives in a window of its own is represented by that
// window's view, which then vends the a11y subtree.
QWindow *childWindow = QAccessibleBridgeUtils::windowFor(child, interface, window);
if (childWindow != window) {
if (auto *view = QCocoaAccessible::accessibleViewFor(childWindow)) {
[kids addObject:view];
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
|| interface->role() == QAccessible::Switch) {
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
|| macRole(interface) == NSAccessibilityDisclosureTriangleRole) {
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);
}
if (macRole(interface) == NSAccessibilityDisclosureTriangleRole) {
return interface->state().expanded ? @(1) : @(0);
}
return nil;
}
} // namespace QCocoaAccessible
#endif // QT_CONFIG(accessibility)
QT_END_NAMESPACE