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
qcocoaaccessibility.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
9#include "qcocoawindow.h"
10#include <QtGui/qaccessible.h>
11#include <QtGui/private/qaccessiblebridgeutils_p.h>
12#include <QtCore/qmap.h>
13#include <private/qcore_mac_p.h>
14
16
17using namespace Qt::StringLiterals;
18
19#if QT_CONFIG(accessibility)
20
21QCocoaAccessibility::QCocoaAccessibility()
22{
23
24}
25
26QCocoaAccessibility::~QCocoaAccessibility()
27{
28
29}
30
31void QCocoaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event)
32{
33 if (!isActive() || !event->accessibleInterface() || !event->accessibleInterface()->isValid())
34 return;
35 QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId: event->uniqueId()];
36 if (!element) {
37 qWarning("QCocoaAccessibility::notifyAccessibilityUpdate: invalid element");
38 return;
39 }
40
41 switch (event->type()) {
42 case QAccessible::Announcement: {
43 auto *announcementEvent = static_cast<QAccessibleAnnouncementEvent *>(event);
44 auto priorityLevel = (announcementEvent->politeness() == QAccessible::AnnouncementPoliteness::Assertive)
45 ? NSAccessibilityPriorityHigh
46 : NSAccessibilityPriorityMedium;
47 NSDictionary *announcementInfo = @{
48 NSAccessibilityPriorityKey: [NSNumber numberWithInt:priorityLevel],
49 NSAccessibilityAnnouncementKey: announcementEvent->message().toNSString()
50 };
51 // post event for application element, as the comment for
52 // NSAccessibilityAnnouncementRequestedNotification in the
53 // NSAccessibilityConstants.h header says
54 NSAccessibilityPostNotificationWithUserInfo(NSApp,
55 NSAccessibilityAnnouncementRequestedNotification,
56 announcementInfo);
57 break;
58 }
59 case QAccessible::Focus: {
60 NSAccessibilityPostNotification(element, NSAccessibilityFocusedUIElementChangedNotification);
61 break;
62 }
63 case QAccessible::PopupMenuStart:
64 NSAccessibilityPostNotification(element, NSAccessibilityFocusedUIElementChangedNotification);
65 break;
66 case QAccessible::StateChanged:
67 case QAccessible::ValueChanged:
68 case QAccessible::TextInserted:
69 case QAccessible::TextRemoved:
70 case QAccessible::TextUpdated:
71 NSAccessibilityPostNotification(element, NSAccessibilityValueChangedNotification);
72 break;
73 case QAccessible::TextCaretMoved:
74 case QAccessible::TextSelectionChanged:
75 NSAccessibilityPostNotification(element, NSAccessibilitySelectedTextChangedNotification);
76 break;
77 case QAccessible::NameChanged:
78 NSAccessibilityPostNotification(element, NSAccessibilityTitleChangedNotification);
79 break;
80 case QAccessible::TableModelChanged:
81 // ### Could NSAccessibilityRowCountChangedNotification be relevant here?
82 [element updateTableModel];
83 break;
84 default:
85 break;
86 }
87}
88
89void QCocoaAccessibility::setRootObject(QObject *o)
90{
91 Q_UNUSED(o);
92}
93
94void QCocoaAccessibility::initialize()
95{
96
97}
98
99void QCocoaAccessibility::cleanup()
100{
101
102}
103
104namespace QCocoaAccessible {
105
106typedef QMap<QAccessible::Role, NSString *> QMacAccessibiltyRoleMap;
107Q_GLOBAL_STATIC(QMacAccessibiltyRoleMap, qMacAccessibiltyRoleMap);
108
109static void populateRoleMap()
110{
111 QMacAccessibiltyRoleMap &roleMap = *qMacAccessibiltyRoleMap();
112 roleMap[QAccessible::MenuItem] = NSAccessibilityMenuItemRole;
113 roleMap[QAccessible::MenuBar] = NSAccessibilityMenuBarRole;
114 roleMap[QAccessible::ScrollBar] = NSAccessibilityScrollBarRole;
115 roleMap[QAccessible::Grip] = NSAccessibilityGrowAreaRole;
116 roleMap[QAccessible::Window] = NSAccessibilityWindowRole;
117 roleMap[QAccessible::Dialog] = NSAccessibilityWindowRole;
118 roleMap[QAccessible::AlertMessage] = NSAccessibilityWindowRole;
119 roleMap[QAccessible::ToolTip] = NSAccessibilityWindowRole;
120 roleMap[QAccessible::HelpBalloon] = NSAccessibilityWindowRole;
121 roleMap[QAccessible::PopupMenu] = NSAccessibilityMenuRole;
122 roleMap[QAccessible::Application] = NSAccessibilityApplicationRole;
123 roleMap[QAccessible::Pane] = NSAccessibilityGroupRole;
124 roleMap[QAccessible::Grouping] = NSAccessibilityGroupRole;
125 roleMap[QAccessible::Separator] = NSAccessibilitySplitterRole;
126 roleMap[QAccessible::ToolBar] = NSAccessibilityToolbarRole;
127 roleMap[QAccessible::PageTab] = NSAccessibilityRadioButtonRole;
128 roleMap[QAccessible::PageTabList] = NSAccessibilityTabGroupRole;
129 roleMap[QAccessible::ButtonMenu] = NSAccessibilityMenuButtonRole;
130 roleMap[QAccessible::ButtonDropDown] = NSAccessibilityPopUpButtonRole;
131 roleMap[QAccessible::SpinBox] = NSAccessibilityIncrementorRole;
132 roleMap[QAccessible::Slider] = NSAccessibilitySliderRole;
133 roleMap[QAccessible::ProgressBar] = NSAccessibilityProgressIndicatorRole;
134 roleMap[QAccessible::ComboBox] = NSAccessibilityComboBoxRole;
135 roleMap[QAccessible::RadioButton] = NSAccessibilityRadioButtonRole;
136 roleMap[QAccessible::CheckBox] = NSAccessibilityCheckBoxRole;
137 roleMap[QAccessible::Switch] = NSAccessibilityCheckBoxRole;
138 roleMap[QAccessible::StaticText] = NSAccessibilityStaticTextRole;
139 roleMap[QAccessible::Table] = NSAccessibilityTableRole;
140 roleMap[QAccessible::StatusBar] = NSAccessibilityStaticTextRole;
141 roleMap[QAccessible::Column] = NSAccessibilityColumnRole;
142 roleMap[QAccessible::ColumnHeader] = NSAccessibilityColumnRole;
143 roleMap[QAccessible::Row] = NSAccessibilityRowRole;
144 roleMap[QAccessible::RowHeader] = NSAccessibilityRowRole;
145 roleMap[QAccessible::Button] = NSAccessibilityButtonRole;
146 roleMap[QAccessible::EditableText] = NSAccessibilityTextFieldRole;
147 roleMap[QAccessible::Link] = NSAccessibilityLinkRole;
148 roleMap[QAccessible::Indicator] = NSAccessibilityValueIndicatorRole;
149 roleMap[QAccessible::Splitter] = NSAccessibilitySplitGroupRole;
150 roleMap[QAccessible::List] = NSAccessibilityListRole;
151 roleMap[QAccessible::ListItem] = NSAccessibilityStaticTextRole;
152 roleMap[QAccessible::Cell] = NSAccessibilityCellRole;
153 roleMap[QAccessible::Client] = NSAccessibilityGroupRole;
154 roleMap[QAccessible::Paragraph] = NSAccessibilityGroupRole;
155 roleMap[QAccessible::Section] = NSAccessibilityGroupRole;
156 roleMap[QAccessible::WebDocument] = NSAccessibilityGroupRole;
157 roleMap[QAccessible::ColorChooser] = NSAccessibilityColorWellRole;
158 roleMap[QAccessible::Footer] = NSAccessibilityGroupRole;
159 roleMap[QAccessible::Form] = NSAccessibilityGroupRole;
160 roleMap[QAccessible::Heading] = @"AXHeading";
161 roleMap[QAccessible::Note] = NSAccessibilityGroupRole;
162 roleMap[QAccessible::ComplementaryContent] = NSAccessibilityGroupRole;
163 roleMap[QAccessible::Graphic] = NSAccessibilityImageRole;
164 roleMap[QAccessible::Tree] = NSAccessibilityOutlineRole;
165 roleMap[QAccessible::BlockQuote] = NSAccessibilityGroupRole;
166 roleMap[QAccessible::LayeredPane] = NSAccessibilityGroupRole;
167}
168
169/*
170 Returns a Cocoa accessibility role for the given interface, or
171 NSAccessibilityUnknownRole if no role mapping is found.
172*/
173NSString *macRole(QAccessibleInterface *interface)
174{
175 QAccessible::Role qtRole = interface->role();
176 QMacAccessibiltyRoleMap &roleMap = *qMacAccessibiltyRoleMap();
177
178 if (roleMap.isEmpty())
179 populateRoleMap();
180
181 // MAC_ACCESSIBILTY_DEBUG() << "role for" << interface.object() << "interface role" << Qt::hex << qtRole;
182
183 if (roleMap.contains(qtRole)) {
184 // MAC_ACCESSIBILTY_DEBUG() << "return" << roleMap[qtRole];
185 if (roleMap[qtRole] == NSAccessibilityComboBoxRole && !interface->state().editable)
186 return NSAccessibilityMenuButtonRole;
187 if (roleMap[qtRole] == NSAccessibilityTextFieldRole && interface->state().multiLine)
188 return NSAccessibilityTextAreaRole;
189 if (roleMap[qtRole] == NSAccessibilityButtonRole && interface->state().expandable)
190 return NSAccessibilityDisclosureTriangleRole;
191 return roleMap[qtRole];
192 }
193
194 // Treat unknown Qt roles as generic group container items. Returning
195 // NSAccessibilityUnknownRole is also possible but makes the screen
196 // reader focus on the item instead of passing focus to child items.
197 // MAC_ACCESSIBILTY_DEBUG() << "return NSAccessibilityGroupRole for unknown Qt role";
198 return NSAccessibilityGroupRole;
199}
200
201/*
202 Returns a Cocoa sub role for the given interface.
203*/
204NSString *macSubrole(QAccessibleInterface *interface)
205{
206 QAccessible::State s = interface->state();
207 if (s.searchEdit)
208 return NSAccessibilitySearchFieldSubrole;
209 if (s.passwordEdit)
210 return NSAccessibilitySecureTextFieldSubrole;
211 if (interface->role() == QAccessible::PageTab)
212 return NSAccessibilityTabButtonSubrole;
213 if (interface->role() == QAccessible::Switch)
214 return NSAccessibilitySwitchSubrole;
215 return nil;
216}
217
218/*
219 Cocoa accessibility supports ignoring elements, which means that
220 the elements are still present in the accessibility tree but is
221 not used by the screen reader.
222*/
223bool shouldBeIgnored(QAccessibleInterface *interface)
224{
225 // Cocoa accessibility does not have an attribute that corresponds to the Invisible/Offscreen
226 // state. Ignore interfaces with those flags set.
227 const QAccessible::State state = interface->state();
228 if (state.invisible || state.offscreen || state.invalid)
229 return true;
230
231 // Some roles are not interesting. In particular, container roles should be
232 // ignored in order to flatten the accessibility tree as seen by the user.
233 switch (interface->role()) {
234 case QAccessible::Border: // QFrame
235 case QAccessible::Application: // We use the system-provided application element.
236 case QAccessible::ToolBar: // Access the tool buttons directly.
237 case QAccessible::Pane: // Scroll areas.
238 case QAccessible::Client: // The default for QWidget.
239 case QAccessible::PopupMenu: // Access the menu items directly
240 return true;
241 default:
242 break;
243 }
244
245 NSString *mac_role = macRole(interface);
246 if (mac_role == NSAccessibilityWindowRole || // We use the system-provided window elements.
247 mac_role == NSAccessibilityUnknownRole) {
248 return true;
249 }
250
251 if (const QObject *object = interface->object()) {
252 const QByteArrayView className = object->metaObject()->className();
253
254 // VoiceOver focusing on tool tips can be confusing. The contents of the
255 // tool tip is available through the description attribute anyway, so
256 // we disable accessibility for tool tips.
257 if (className == "QTipLabel"_ba)
258 return true;
259 }
260
261 return false;
262}
263
264bool defaultUnignored(QAccessibleInterface *child)
265{
266 if (child && child->isValid()) {
267 const auto state = child->state();
268 return !state.invalid && !state.invisible;
269 }
270 return false;
271}
272
273/*
274 Returns whether AppKit reports \a interface on our behalf.
275
276 macOS expects the hierarchy App -> Window -> Children, and takes the first
277 two from NSApp and the NSWindow, so an element of ours for either of them
278 would double up on a node AppKit already provides.
279*/
280bool isRepresentedByAppKit(QAccessibleInterface *interface)
281{
282 const QAccessible::Role role = interface->role();
283 return role == QAccessible::Application || role == QAccessible::Window;
284}
285
286/*
287 Returns the view backing \a window, with its accessible parent up to date.
288
289 A Qt view resolves its parent when asked, via [QNSView accessibilityParent],
290 but foreign views have no getter, so the parent has to be pushed onto it.
291 The parent we push is the unignored ancestor of the window container hosting
292 the window, which can change whenever anything above the container moves in
293 the hierarchy, or stops being ignored.
294
295 Rather than tracking those changes we resolve the parent here, on every path
296 by which a client reaches a foreign view. The parent is then correct as of
297 the last query that touched the view, and a query is the only thing that can
298 observe it.
299
300 Any stale parent relationships held by accessibility clients will either be
301 reflected as invalidated accessibility handles as a result of us emitting
302 layout changes, or be refreshed on the next walk of the a11y herarchy.
303*/
304NSView *accessibleViewFor(QWindow *window)
305{
306 // Opt-out, in case this regresses some use-cases
307 const bool disable = qEnvironmentVariableIsSet("QT_MAC_A11Y_NO_SPLIT_ON_VIEW_BOUNDARY");
308 if (disable)
309 return nil;
310
311 auto *platformWindow = static_cast<QCocoaWindow *>(window->handle());
312 if (!platformWindow)
313 return nil;
314 if (platformWindow->isForeignWindow())
315 platformWindow->updateAccessibleParent();
316 return platformWindow->view();
317}
318
319NSArray<id> *unignoredChildren(QAccessibleInterface *interface,
320 const std::function<bool(QAccessibleInterface *child)> &pred)
321{
322 QWindow *window = QAccessibleBridgeUtils::windowFor(interface);
323
324 int numKids = interface->childCount();
325 NSMutableArray<id> *kids = [NSMutableArray<id> arrayWithCapacity:numKids];
326 for (int i = 0; i < numKids; ++i) {
327 QAccessibleInterface *child = interface->child(i);
328
329 if (!pred(child))
330 continue;
331
332 // A child that lives in a window of its own is represented by that
333 // window's view, which then vends the a11y subtree.
334 QWindow *childWindow = QAccessibleBridgeUtils::windowFor(child, interface, window);
335 if (childWindow != window) {
336 if (auto *view = QCocoaAccessible::accessibleViewFor(childWindow)) {
337 [kids addObject:view];
338 continue;
339 }
340 }
341
342 QAccessible::Id childId = QAccessible::uniqueId(child);
343
344 QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId: childId];
345 if (element)
346 [kids addObject: element];
347 else
348 qWarning("QCocoaAccessibility: invalid child");
349 }
350 return NSAccessibilityUnignoredChildren(kids);
351}
352
353/*
354 Translates a predefined QAccessibleActionInterface action to a Mac action constant.
355 Returns 0 if the Qt Action has no mac equivalent. Ownership of the NSString is
356 not transferred.
357*/
358NSString *getTranslatedAction(const QString &qtAction)
359{
360 if (qtAction == QAccessibleActionInterface::pressAction())
361 return NSAccessibilityPressAction;
362 else if (qtAction == QAccessibleActionInterface::increaseAction())
363 return NSAccessibilityIncrementAction;
364 else if (qtAction == QAccessibleActionInterface::decreaseAction())
365 return NSAccessibilityDecrementAction;
366 else if (qtAction == QAccessibleActionInterface::showMenuAction())
367 return NSAccessibilityShowMenuAction;
368 else if (qtAction == QAccessibleActionInterface::setFocusAction()) // Not 100% sure on this one
369 return NSAccessibilityRaiseAction;
370 else if (qtAction == QAccessibleActionInterface::toggleAction())
371 return NSAccessibilityPressAction;
372
373 // Not translated:
374 //
375 // Qt:
376 // static const QString &checkAction();
377 // static const QString &uncheckAction();
378 //
379 // Cocoa:
380 // NSAccessibilityConfirmAction;
381 // NSAccessibilityPickAction;
382 // NSAccessibilityCancelAction;
383 // NSAccessibilityDeleteAction;
384
385 return nil;
386}
387
388
389/*
390 Translates between a Mac action constant and a QAccessibleActionInterface action
391 Returns an empty QString if there is no Qt predefined equivalent.
392*/
393QString translateAction(NSString *nsAction, QAccessibleInterface *interface)
394{
395 if ([nsAction compare: NSAccessibilityPressAction] == NSOrderedSame) {
396 if (interface->role() == QAccessible::CheckBox
397 || interface->role() == QAccessible::RadioButton
398 || interface->role() == QAccessible::Switch) {
399 return QAccessibleActionInterface::toggleAction();
400 }
401 return QAccessibleActionInterface::pressAction();
402 } else if ([nsAction compare: NSAccessibilityIncrementAction] == NSOrderedSame)
403 return QAccessibleActionInterface::increaseAction();
404 else if ([nsAction compare: NSAccessibilityDecrementAction] == NSOrderedSame)
405 return QAccessibleActionInterface::decreaseAction();
406 else if ([nsAction compare: NSAccessibilityShowMenuAction] == NSOrderedSame)
407 return QAccessibleActionInterface::showMenuAction();
408 else if ([nsAction compare: NSAccessibilityRaiseAction] == NSOrderedSame)
409 return QAccessibleActionInterface::setFocusAction();
410
411 // See getTranslatedAction for not matched translations.
412
413 return QString();
414}
415
416bool hasValueAttribute(QAccessibleInterface *interface)
417{
418 Q_ASSERT(interface);
419 const QAccessible::Role qtrole = interface->role();
420 if (qtrole == QAccessible::EditableText
421 || qtrole == QAccessible::StaticText
422 || interface->valueInterface()
423 || interface->state().checkable
424 || macRole(interface) == NSAccessibilityDisclosureTriangleRole) {
425 return true;
426 }
427
428 return false;
429}
430
431id getValueAttribute(QAccessibleInterface *interface)
432{
433 const QAccessible::Role qtrole = interface->role();
434 if (qtrole == QAccessible::StaticText) {
435 return interface->text(QAccessible::Name).toNSString();
436 }
437 if (qtrole == QAccessible::EditableText) {
438 if (QAccessibleTextInterface *textInterface = interface->textInterface()) {
439
440 int begin = 0;
441 int end = textInterface->characterCount();
442 QString text;
443 if (interface->state().passwordEdit) {
444 // return round password replacement chars
445 text = QString(end, QChar(0x2022));
446 } else {
447 // VoiceOver will read out the entire text string at once when returning
448 // text as a value. For large text edits the size of the returned string
449 // needs to be limited and text range attributes need to be used instead.
450 // NSTextEdit returns the first sentence as the value, Do the same here:
451 // ### call to textAfterOffset hangs. Booo!
452 //if (textInterface->characterCount() > 0)
453 // textInterface->textAfterOffset(0, QAccessible2::SentenceBoundary, &begin, &end);
454 text = textInterface->text(begin, end);
455 }
456 return text.toNSString();
457 }
458 }
459
460 if (QAccessibleValueInterface *valueInterface = interface->valueInterface()) {
461 return valueInterface->currentValue().toString().toNSString();
462 }
463
464 if (interface->state().checkable) {
465 if (interface->state().checkStateMixed)
466 return @(2);
467 return interface->state().checked ? @(1) : @(0);
468 }
469
470 if (macRole(interface) == NSAccessibilityDisclosureTriangleRole) {
471 return interface->state().expanded ? @(1) : @(0);
472 }
473
474 return nil;
475}
476
477} // namespace QCocoaAccessible
478
479#endif // QT_CONFIG(accessibility)
480
481QT_END_NAMESPACE
Combined button and popup list for selecting options.