1/**************************************************************************** 2** 3** Copyright (C) 2016 The Qt Company Ltd. 4** Contact: https://www.qt.io/licensing/ 5** 6** This file is part of the plugins of the Qt Toolkit. 7** 8** $QT_BEGIN_LICENSE:LGPL$ 9** Commercial License Usage 10** Licensees holding valid commercial Qt licenses may use this file in 11** accordance with the commercial license agreement provided with the 12** Software or, alternatively, in accordance with the terms contained in 13** a written agreement between you and The Qt Company. For licensing terms 14** and conditions see https://www.qt.io/terms-conditions. For further 15** information use the contact form at https://www.qt.io/contact-us. 16** 17** GNU Lesser General Public License Usage 18** Alternatively, this file may be used under the terms of the GNU Lesser 19** General Public License version 3 as published by the Free Software 20** Foundation and appearing in the file LICENSE.LGPL3 included in the 21** packaging of this file. Please review the following information to 22** ensure the GNU Lesser General Public License version 3 requirements 23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24** 25** GNU General Public License Usage 26** Alternatively, this file may be used under the terms of the GNU 27** General Public License version 2.0 or (at your option) the GNU General 28** Public license version 3 or any later version approved by the KDE Free 29** Qt Foundation. The licenses are as published by the Free Software 30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31** included in the packaging of this file. Please review the following 32** information to ensure the GNU General Public License requirements will 33** be met: https://www.gnu.org/licenses/gpl-2.0.html and 34** https://www.gnu.org/licenses/gpl-3.0.html. 35** 36** $QT_END_LICENSE$ 37** 38****************************************************************************/ 39#include "qcocoaaccessibility.h" 40#include "qcocoaaccessibilityelement.h" 41#include <QtGui/qaccessible.h> 42#include <private/qcore_mac_p.h> 43 44QT_BEGIN_NAMESPACE 45 46#ifndef QT_NO_ACCESSIBILITY 47 48QCocoaAccessibility::QCocoaAccessibility() 49{ 50 51} 52 53QCocoaAccessibility::~QCocoaAccessibility() 54{ 55 56} 57 58void QCocoaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) 59{ 60 if (!isActive() || !event->accessibleInterface() || !event->accessibleInterface()->isValid()) 61 return; 62 QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId: event->uniqueId()]; 63 if (!element) { 64 qWarning("QCocoaAccessibility::notifyAccessibilityUpdate: invalid element"); 65 return; 66 } 67 68 switch (event->type()) { 69 case QAccessible::Focus: { 70 NSAccessibilityPostNotification(element, NSAccessibilityFocusedUIElementChangedNotification); 71 break; 72 } 73 case QAccessible::StateChanged: 74 case QAccessible::ValueChanged: 75 case QAccessible::TextInserted: 76 case QAccessible::TextRemoved: 77 case QAccessible::TextUpdated: 78 NSAccessibilityPostNotification(element, NSAccessibilityValueChangedNotification); 79 break; 80 case QAccessible::TextCaretMoved: 81 case QAccessible::TextSelectionChanged: 82 NSAccessibilityPostNotification(element, NSAccessibilitySelectedTextChangedNotification); 83 break; 84 case QAccessible::NameChanged: 85 NSAccessibilityPostNotification(element, NSAccessibilityTitleChangedNotification); 86 break; 87 default: 88 break; 89 } 90} 91 92void QCocoaAccessibility::setRootObject(QObject *o) 93{ 94 Q_UNUSED(o) 95} 96 97void QCocoaAccessibility::initialize() 98{ 99 100} 101 102void QCocoaAccessibility::cleanup() 103{ 104 105} 106 107namespace QCocoaAccessible { 108 109typedef QMap<QAccessible::Role, NSString *> QMacAccessibiltyRoleMap; 110Q_GLOBAL_STATIC(QMacAccessibiltyRoleMap, qMacAccessibiltyRoleMap); 111 112static void populateRoleMap() 113{ 114 QMacAccessibiltyRoleMap &roleMap = *qMacAccessibiltyRoleMap(); 115 roleMap[QAccessible::MenuItem] = NSAccessibilityMenuItemRole; 116 roleMap[QAccessible::MenuBar] = NSAccessibilityMenuBarRole; 117 roleMap[QAccessible::ScrollBar] = NSAccessibilityScrollBarRole; 118 roleMap[QAccessible::Grip] = NSAccessibilityGrowAreaRole; 119 roleMap[QAccessible::Window] = NSAccessibilityWindowRole; 120 roleMap[QAccessible::Dialog] = NSAccessibilityWindowRole; 121 roleMap[QAccessible::AlertMessage] = NSAccessibilityWindowRole; 122 roleMap[QAccessible::ToolTip] = NSAccessibilityWindowRole; 123 roleMap[QAccessible::HelpBalloon] = NSAccessibilityWindowRole; 124 roleMap[QAccessible::PopupMenu] = NSAccessibilityMenuRole; 125 roleMap[QAccessible::Application] = NSAccessibilityApplicationRole; 126 roleMap[QAccessible::Pane] = NSAccessibilityGroupRole; 127 roleMap[QAccessible::Grouping] = NSAccessibilityGroupRole; 128 roleMap[QAccessible::Separator] = NSAccessibilitySplitterRole; 129 roleMap[QAccessible::ToolBar] = NSAccessibilityToolbarRole; 130 roleMap[QAccessible::PageTab] = NSAccessibilityRadioButtonRole; 131 roleMap[QAccessible::ButtonMenu] = NSAccessibilityMenuButtonRole; 132 roleMap[QAccessible::ButtonDropDown] = NSAccessibilityPopUpButtonRole; 133 roleMap[QAccessible::SpinBox] = NSAccessibilityIncrementorRole; 134 roleMap[QAccessible::Slider] = NSAccessibilitySliderRole; 135 roleMap[QAccessible::ProgressBar] = NSAccessibilityProgressIndicatorRole; 136 roleMap[QAccessible::ComboBox] = NSAccessibilityComboBoxRole; 137 roleMap[QAccessible::RadioButton] = NSAccessibilityRadioButtonRole; 138 roleMap[QAccessible::CheckBox] = NSAccessibilityCheckBoxRole; 139 roleMap[QAccessible::StaticText] = NSAccessibilityStaticTextRole; 140 roleMap[QAccessible::Table] = NSAccessibilityTableRole; 141 roleMap[QAccessible::StatusBar] = NSAccessibilityStaticTextRole; 142 roleMap[QAccessible::Column] = NSAccessibilityColumnRole; 143 roleMap[QAccessible::ColumnHeader] = NSAccessibilityColumnRole; 144 roleMap[QAccessible::Row] = NSAccessibilityRowRole; 145 roleMap[QAccessible::RowHeader] = NSAccessibilityRowRole; 146 roleMap[QAccessible::Cell] = NSAccessibilityTextFieldRole; 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] = NSAccessibilityStaticTextRole; 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} 167 168/* 169 Returns a Cocoa accessibility role for the given interface, or 170 NSAccessibilityUnknownRole if no role mapping is found. 171*/ 172NSString *macRole(QAccessibleInterface *interface) 173{ 174 QAccessible::Role qtRole = interface->role(); 175 QMacAccessibiltyRoleMap &roleMap = *qMacAccessibiltyRoleMap(); 176 177 if (roleMap.isEmpty()) 178 populateRoleMap(); 179 180 // MAC_ACCESSIBILTY_DEBUG() << "role for" << interface.object() << "interface role" << Qt::hex << qtRole; 181 182 if (roleMap.contains(qtRole)) { 183 // MAC_ACCESSIBILTY_DEBUG() << "return" << roleMap[qtRole]; 184 if (roleMap[qtRole] == NSAccessibilityTextFieldRole && interface->state().multiLine) 185 return NSAccessibilityTextAreaRole; 186 return roleMap[qtRole]; 187 } 188 189 // Treat unknown Qt roles as generic group container items. Returning 190 // NSAccessibilityUnknownRole is also possible but makes the screen 191 // reader focus on the item instead of passing focus to child items. 192 // MAC_ACCESSIBILTY_DEBUG() << "return NSAccessibilityGroupRole for unknown Qt role"; 193 return NSAccessibilityGroupRole; 194} 195 196/* 197 Returns a Cocoa sub role for the given interface. 198*/ 199NSString *macSubrole(QAccessibleInterface *interface) 200{ 201 QAccessible::State s = interface->state(); 202 if (s.searchEdit) 203 return NSAccessibilitySearchFieldSubrole; 204 if (s.passwordEdit) 205 return NSAccessibilitySecureTextFieldSubrole; 206 return nil; 207} 208 209/* 210 Cocoa accessibility supports ignoring elements, which means that 211 the elements are still present in the accessibility tree but is 212 not used by the screen reader. 213*/ 214bool shouldBeIgnored(QAccessibleInterface *interface) 215{ 216 // Cocoa accessibility does not have an attribute that corresponds to the Invisible/Offscreen 217 // state. Ignore interfaces with those flags set. 218 const QAccessible::State state = interface->state(); 219 if (state.invisible || 220 state.offscreen || 221 state.invalid) 222 return true; 223 224 // Some roles are not interesting. In particular, container roles should be 225 // ignored in order to flatten the accessibility tree as seen by the user. 226 const QAccessible::Role role = interface->role(); 227 if (role == QAccessible::Border || // QFrame 228 role == QAccessible::Application || // We use the system-provided application element. 229 role == QAccessible::ToolBar || // Access the tool buttons directly. 230 role == QAccessible::Pane || // Scroll areas. 231 role == QAccessible::Client) // The default for QWidget. 232 return true; 233 234 NSString *mac_role = macRole(interface); 235 if (mac_role == NSAccessibilityWindowRole || // We use the system-provided window elements. 236 mac_role == NSAccessibilityUnknownRole) 237 return true; 238 239 // Client is a generic role returned by plain QWidgets or other 240 // widgets that does not have separate QAccessible interface, such 241 // as the TabWidget. Return false unless macRole gives the interface 242 // a special role. 243 if (role == QAccessible::Client && mac_role == NSAccessibilityUnknownRole) 244 return true; 245 246 if (QObject * const object = interface->object()) { 247 const QString className = QLatin1String(object->metaObject()->className()); 248 249 // VoiceOver focusing on tool tips can be confusing. The contents of the 250 // tool tip is available through the description attribute anyway, so 251 // we disable accessibility for tool tips. 252 if (className == QLatin1String("QTipLabel")) 253 return true; 254 } 255 256 return false; 257} 258 259NSArray<QMacAccessibilityElement *> *unignoredChildren(QAccessibleInterface *interface) 260{ 261 int numKids = interface->childCount(); 262 // qDebug() << "Children for: " << axid << iface << " are: " << numKids; 263 264 NSMutableArray<QMacAccessibilityElement *> *kids = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:numKids]; 265 for (int i = 0; i < numKids; ++i) { 266 QAccessibleInterface *child = interface->child(i); 267 if (!child || !child->isValid() || child->state().invalid || child->state().invisible) 268 continue; 269 270 QAccessible::Id childId = QAccessible::uniqueId(child); 271 //qDebug() << " kid: " << childId << child; 272 273 QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId: childId]; 274 if (element) 275 [kids addObject: element]; 276 else 277 qWarning("QCocoaAccessibility: invalid child"); 278 } 279 return NSAccessibilityUnignoredChildren(kids); 280} 281/* 282 Translates a predefined QAccessibleActionInterface action to a Mac action constant. 283 Returns 0 if the Qt Action has no mac equivalent. Ownership of the NSString is 284 not transferred. 285*/ 286NSString *getTranslatedAction(const QString &qtAction) 287{ 288 if (qtAction == QAccessibleActionInterface::pressAction()) 289 return NSAccessibilityPressAction; 290 else if (qtAction == QAccessibleActionInterface::increaseAction()) 291 return NSAccessibilityIncrementAction; 292 else if (qtAction == QAccessibleActionInterface::decreaseAction()) 293 return NSAccessibilityDecrementAction; 294 else if (qtAction == QAccessibleActionInterface::showMenuAction()) 295 return NSAccessibilityShowMenuAction; 296 else if (qtAction == QAccessibleActionInterface::setFocusAction()) // Not 100% sure on this one 297 return NSAccessibilityRaiseAction; 298 else if (qtAction == QAccessibleActionInterface::toggleAction()) 299 return NSAccessibilityPressAction; 300 301 // Not translated: 302 // 303 // Qt: 304 // static const QString &checkAction(); 305 // static const QString &uncheckAction(); 306 // 307 // Cocoa: 308 // NSAccessibilityConfirmAction; 309 // NSAccessibilityPickAction; 310 // NSAccessibilityCancelAction; 311 // NSAccessibilityDeleteAction; 312 313 return nil; 314} 315 316 317/* 318 Translates between a Mac action constant and a QAccessibleActionInterface action 319 Returns an empty QString if there is no Qt predefined equivalent. 320*/ 321QString translateAction(NSString *nsAction, QAccessibleInterface *interface) 322{ 323 if ([nsAction compare: NSAccessibilityPressAction] == NSOrderedSame) { 324 if (interface->role() == QAccessible::CheckBox || interface->role() == QAccessible::RadioButton) 325 return QAccessibleActionInterface::toggleAction(); 326 return QAccessibleActionInterface::pressAction(); 327 } else if ([nsAction compare: NSAccessibilityIncrementAction] == NSOrderedSame) 328 return QAccessibleActionInterface::increaseAction(); 329 else if ([nsAction compare: NSAccessibilityDecrementAction] == NSOrderedSame) 330 return QAccessibleActionInterface::decreaseAction(); 331 else if ([nsAction compare: NSAccessibilityShowMenuAction] == NSOrderedSame) 332 return QAccessibleActionInterface::showMenuAction(); 333 else if ([nsAction compare: NSAccessibilityRaiseAction] == NSOrderedSame) 334 return QAccessibleActionInterface::setFocusAction(); 335 336 // See getTranslatedAction for not matched translations. 337 338 return QString(); 339} 340 341bool hasValueAttribute(QAccessibleInterface *interface) 342{ 343 Q_ASSERT(interface); 344 const QAccessible::Role qtrole = interface->role(); 345 if (qtrole == QAccessible::EditableText 346 || qtrole == QAccessible::StaticText 347 || interface->valueInterface() 348 || interface->state().checkable) { 349 return true; 350 } 351 352 return false; 353} 354 355id getValueAttribute(QAccessibleInterface *interface) 356{ 357 const QAccessible::Role qtrole = interface->role(); 358 if (qtrole == QAccessible::StaticText) { 359 return interface->text(QAccessible::Name).toNSString(); 360 } 361 if (qtrole == QAccessible::EditableText) { 362 if (QAccessibleTextInterface *textInterface = interface->textInterface()) { 363 364 int begin = 0; 365 int end = textInterface->characterCount(); 366 QString text; 367 if (interface->state().passwordEdit) { 368 // return round password replacement chars 369 text = QString(end, QChar(0x2022)); 370 } else { 371 // VoiceOver will read out the entire text string at once when returning 372 // text as a value. For large text edits the size of the returned string 373 // needs to be limited and text range attributes need to be used instead. 374 // NSTextEdit returns the first sentence as the value, Do the same here: 375 // ### call to textAfterOffset hangs. Booo! 376 //if (textInterface->characterCount() > 0) 377 // textInterface->textAfterOffset(0, QAccessible2::SentenceBoundary, &begin, &end); 378 text = textInterface->text(begin, end); 379 } 380 return text.toNSString(); 381 } 382 } 383 384 if (QAccessibleValueInterface *valueInterface = interface->valueInterface()) { 385 return valueInterface->currentValue().toString().toNSString(); 386 } 387 388 if (interface->state().checkable) { 389 return interface->state().checked ? @(1) : @(0); 390 } 391 392 return nil; 393} 394 395} // namespace QCocoaAccessible 396 397#endif // QT_NO_ACCESSIBILITY 398 399QT_END_NAMESPACE 400 401