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::Switch] = NSAccessibilityCheckBoxRole;
136 roleMap[QAccessible::StaticText] = NSAccessibilityStaticTextRole;
137 roleMap[QAccessible::Table] = NSAccessibilityTableRole;
138 roleMap[QAccessible::StatusBar] = NSAccessibilityStaticTextRole;
139 roleMap[QAccessible::Column] = NSAccessibilityColumnRole;
140 roleMap[QAccessible::ColumnHeader] = NSAccessibilityColumnRole;
141 roleMap[QAccessible::Row] = NSAccessibilityRowRole;
142 roleMap[QAccessible::RowHeader] = NSAccessibilityRowRole;
143 roleMap[QAccessible::Button] = NSAccessibilityButtonRole;
144 roleMap[QAccessible::EditableText] = NSAccessibilityTextFieldRole;
145 roleMap[QAccessible::Link] = NSAccessibilityLinkRole;
146 roleMap[QAccessible::Indicator] = NSAccessibilityValueIndicatorRole;
147 roleMap[QAccessible::Splitter] = NSAccessibilitySplitGroupRole;
148 roleMap[QAccessible::List] = NSAccessibilityListRole;
149 roleMap[QAccessible::ListItem] = NSAccessibilityStaticTextRole;
150 roleMap[QAccessible::Cell] = NSAccessibilityCellRole;
151 roleMap[QAccessible::Client] = NSAccessibilityGroupRole;
152 roleMap[QAccessible::Paragraph] = NSAccessibilityGroupRole;
153 roleMap[QAccessible::Section] = NSAccessibilityGroupRole;
154 roleMap[QAccessible::WebDocument] = NSAccessibilityGroupRole;
155 roleMap[QAccessible::ColorChooser] = NSAccessibilityColorWellRole;
156 roleMap[QAccessible::Footer] = NSAccessibilityGroupRole;
157 roleMap[QAccessible::Form] = NSAccessibilityGroupRole;
158 roleMap[QAccessible::Heading] = @"AXHeading";
159 roleMap[QAccessible::Note] = NSAccessibilityGroupRole;
160 roleMap[QAccessible::ComplementaryContent] = NSAccessibilityGroupRole;
161 roleMap[QAccessible::Graphic] = NSAccessibilityImageRole;
162 roleMap[QAccessible::Tree] = NSAccessibilityOutlineRole;
163 roleMap[QAccessible::BlockQuote] = NSAccessibilityGroupRole;
164 roleMap[QAccessible::LayeredPane] = NSAccessibilityGroupRole;
165}
166
167/*
168 Returns a Cocoa accessibility role for the given interface, or
169 NSAccessibilityUnknownRole if no role mapping is found.
170*/
171NSString *macRole(QAccessibleInterface *interface)
172{
173 QAccessible::Role qtRole = interface->role();
174 QMacAccessibiltyRoleMap &roleMap = *qMacAccessibiltyRoleMap();
175
176 if (roleMap.isEmpty())
177 populateRoleMap();
178
179 // MAC_ACCESSIBILTY_DEBUG() << "role for" << interface.object() << "interface role" << Qt::hex << qtRole;
180
181 if (roleMap.contains(qtRole)) {
182 // MAC_ACCESSIBILTY_DEBUG() << "return" << roleMap[qtRole];
183 if (roleMap[qtRole] == NSAccessibilityComboBoxRole && !interface->state().editable)
184 return NSAccessibilityMenuButtonRole;
185 if (roleMap[qtRole] == NSAccessibilityTextFieldRole && interface->state().multiLine)
186 return NSAccessibilityTextAreaRole;
187 return roleMap[qtRole];
188 }
189
190 // Treat unknown Qt roles as generic group container items. Returning
191 // NSAccessibilityUnknownRole is also possible but makes the screen
192 // reader focus on the item instead of passing focus to child items.
193 // MAC_ACCESSIBILTY_DEBUG() << "return NSAccessibilityGroupRole for unknown Qt role";
194 return NSAccessibilityGroupRole;
195}
196
197/*
198 Returns a Cocoa sub role for the given interface.
199*/
200NSString *macSubrole(QAccessibleInterface *interface)
201{
202 QAccessible::State s = interface->state();
203 if (s.searchEdit)
204 return NSAccessibilitySearchFieldSubrole;
205 if (s.passwordEdit)
206 return NSAccessibilitySecureTextFieldSubrole;
207 if (interface->role() == QAccessible::PageTab)
208 return NSAccessibilityTabButtonSubrole;
209 if (interface->role() == QAccessible::Switch)
210 return NSAccessibilitySwitchSubrole;
211 return nil;
212}
213
214/*
215 Cocoa accessibility supports ignoring elements, which means that
216 the elements are still present in the accessibility tree but is
217 not used by the screen reader.
218*/
219bool shouldBeIgnored(QAccessibleInterface *interface)
220{
221 // Cocoa accessibility does not have an attribute that corresponds to the Invisible/Offscreen
222 // state. Ignore interfaces with those flags set.
223 const QAccessible::State state = interface->state();
224 if (state.invisible || state.offscreen || state.invalid)
225 return true;
226
227 // Some roles are not interesting. In particular, container roles should be
228 // ignored in order to flatten the accessibility tree as seen by the user.
229 switch (interface->role()) {
230 case QAccessible::Border: // QFrame
231 case QAccessible::Application: // We use the system-provided application element.
232 case QAccessible::ToolBar: // Access the tool buttons directly.
233 case QAccessible::Pane: // Scroll areas.
234 case QAccessible::Client: // The default for QWidget.
235 case QAccessible::PopupMenu: // Access the menu items directly
236 return true;
237 default:
238 break;
239 }
240
241 NSString *mac_role = macRole(interface);
242 if (mac_role == NSAccessibilityWindowRole || // We use the system-provided window elements.
243 mac_role == NSAccessibilityUnknownRole) {
244 return true;
245 }
246
247 if (const QObject *object = interface->object()) {
248 const QByteArrayView className = object->metaObject()->className();
249
250 // VoiceOver focusing on tool tips can be confusing. The contents of the
251 // tool tip is available through the description attribute anyway, so
252 // we disable accessibility for tool tips.
253 if (className == "QTipLabel"_ba)
254 return true;
255 }
256
257 return false;
258}
259
260bool defaultUnignored(QAccessibleInterface *child)
261{
262 if (child && child->isValid()) {
263 const auto state = child->state();
264 return !state.invalid && !state.invisible;
265 }
266 return false;
267}
268
269NSArray<QMacAccessibilityElement *> *unignoredChildren(QAccessibleInterface *interface,
270 const std::function<bool(QAccessibleInterface *child)> &pred)
271{
272 int numKids = interface->childCount();
273
274 NSMutableArray<QMacAccessibilityElement *> *kids = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:numKids];
275 for (int i = 0; i < numKids; ++i) {
276 QAccessibleInterface *child = interface->child(i);
277
278 if (!pred(child))
279 continue;
280
281 QAccessible::Id childId = QAccessible::uniqueId(child);
282
283 QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId: childId];
284 if (element)
285 [kids addObject: element];
286 else
287 qWarning("QCocoaAccessibility: invalid child");
288 }
289 return NSAccessibilityUnignoredChildren(kids);
290}
291
292/*
293 Translates a predefined QAccessibleActionInterface action to a Mac action constant.
294 Returns 0 if the Qt Action has no mac equivalent. Ownership of the NSString is
295 not transferred.
296*/
297NSString *getTranslatedAction(const QString &qtAction)
298{
299 if (qtAction == QAccessibleActionInterface::pressAction())
300 return NSAccessibilityPressAction;
301 else if (qtAction == QAccessibleActionInterface::increaseAction())
302 return NSAccessibilityIncrementAction;
303 else if (qtAction == QAccessibleActionInterface::decreaseAction())
304 return NSAccessibilityDecrementAction;
305 else if (qtAction == QAccessibleActionInterface::showMenuAction())
306 return NSAccessibilityShowMenuAction;
307 else if (qtAction == QAccessibleActionInterface::setFocusAction()) // Not 100% sure on this one
308 return NSAccessibilityRaiseAction;
309 else if (qtAction == QAccessibleActionInterface::toggleAction())
310 return NSAccessibilityPressAction;
311
312 // Not translated:
313 //
314 // Qt:
315 // static const QString &checkAction();
316 // static const QString &uncheckAction();
317 //
318 // Cocoa:
319 // NSAccessibilityConfirmAction;
320 // NSAccessibilityPickAction;
321 // NSAccessibilityCancelAction;
322 // NSAccessibilityDeleteAction;
323
324 return nil;
325}
326
327
328/*
329 Translates between a Mac action constant and a QAccessibleActionInterface action
330 Returns an empty QString if there is no Qt predefined equivalent.
331*/
332QString translateAction(NSString *nsAction, QAccessibleInterface *interface)
333{
334 if ([nsAction compare: NSAccessibilityPressAction] == NSOrderedSame) {
335 if (interface->role() == QAccessible::CheckBox
336 || interface->role() == QAccessible::RadioButton
337 || interface->role() == QAccessible::Switch) {
338 return QAccessibleActionInterface::toggleAction();
339 }
340 return QAccessibleActionInterface::pressAction();
341 } else if ([nsAction compare: NSAccessibilityIncrementAction] == NSOrderedSame)
342 return QAccessibleActionInterface::increaseAction();
343 else if ([nsAction compare: NSAccessibilityDecrementAction] == NSOrderedSame)
344 return QAccessibleActionInterface::decreaseAction();
345 else if ([nsAction compare: NSAccessibilityShowMenuAction] == NSOrderedSame)
346 return QAccessibleActionInterface::showMenuAction();
347 else if ([nsAction compare: NSAccessibilityRaiseAction] == NSOrderedSame)
348 return QAccessibleActionInterface::setFocusAction();
349
350 // See getTranslatedAction for not matched translations.
351
352 return QString();
353}
354
355bool hasValueAttribute(QAccessibleInterface *interface)
356{
357 Q_ASSERT(interface);
358 const QAccessible::Role qtrole = interface->role();
359 if (qtrole == QAccessible::EditableText
360 || qtrole == QAccessible::StaticText
361 || interface->valueInterface()
362 || interface->state().checkable) {
363 return true;
364 }
365
366 return false;
367}
368
369id getValueAttribute(QAccessibleInterface *interface)
370{
371 const QAccessible::Role qtrole = interface->role();
372 if (qtrole == QAccessible::StaticText) {
373 return interface->text(QAccessible::Name).toNSString();
374 }
375 if (qtrole == QAccessible::EditableText) {
376 if (QAccessibleTextInterface *textInterface = interface->textInterface()) {
377
378 int begin = 0;
379 int end = textInterface->characterCount();
380 QString text;
381 if (interface->state().passwordEdit) {
382 // return round password replacement chars
383 text = QString(end, QChar(0x2022));
384 } else {
385 // VoiceOver will read out the entire text string at once when returning
386 // text as a value. For large text edits the size of the returned string
387 // needs to be limited and text range attributes need to be used instead.
388 // NSTextEdit returns the first sentence as the value, Do the same here:
389 // ### call to textAfterOffset hangs. Booo!
390 //if (textInterface->characterCount() > 0)
391 // textInterface->textAfterOffset(0, QAccessible2::SentenceBoundary, &begin, &end);
392 text = textInterface->text(begin, end);
393 }
394 return text.toNSString();
395 }
396 }
397
398 if (QAccessibleValueInterface *valueInterface = interface->valueInterface()) {
399 return valueInterface->currentValue().toString().toNSString();
400 }
401
402 if (interface->state().checkable) {
403 if (interface->state().checkStateMixed)
404 return @(2);
405 return interface->state().checked ? @(1) : @(0);
406 }
407
408 return nil;
409}
410
411} // namespace QCocoaAccessible
412
413#endif // QT_CONFIG(accessibility)
414
415QT_END_NAMESPACE