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