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 // AppKit has no rotary role, and its own circular NSSlider reports as a slider.
134 roleMap[QAccessible::Dial] = NSAccessibilitySliderRole;
135 roleMap[QAccessible::ProgressBar] = NSAccessibilityProgressIndicatorRole;
136 roleMap[QAccessible::ComboBox] = NSAccessibilityComboBoxRole;
137 roleMap[QAccessible::RadioButton] = NSAccessibilityRadioButtonRole;
138 roleMap[QAccessible::CheckBox] = NSAccessibilityCheckBoxRole;
139 roleMap[QAccessible::Switch] = NSAccessibilityCheckBoxRole;
140 roleMap[QAccessible::StaticText] = NSAccessibilityStaticTextRole;
141 roleMap[QAccessible::Table] = NSAccessibilityTableRole;
142 roleMap[QAccessible::StatusBar] = NSAccessibilityStaticTextRole;
143 roleMap[QAccessible::Column] = NSAccessibilityColumnRole;
144 roleMap[QAccessible::ColumnHeader] = NSAccessibilityColumnRole;
145 roleMap[QAccessible::Row] = NSAccessibilityRowRole;
146 roleMap[QAccessible::RowHeader] = NSAccessibilityRowRole;
147 roleMap[QAccessible::Button] = NSAccessibilityButtonRole;
148 roleMap[QAccessible::EditableText] = NSAccessibilityTextFieldRole;
149 roleMap[QAccessible::Link] = NSAccessibilityLinkRole;
150 roleMap[QAccessible::Indicator] = NSAccessibilityValueIndicatorRole;
151 roleMap[QAccessible::Splitter] = NSAccessibilitySplitGroupRole;
152 roleMap[QAccessible::List] = NSAccessibilityListRole;
153 roleMap[QAccessible::ListItem] = NSAccessibilityStaticTextRole;
154 roleMap[QAccessible::Cell] = NSAccessibilityCellRole;
155 roleMap[QAccessible::Client] = NSAccessibilityGroupRole;
156 roleMap[QAccessible::Paragraph] = NSAccessibilityGroupRole;
157 roleMap[QAccessible::Section] = NSAccessibilityGroupRole;
158 roleMap[QAccessible::WebDocument] = NSAccessibilityGroupRole;
159 roleMap[QAccessible::ColorChooser] = NSAccessibilityColorWellRole;
160 roleMap[QAccessible::Footer] = NSAccessibilityGroupRole;
161 roleMap[QAccessible::Form] = NSAccessibilityGroupRole;
162 roleMap[QAccessible::Heading] = @"AXHeading";
163 roleMap[QAccessible::Note] = NSAccessibilityGroupRole;
164 roleMap[QAccessible::ComplementaryContent] = NSAccessibilityGroupRole;
165 roleMap[QAccessible::Graphic] = NSAccessibilityImageRole;
166 roleMap[QAccessible::Tree] = NSAccessibilityOutlineRole;
167 roleMap[QAccessible::BlockQuote] = NSAccessibilityGroupRole;
168 roleMap[QAccessible::LayeredPane] = NSAccessibilityGroupRole;
169}
170
171/*
172 Returns a Cocoa accessibility role for the given interface, or
173 NSAccessibilityUnknownRole if no role mapping is found.
174*/
175NSString *macRole(QAccessibleInterface *interface)
176{
177 QAccessible::Role qtRole = interface->role();
178 QMacAccessibiltyRoleMap &roleMap = *qMacAccessibiltyRoleMap();
179
180 if (roleMap.isEmpty())
181 populateRoleMap();
182
183 // MAC_ACCESSIBILTY_DEBUG() << "role for" << interface.object() << "interface role" << Qt::hex << qtRole;
184
185 if (roleMap.contains(qtRole)) {
186 // MAC_ACCESSIBILTY_DEBUG() << "return" << roleMap[qtRole];
187 if (roleMap[qtRole] == NSAccessibilityComboBoxRole && !interface->state().editable)
188 return NSAccessibilityMenuButtonRole;
189 if (roleMap[qtRole] == NSAccessibilityTextFieldRole && interface->state().multiLine)
190 return NSAccessibilityTextAreaRole;
191 if (roleMap[qtRole] == NSAccessibilityButtonRole && interface->state().expandable)
192 return NSAccessibilityDisclosureTriangleRole;
193 return roleMap[qtRole];
194 }
195
196 // Treat unknown Qt roles as generic group container items. Returning
197 // NSAccessibilityUnknownRole is also possible but makes the screen
198 // reader focus on the item instead of passing focus to child items.
199 // MAC_ACCESSIBILTY_DEBUG() << "return NSAccessibilityGroupRole for unknown Qt role";
200 return NSAccessibilityGroupRole;
201}
202
203/*
204 Returns a Cocoa sub role for the given interface.
205*/
206NSString *macSubrole(QAccessibleInterface *interface)
207{
208 QAccessible::State s = interface->state();
209 if (s.searchEdit)
210 return NSAccessibilitySearchFieldSubrole;
211 if (s.passwordEdit)
212 return NSAccessibilitySecureTextFieldSubrole;
213 if (interface->role() == QAccessible::PageTab)
214 return NSAccessibilityTabButtonSubrole;
215 if (interface->role() == QAccessible::Switch)
216 return NSAccessibilitySwitchSubrole;
217 return nil;
218}
219
220/*
221 Cocoa accessibility supports ignoring elements, which means that
222 the elements are still present in the accessibility tree but is
223 not used by the screen reader.
224*/
225bool shouldBeIgnored(QAccessibleInterface *interface)
226{
227 // Cocoa accessibility does not have an attribute that corresponds to the Invisible/Offscreen
228 // state. Ignore interfaces with those flags set.
229 const QAccessible::State state = interface->state();
230 if (state.invisible || state.offscreen || state.invalid)
231 return true;
232
233 // Some roles are not interesting. In particular, container roles should be
234 // ignored in order to flatten the accessibility tree as seen by the user.
235 switch (interface->role()) {
236 case QAccessible::Border: // QFrame
237 case QAccessible::Application: // We use the system-provided application element.
238 case QAccessible::ToolBar: // Access the tool buttons directly.
239 case QAccessible::Pane: // Scroll areas.
240 case QAccessible::Client: // The default for QWidget.
241 case QAccessible::PopupMenu: // Access the menu items directly
242 return true;
243 default:
244 break;
245 }
246
247 NSString *mac_role = macRole(interface);
248 if (mac_role == NSAccessibilityWindowRole || // We use the system-provided window elements.
249 mac_role == NSAccessibilityUnknownRole) {
250 return true;
251 }
252
253 if (const QObject *object = interface->object()) {
254 const QByteArrayView className = object->metaObject()->className();
255
256 // VoiceOver focusing on tool tips can be confusing. The contents of the
257 // tool tip is available through the description attribute anyway, so
258 // we disable accessibility for tool tips.
259 if (className == "QTipLabel"_ba)
260 return true;
261 }
262
263 return false;
264}
265
266bool defaultUnignored(QAccessibleInterface *child)
267{
268 if (child && child->isValid()) {
269 const auto state = child->state();
270 return !state.invalid && !state.invisible;
271 }
272 return false;
273}
274
275/*
276 Returns whether AppKit reports \a interface on our behalf.
277
278 macOS expects the hierarchy App -> Window -> Children, and takes the first
279 two from NSApp and the NSWindow, so an element of ours for either of them
280 would double up on a node AppKit already provides.
281*/
282bool isRepresentedByAppKit(QAccessibleInterface *interface)
283{
284 const QAccessible::Role role = interface->role();
285 return role == QAccessible::Application || role == QAccessible::Window;
286}
287
288/*
289 Returns the view backing \a window, with its accessible parent up to date.
290
291 A Qt view resolves its parent when asked, via [QNSView accessibilityParent],
292 but foreign views have no getter, so the parent has to be pushed onto it.
293 The parent we push is the unignored ancestor of the window container hosting
294 the window, which can change whenever anything above the container moves in
295 the hierarchy, or stops being ignored.
296
297 Rather than tracking those changes we resolve the parent here, on every path
298 by which a client reaches a foreign view. The parent is then correct as of
299 the last query that touched the view, and a query is the only thing that can
300 observe it.
301
302 Any stale parent relationships held by accessibility clients will either be
303 reflected as invalidated accessibility handles as a result of us emitting
304 layout changes, or be refreshed on the next walk of the a11y herarchy.
305*/
306NSView *accessibleViewFor(QWindow *window)
307{
308 // Opt-out, in case this regresses some use-cases
309 const bool disable = qEnvironmentVariableIsSet("QT_MAC_A11Y_NO_SPLIT_ON_VIEW_BOUNDARY");
310 if (disable)
311 return nil;
312
313 auto *platformWindow = static_cast<QCocoaWindow *>(window->handle());
314 if (!platformWindow)
315 return nil;
316 if (platformWindow->isForeignWindow())
317 platformWindow->updateAccessibleParent();
318 return platformWindow->view();
319}
320
321NSArray<id> *unignoredChildren(QAccessibleInterface *interface,
322 const std::function<bool(QAccessibleInterface *child)> &pred)
323{
324 QWindow *window = QAccessibleBridgeUtils::windowFor(interface);
325
326 int numKids = interface->childCount();
327 NSMutableArray<id> *kids = [NSMutableArray<id> arrayWithCapacity:numKids];
328 for (int i = 0; i < numKids; ++i) {
329 QAccessibleInterface *child = interface->child(i);
330
331 if (!pred(child))
332 continue;
333
334 // A child that lives in a window of its own is represented by that
335 // window's view, which then vends the a11y subtree.
336 QWindow *childWindow = QAccessibleBridgeUtils::windowFor(child, interface, window);
337 if (childWindow != window) {
338 if (auto *view = QCocoaAccessible::accessibleViewFor(childWindow)) {
339 [kids addObject:view];
340 continue;
341 }
342 }
343
344 QAccessible::Id childId = QAccessible::uniqueId(child);
345
346 QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId: childId];
347 if (element)
348 [kids addObject: element];
349 else
350 qWarning("QCocoaAccessibility: invalid child");
351 }
352 return NSAccessibilityUnignoredChildren(kids);
353}
354
355/*
356 Translates a predefined QAccessibleActionInterface action to a Mac action constant.
357 Returns 0 if the Qt Action has no mac equivalent. Ownership of the NSString is
358 not transferred.
359*/
360NSString *getTranslatedAction(const QString &qtAction)
361{
362 if (qtAction == QAccessibleActionInterface::pressAction())
363 return NSAccessibilityPressAction;
364 else if (qtAction == QAccessibleActionInterface::increaseAction())
365 return NSAccessibilityIncrementAction;
366 else if (qtAction == QAccessibleActionInterface::decreaseAction())
367 return NSAccessibilityDecrementAction;
368 else if (qtAction == QAccessibleActionInterface::showMenuAction())
369 return NSAccessibilityShowMenuAction;
370 else if (qtAction == QAccessibleActionInterface::setFocusAction()) // Not 100% sure on this one
371 return NSAccessibilityRaiseAction;
372 else if (qtAction == QAccessibleActionInterface::toggleAction())
373 return NSAccessibilityPressAction;
374
375 // Not translated:
376 //
377 // Qt:
378 // static const QString &checkAction();
379 // static const QString &uncheckAction();
380 //
381 // Cocoa:
382 // NSAccessibilityConfirmAction;
383 // NSAccessibilityPickAction;
384 // NSAccessibilityCancelAction;
385 // NSAccessibilityDeleteAction;
386
387 return nil;
388}
389
390
391/*
392 Translates between a Mac action constant and a QAccessibleActionInterface action
393 Returns an empty QString if there is no Qt predefined equivalent.
394*/
395QString translateAction(NSString *nsAction, QAccessibleInterface *interface)
396{
397 if ([nsAction compare: NSAccessibilityPressAction] == NSOrderedSame) {
398 if (interface->role() == QAccessible::CheckBox
399 || interface->role() == QAccessible::RadioButton
400 || interface->role() == QAccessible::Switch) {
401 return QAccessibleActionInterface::toggleAction();
402 }
403 return QAccessibleActionInterface::pressAction();
404 } else if ([nsAction compare: NSAccessibilityIncrementAction] == NSOrderedSame)
405 return QAccessibleActionInterface::increaseAction();
406 else if ([nsAction compare: NSAccessibilityDecrementAction] == NSOrderedSame)
407 return QAccessibleActionInterface::decreaseAction();
408 else if ([nsAction compare: NSAccessibilityShowMenuAction] == NSOrderedSame)
409 return QAccessibleActionInterface::showMenuAction();
410 else if ([nsAction compare: NSAccessibilityRaiseAction] == NSOrderedSame)
411 return QAccessibleActionInterface::setFocusAction();
412
413 // See getTranslatedAction for not matched translations.
414
415 return QString();
416}
417
418bool hasValueAttribute(QAccessibleInterface *interface)
419{
420 Q_ASSERT(interface);
421 const QAccessible::Role qtrole = interface->role();
422 if (qtrole == QAccessible::EditableText
423 || qtrole == QAccessible::StaticText
424 || interface->valueInterface()
425 || interface->state().checkable
426 || macRole(interface) == NSAccessibilityDisclosureTriangleRole) {
427 return true;
428 }
429
430 return false;
431}
432
433id getValueAttribute(QAccessibleInterface *interface)
434{
435 const QAccessible::Role qtrole = interface->role();
436 if (qtrole == QAccessible::StaticText) {
437 return interface->text(QAccessible::Name).toNSString();
438 }
439 if (qtrole == QAccessible::EditableText) {
440 if (QAccessibleTextInterface *textInterface = interface->textInterface()) {
441
442 int begin = 0;
443 int end = textInterface->characterCount();
444 QString text;
445 if (interface->state().passwordEdit) {
446 // return round password replacement chars
447 text = QString(end, QChar(0x2022));
448 } else {
449 // VoiceOver will read out the entire text string at once when returning
450 // text as a value. For large text edits the size of the returned string
451 // needs to be limited and text range attributes need to be used instead.
452 // NSTextEdit returns the first sentence as the value, Do the same here:
453 // ### call to textAfterOffset hangs. Booo!
454 //if (textInterface->characterCount() > 0)
455 // textInterface->textAfterOffset(0, QAccessible2::SentenceBoundary, &begin, &end);
456 text = textInterface->text(begin, end);
457 }
458 return text.toNSString();
459 }
460 }
461
462 if (QAccessibleValueInterface *valueInterface = interface->valueInterface()) {
463 return valueInterface->currentValue().toString().toNSString();
464 }
465
466 if (interface->state().checkable) {
467 if (interface->state().checkStateMixed)
468 return @(2);
469 return interface->state().checked ? @(1) : @(0);
470 }
471
472 if (macRole(interface) == NSAccessibilityDisclosureTriangleRole) {
473 return interface->state().expanded ? @(1) : @(0);
474 }
475
476 return nil;
477}
478
479} // namespace QCocoaAccessible
480
481#endif // QT_CONFIG(accessibility)
482
483QT_END_NAMESPACE
Combined button and popup list for selecting options.