1// Copyright 2017 The Chromium Embedded Framework Authors. Portions copyright 2// 2013 The Chromium Authors. All rights reserved. Use of this source code is 3// governed by a BSD-style license that can be found in the LICENSE file. 4 5// Sample implementation for the NSAccessibility protocol for interacting with 6// VoiceOver and other accessibility clients. 7 8#include "tests/cefclient/browser/osr_accessibility_node.h" 9 10#import <AppKit/NSAccessibility.h> 11#import <Cocoa/Cocoa.h> 12 13#include "tests/cefclient/browser/osr_accessibility_helper.h" 14 15namespace { 16 17NSString* AxRoleToNSAxRole(const std::string& role_string) { 18 if (role_string == "abbr") 19 return NSAccessibilityGroupRole; 20 if (role_string == "alertDialog") 21 return NSAccessibilityGroupRole; 22 if (role_string == "alert") 23 return NSAccessibilityGroupRole; 24 if (role_string == "annotation") 25 return NSAccessibilityUnknownRole; 26 if (role_string == "application") 27 return NSAccessibilityGroupRole; 28 if (role_string == "article") 29 return NSAccessibilityGroupRole; 30 if (role_string == "audio") 31 return NSAccessibilityGroupRole; 32 if (role_string == "banner") 33 return NSAccessibilityGroupRole; 34 if (role_string == "blockquote") 35 return NSAccessibilityGroupRole; 36 if (role_string == "busyIndicator") 37 return NSAccessibilityBusyIndicatorRole; 38 if (role_string == "button") 39 return NSAccessibilityButtonRole; 40 if (role_string == "buttonDropDown") 41 return NSAccessibilityButtonRole; 42 if (role_string == "canvas") 43 return NSAccessibilityImageRole; 44 if (role_string == "caption") 45 return NSAccessibilityGroupRole; 46 if (role_string == "checkBox") 47 return NSAccessibilityCheckBoxRole; 48 if (role_string == "colorWell") 49 return NSAccessibilityColorWellRole; 50 if (role_string == "column") 51 return NSAccessibilityColumnRole; 52 if (role_string == "comboBox") 53 return NSAccessibilityComboBoxRole; 54 if (role_string == "complementary") 55 return NSAccessibilityGroupRole; 56 if (role_string == "contentInfo") 57 return NSAccessibilityGroupRole; 58 if (role_string == "definition") 59 return NSAccessibilityGroupRole; 60 if (role_string == "descriptionListDetail") 61 return NSAccessibilityGroupRole; 62 if (role_string == "descriptionList") 63 return NSAccessibilityListRole; 64 if (role_string == "descriptionListTerm") 65 return NSAccessibilityGroupRole; 66 if (role_string == "details") 67 return NSAccessibilityGroupRole; 68 if (role_string == "dialog") 69 return NSAccessibilityGroupRole; 70 if (role_string == "directory") 71 return NSAccessibilityListRole; 72 if (role_string == "disclosureTriangle") 73 return NSAccessibilityDisclosureTriangleRole; 74 if (role_string == "div") 75 return NSAccessibilityGroupRole; 76 if (role_string == "document") 77 return NSAccessibilityGroupRole; 78 if (role_string == "embeddedObject") 79 return NSAccessibilityGroupRole; 80 if (role_string == "figcaption") 81 return NSAccessibilityGroupRole; 82 if (role_string == "figure") 83 return NSAccessibilityGroupRole; 84 if (role_string == "footer") 85 return NSAccessibilityGroupRole; 86 if (role_string == "form") 87 return NSAccessibilityGroupRole; 88 if (role_string == "genericContainer") 89 return NSAccessibilityGroupRole; 90 if (role_string == "grid") 91 return NSAccessibilityGroupRole; 92 if (role_string == "group") 93 return NSAccessibilityGroupRole; 94 if (role_string == "iframe") 95 return NSAccessibilityGroupRole; 96 if (role_string == "iframePresentational") 97 return NSAccessibilityGroupRole; 98 if (role_string == "ignored") 99 return NSAccessibilityUnknownRole; 100 if (role_string == "imageMapLink") 101 return NSAccessibilityLinkRole; 102 if (role_string == "imageMap") 103 return NSAccessibilityGroupRole; 104 if (role_string == "image") 105 return NSAccessibilityImageRole; 106 if (role_string == "labelText") 107 return NSAccessibilityGroupRole; 108 if (role_string == "legend") 109 return NSAccessibilityGroupRole; 110 if (role_string == "link") 111 return NSAccessibilityLinkRole; 112 if (role_string == "listBoxOption") 113 return NSAccessibilityStaticTextRole; 114 if (role_string == "listBox") 115 return NSAccessibilityListRole; 116 if (role_string == "listItem") 117 return NSAccessibilityGroupRole; 118 if (role_string == "list") 119 return NSAccessibilityListRole; 120 if (role_string == "log") 121 return NSAccessibilityGroupRole; 122 if (role_string == "main") 123 return NSAccessibilityGroupRole; 124 if (role_string == "mark") 125 return NSAccessibilityGroupRole; 126 if (role_string == "marquee") 127 return NSAccessibilityGroupRole; 128 if (role_string == "math") 129 return NSAccessibilityGroupRole; 130 if (role_string == "menu") 131 return NSAccessibilityMenuRole; 132 if (role_string == "menuBar") 133 return NSAccessibilityMenuBarRole; 134 if (role_string == "menuButton") 135 return NSAccessibilityButtonRole; 136 if (role_string == "menuItem") 137 return NSAccessibilityMenuItemRole; 138 if (role_string == "menuItemCheckBox") 139 return NSAccessibilityMenuItemRole; 140 if (role_string == "menuItemRadio") 141 return NSAccessibilityMenuItemRole; 142 if (role_string == "menuListOption") 143 return NSAccessibilityMenuItemRole; 144 if (role_string == "menuListPopup") 145 return NSAccessibilityUnknownRole; 146 if (role_string == "meter") 147 return NSAccessibilityProgressIndicatorRole; 148 if (role_string == "navigation") 149 return NSAccessibilityGroupRole; 150 if (role_string == "note") 151 return NSAccessibilityGroupRole; 152 if (role_string == "outline") 153 return NSAccessibilityOutlineRole; 154 if (role_string == "paragraph") 155 return NSAccessibilityGroupRole; 156 if (role_string == "popUpButton") 157 return NSAccessibilityPopUpButtonRole; 158 if (role_string == "pre") 159 return NSAccessibilityGroupRole; 160 if (role_string == "presentational") 161 return NSAccessibilityGroupRole; 162 if (role_string == "progressIndicator") 163 return NSAccessibilityProgressIndicatorRole; 164 if (role_string == "radioButton") 165 return NSAccessibilityRadioButtonRole; 166 if (role_string == "radioGroup") 167 return NSAccessibilityRadioGroupRole; 168 if (role_string == "region") 169 return NSAccessibilityGroupRole; 170 if (role_string == "row") 171 return NSAccessibilityRowRole; 172 if (role_string == "ruler") 173 return NSAccessibilityRulerRole; 174 if (role_string == "scrollBar") 175 return NSAccessibilityScrollBarRole; 176 if (role_string == "search") 177 return NSAccessibilityGroupRole; 178 if (role_string == "searchBox") 179 return NSAccessibilityTextFieldRole; 180 if (role_string == "slider") 181 return NSAccessibilitySliderRole; 182 if (role_string == "sliderThumb") 183 return NSAccessibilityValueIndicatorRole; 184 if (role_string == "spinButton") 185 return NSAccessibilityIncrementorRole; 186 if (role_string == "splitter") 187 return NSAccessibilitySplitterRole; 188 if (role_string == "staticText") 189 return NSAccessibilityStaticTextRole; 190 if (role_string == "status") 191 return NSAccessibilityGroupRole; 192 if (role_string == "svgRoot") 193 return NSAccessibilityGroupRole; 194 if (role_string == "switch") 195 return NSAccessibilityCheckBoxRole; 196 if (role_string == "tabGroup") 197 return NSAccessibilityTabGroupRole; 198 if (role_string == "tabList") 199 return NSAccessibilityTabGroupRole; 200 if (role_string == "tabPanel") 201 return NSAccessibilityGroupRole; 202 if (role_string == "tab") 203 return NSAccessibilityRadioButtonRole; 204 if (role_string == "tableHeaderContainer") 205 return NSAccessibilityGroupRole; 206 if (role_string == "table") 207 return NSAccessibilityTableRole; 208 if (role_string == "textField") 209 return NSAccessibilityTextFieldRole; 210 if (role_string == "time") 211 return NSAccessibilityGroupRole; 212 if (role_string == "timer") 213 return NSAccessibilityGroupRole; 214 if (role_string == "toggleButton") 215 return NSAccessibilityCheckBoxRole; 216 if (role_string == "toolbar") 217 return NSAccessibilityToolbarRole; 218 if (role_string == "treeGrid") 219 return NSAccessibilityTableRole; 220 if (role_string == "treeItem") 221 return NSAccessibilityRowRole; 222 if (role_string == "tree") 223 return NSAccessibilityOutlineRole; 224 if (role_string == "unknown") 225 return NSAccessibilityUnknownRole; 226 if (role_string == "tooltip") 227 return NSAccessibilityGroupRole; 228 if (role_string == "video") 229 return NSAccessibilityGroupRole; 230 if (role_string == "window") 231 return NSAccessibilityWindowRole; 232 return [NSString stringWithUTF8String:role_string.c_str()]; 233} 234 235inline int MiddleX(const CefRect& rect) { 236 return rect.x + rect.width / 2; 237} 238 239inline int MiddleY(const CefRect& rect) { 240 return rect.y + rect.height / 2; 241} 242 243} // namespace 244 245// OsrAXNodeObject is sample implementation for the NSAccessibility protocol 246// for interacting with VoiceOver and other accessibility clients. 247@interface OsrAXNodeObject : NSObject { 248 // OsrAXNode* proxy object 249 client::OsrAXNode* node_; 250 CefNativeAccessible* parent_; 251} 252 253- (id)init:(client::OsrAXNode*)node; 254+ (OsrAXNodeObject*)elementWithNode:(client::OsrAXNode*)node; 255@end 256 257@implementation OsrAXNodeObject 258- (id)init:(client::OsrAXNode*)node { 259 node_ = node; 260 parent_ = node_->GetParentAccessibleObject(); 261 if (!parent_) { 262 parent_ = node_->GetWindowHandle(); 263 } 264 return self; 265} 266 267+ (OsrAXNodeObject*)elementWithNode:(client::OsrAXNode*)node { 268 // We manage the release ourself 269 return [[OsrAXNodeObject alloc] init:node]; 270} 271 272- (BOOL)isEqual:(id)object { 273 if ([object isKindOfClass:[OsrAXNodeObject self]]) { 274 OsrAXNodeObject* other = object; 275 return (node_ == other->node_); 276 } else { 277 return NO; 278 } 279} 280 281// Utility methods to map AX information received from renderer 282// to platform properties 283- (NSString*)axRole { 284 // Get the Role from CefAccessibilityHelper and Map to NSRole 285 return AxRoleToNSAxRole(node_->AxRole()); 286} 287 288- (NSString*)axDescription { 289 std::string desc = node_->AxDescription(); 290 return [NSString stringWithUTF8String:desc.c_str()]; 291} 292 293- (NSString*)axName { 294 std::string desc = node_->AxName(); 295 return [NSString stringWithUTF8String:desc.c_str()]; 296} 297 298- (NSString*)axValue { 299 std::string desc = node_->AxValue(); 300 return [NSString stringWithUTF8String:desc.c_str()]; 301} 302 303- (void)doMouseClick:(cef_mouse_button_type_t)type { 304 CefRefPtr<CefBrowser> browser = node_->GetBrowser(); 305 if (browser) { 306 CefMouseEvent mouse_event; 307 const CefRect& rect = node_->AxLocation(); 308 mouse_event.x = MiddleX(rect); 309 mouse_event.y = MiddleY(rect); 310 311 mouse_event.modifiers = 0; 312 browser->GetHost()->SendMouseClickEvent(mouse_event, type, false, 1); 313 browser->GetHost()->SendMouseClickEvent(mouse_event, type, true, 1); 314 } 315} 316 317- (NSMutableArray*)getKids { 318 int numChilds = node_->GetChildCount(); 319 if (numChilds > 0) { 320 NSMutableArray* kids = [NSMutableArray arrayWithCapacity:numChilds]; 321 for (int index = 0; index < numChilds; index++) { 322 client::OsrAXNode* child = node_->ChildAtIndex(index); 323 [kids addObject:child ? CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT( 324 child->GetNativeAccessibleObject(node_)) 325 : nil]; 326 } 327 return kids; 328 } 329 return nil; 330} 331 332- (NSPoint)position { 333 CefRect cef_rect = node_->AxLocation(); 334 NSPoint origin = NSMakePoint(cef_rect.x, cef_rect.y); 335 NSSize size = NSMakeSize(cef_rect.width, cef_rect.height); 336 337 NSView* view = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(node_->GetWindowHandle()); 338 origin.y = NSHeight([view bounds]) - origin.y; 339 NSPoint originInWindow = [view convertPoint:origin toView:nil]; 340 341 NSRect point_rect = NSMakeRect(originInWindow.x, originInWindow.y, 0, 0); 342 NSPoint originInScreen = 343 [[view window] convertRectToScreen:point_rect].origin; 344 345 originInScreen.y = originInScreen.y - size.height; 346 return originInScreen; 347} 348 349- (NSSize)size { 350 CefRect cef_rect = node_->AxLocation(); 351 NSRect rect = 352 NSMakeRect(cef_rect.x, cef_rect.y, cef_rect.width, cef_rect.height); 353 NSView* view = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(node_->GetWindowHandle()); 354 rect = [[view window] convertRectToScreen:rect]; 355 return rect.size; 356} 357 358// 359// accessibility protocol 360// 361 362// attributes 363 364- (BOOL)accessibilityIsIgnored { 365 return NO; 366} 367 368- (NSArray*)accessibilityAttributeNames { 369 static NSArray* attributes = nil; 370 if (attributes == nil) { 371 attributes = [[NSArray alloc] 372 initWithObjects:NSAccessibilityRoleAttribute, 373 NSAccessibilityRoleDescriptionAttribute, 374 NSAccessibilityChildrenAttribute, 375 NSAccessibilityValueAttribute, 376 NSAccessibilityTitleAttribute, 377 NSAccessibilityDescriptionAttribute, 378 NSAccessibilityFocusedAttribute, 379 NSAccessibilityParentAttribute, 380 NSAccessibilityWindowAttribute, 381 NSAccessibilityTopLevelUIElementAttribute, 382 NSAccessibilityPositionAttribute, 383 NSAccessibilitySizeAttribute, nil]; 384 } 385 return attributes; 386} 387 388- (id)accessibilityAttributeValue:(NSString*)attribute { 389 NSObject* typed_parent = CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT(parent_); 390 if (!node_) 391 return nil; 392 if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) { 393 return [self axRole]; 394 } else if ([attribute 395 isEqualToString:NSAccessibilityRoleDescriptionAttribute]) { 396 return NSAccessibilityRoleDescription([self axRole], nil); 397 } else if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { 398 // Just check if the app thinks we're focused. 399 id focusedElement = [NSApp 400 accessibilityAttributeValue:NSAccessibilityFocusedUIElementAttribute]; 401 return [NSNumber numberWithBool:[focusedElement isEqual:self]]; 402 } else if ([attribute isEqualToString:NSAccessibilityParentAttribute]) { 403 return NSAccessibilityUnignoredAncestor(typed_parent); 404 } else if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) { 405 return NSAccessibilityUnignoredChildren([self getKids]); 406 } else if ([attribute isEqualToString:NSAccessibilityWindowAttribute]) { 407 // We're in the same window as our parent. 408 return [typed_parent 409 accessibilityAttributeValue:NSAccessibilityWindowAttribute]; 410 } else if ([attribute 411 isEqualToString:NSAccessibilityTopLevelUIElementAttribute]) { 412 // We're in the same top level element as our parent. 413 return [typed_parent 414 accessibilityAttributeValue:NSAccessibilityTopLevelUIElementAttribute]; 415 } else if ([attribute isEqualToString:NSAccessibilityPositionAttribute]) { 416 return [NSValue valueWithPoint:[self position]]; 417 } else if ([attribute isEqualToString:NSAccessibilitySizeAttribute]) { 418 return [NSValue valueWithSize:[self size]]; 419 } else if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) { 420 return [self axDescription]; 421 } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { 422 return [self axValue]; 423 } else if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) { 424 return [self axName]; 425 } 426 return nil; 427} 428 429- (id)accessibilityHitTest:(NSPoint)point { 430 return NSAccessibilityUnignoredAncestor(self); 431} 432 433- (NSArray*)accessibilityActionNames { 434 return [NSArray arrayWithObject:NSAccessibilityPressAction]; 435} 436 437- (NSString*)accessibilityActionDescription:(NSString*)action { 438 return NSAccessibilityActionDescription(action); 439} 440 441- (void)accessibilityPerformAction:(NSString*)action { 442 if ([action isEqualToString:NSAccessibilityPressAction]) { 443 // Do Click on Default action 444 [self doMouseClick:MBT_LEFT]; 445 } else if ([action isEqualToString:NSAccessibilityShowMenuAction]) { 446 // Right click for Context Menu 447 [self doMouseClick:MBT_RIGHT]; 448 } 449} 450 451- (id)accessibilityFocusedUIElement { 452 return NSAccessibilityUnignoredAncestor(self); 453} 454 455- (BOOL)accessibilityNotifiesWhenDestroyed { 456 // Indicate that BrowserAccessibilityCocoa will post a notification when it's 457 // destroyed (see -detach). This allows VoiceOver to do some internal things 458 // more efficiently. 459 return YES; 460} 461 462@end 463 464namespace client { 465 466void OsrAXNode::NotifyAccessibilityEvent(std::string event_type) const { 467 NSView* view = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(GetWindowHandle()); 468 if (event_type == "focus") { 469 NSAccessibilityPostNotification( 470 view, NSAccessibilityFocusedUIElementChangedNotification); 471 } else if (event_type == "textChanged") { 472 NSAccessibilityPostNotification(view, 473 NSAccessibilityTitleChangedNotification); 474 } else if (event_type == "valueChanged") { 475 NSAccessibilityPostNotification(view, 476 NSAccessibilityValueChangedNotification); 477 } else if (event_type == "textSelectionChanged") { 478 NSAccessibilityPostNotification(view, 479 NSAccessibilityValueChangedNotification); 480 } 481} 482 483void OsrAXNode::Destroy() { 484 if (platform_accessibility_) { 485 NSAccessibilityPostNotification( 486 CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT(platform_accessibility_), 487 NSAccessibilityUIElementDestroyedNotification); 488 } 489 490 delete this; 491} 492 493// Create and return NSAccessibility Implementation Object for Mac 494CefNativeAccessible* OsrAXNode::GetNativeAccessibleObject( 495 client::OsrAXNode* parent) { 496 if (!platform_accessibility_) { 497 platform_accessibility_ = CAST_NSOBJECT_TO_CEF_NATIVE_ACCESSIBLE( 498 [OsrAXNodeObject elementWithNode:this]); 499 SetParent(parent); 500 } 501 return platform_accessibility_; 502} 503 504} // namespace client 505