// Copyright 2017 The Chromium Embedded Framework Authors. Portions copyright // 2013 The Chromium Authors. All rights reserved. Use of this source code is // governed by a BSD-style license that can be found in the LICENSE file. // Sample implementation for the NSAccessibility protocol for interacting with // VoiceOver and other accessibility clients. #include "tests/cefclient/browser/osr_accessibility_node.h" #import #import #include "tests/cefclient/browser/osr_accessibility_helper.h" namespace { NSString* AxRoleToNSAxRole(const std::string& role_string) { if (role_string == "abbr") { return NSAccessibilityGroupRole; } if (role_string == "alertDialog") { return NSAccessibilityGroupRole; } if (role_string == "alert") { return NSAccessibilityGroupRole; } if (role_string == "annotation") { return NSAccessibilityUnknownRole; } if (role_string == "application") { return NSAccessibilityGroupRole; } if (role_string == "article") { return NSAccessibilityGroupRole; } if (role_string == "audio") { return NSAccessibilityGroupRole; } if (role_string == "banner") { return NSAccessibilityGroupRole; } if (role_string == "blockquote") { return NSAccessibilityGroupRole; } if (role_string == "busyIndicator") { return NSAccessibilityBusyIndicatorRole; } if (role_string == "button") { return NSAccessibilityButtonRole; } if (role_string == "buttonDropDown") { return NSAccessibilityButtonRole; } if (role_string == "canvas") { return NSAccessibilityImageRole; } if (role_string == "caption") { return NSAccessibilityGroupRole; } if (role_string == "checkBox") { return NSAccessibilityCheckBoxRole; } if (role_string == "colorWell") { return NSAccessibilityColorWellRole; } if (role_string == "column") { return NSAccessibilityColumnRole; } if (role_string == "comboBox") { return NSAccessibilityComboBoxRole; } if (role_string == "complementary") { return NSAccessibilityGroupRole; } if (role_string == "contentInfo") { return NSAccessibilityGroupRole; } if (role_string == "definition") { return NSAccessibilityGroupRole; } if (role_string == "descriptionListDetail") { return NSAccessibilityGroupRole; } if (role_string == "descriptionList") { return NSAccessibilityListRole; } if (role_string == "descriptionListTerm") { return NSAccessibilityGroupRole; } if (role_string == "details") { return NSAccessibilityGroupRole; } if (role_string == "dialog") { return NSAccessibilityGroupRole; } if (role_string == "directory") { return NSAccessibilityListRole; } if (role_string == "disclosureTriangle") { return NSAccessibilityDisclosureTriangleRole; } if (role_string == "div") { return NSAccessibilityGroupRole; } if (role_string == "document") { return NSAccessibilityGroupRole; } if (role_string == "embeddedObject") { return NSAccessibilityGroupRole; } if (role_string == "figcaption") { return NSAccessibilityGroupRole; } if (role_string == "figure") { return NSAccessibilityGroupRole; } if (role_string == "footer") { return NSAccessibilityGroupRole; } if (role_string == "form") { return NSAccessibilityGroupRole; } if (role_string == "genericContainer") { return NSAccessibilityGroupRole; } if (role_string == "grid") { return NSAccessibilityGroupRole; } if (role_string == "group") { return NSAccessibilityGroupRole; } if (role_string == "iframe") { return NSAccessibilityGroupRole; } if (role_string == "iframePresentational") { return NSAccessibilityGroupRole; } if (role_string == "ignored") { return NSAccessibilityUnknownRole; } if (role_string == "imageMapLink") { return NSAccessibilityLinkRole; } if (role_string == "imageMap") { return NSAccessibilityGroupRole; } if (role_string == "image") { return NSAccessibilityImageRole; } if (role_string == "labelText") { return NSAccessibilityGroupRole; } if (role_string == "legend") { return NSAccessibilityGroupRole; } if (role_string == "link") { return NSAccessibilityLinkRole; } if (role_string == "listBoxOption") { return NSAccessibilityStaticTextRole; } if (role_string == "listBox") { return NSAccessibilityListRole; } if (role_string == "listItem") { return NSAccessibilityGroupRole; } if (role_string == "list") { return NSAccessibilityListRole; } if (role_string == "log") { return NSAccessibilityGroupRole; } if (role_string == "main") { return NSAccessibilityGroupRole; } if (role_string == "mark") { return NSAccessibilityGroupRole; } if (role_string == "marquee") { return NSAccessibilityGroupRole; } if (role_string == "math") { return NSAccessibilityGroupRole; } if (role_string == "menu") { return NSAccessibilityMenuRole; } if (role_string == "menuBar") { return NSAccessibilityMenuBarRole; } if (role_string == "menuButton") { return NSAccessibilityButtonRole; } if (role_string == "menuItem") { return NSAccessibilityMenuItemRole; } if (role_string == "menuItemCheckBox") { return NSAccessibilityMenuItemRole; } if (role_string == "menuItemRadio") { return NSAccessibilityMenuItemRole; } if (role_string == "menuListOption") { return NSAccessibilityMenuItemRole; } if (role_string == "menuListPopup") { return NSAccessibilityUnknownRole; } if (role_string == "meter") { return NSAccessibilityProgressIndicatorRole; } if (role_string == "navigation") { return NSAccessibilityGroupRole; } if (role_string == "note") { return NSAccessibilityGroupRole; } if (role_string == "outline") { return NSAccessibilityOutlineRole; } if (role_string == "paragraph") { return NSAccessibilityGroupRole; } if (role_string == "popUpButton") { return NSAccessibilityPopUpButtonRole; } if (role_string == "pre") { return NSAccessibilityGroupRole; } if (role_string == "presentational") { return NSAccessibilityGroupRole; } if (role_string == "progressIndicator") { return NSAccessibilityProgressIndicatorRole; } if (role_string == "radioButton") { return NSAccessibilityRadioButtonRole; } if (role_string == "radioGroup") { return NSAccessibilityRadioGroupRole; } if (role_string == "region") { return NSAccessibilityGroupRole; } if (role_string == "row") { return NSAccessibilityRowRole; } if (role_string == "ruler") { return NSAccessibilityRulerRole; } if (role_string == "scrollBar") { return NSAccessibilityScrollBarRole; } if (role_string == "search") { return NSAccessibilityGroupRole; } if (role_string == "searchBox") { return NSAccessibilityTextFieldRole; } if (role_string == "slider") { return NSAccessibilitySliderRole; } if (role_string == "sliderThumb") { return NSAccessibilityValueIndicatorRole; } if (role_string == "spinButton") { return NSAccessibilityIncrementorRole; } if (role_string == "splitter") { return NSAccessibilitySplitterRole; } if (role_string == "staticText") { return NSAccessibilityStaticTextRole; } if (role_string == "status") { return NSAccessibilityGroupRole; } if (role_string == "svgRoot") { return NSAccessibilityGroupRole; } if (role_string == "switch") { return NSAccessibilityCheckBoxRole; } if (role_string == "tabGroup") { return NSAccessibilityTabGroupRole; } if (role_string == "tabList") { return NSAccessibilityTabGroupRole; } if (role_string == "tabPanel") { return NSAccessibilityGroupRole; } if (role_string == "tab") { return NSAccessibilityRadioButtonRole; } if (role_string == "tableHeaderContainer") { return NSAccessibilityGroupRole; } if (role_string == "table") { return NSAccessibilityTableRole; } if (role_string == "textField") { return NSAccessibilityTextFieldRole; } if (role_string == "time") { return NSAccessibilityGroupRole; } if (role_string == "timer") { return NSAccessibilityGroupRole; } if (role_string == "toggleButton") { return NSAccessibilityCheckBoxRole; } if (role_string == "toolbar") { return NSAccessibilityToolbarRole; } if (role_string == "treeGrid") { return NSAccessibilityTableRole; } if (role_string == "treeItem") { return NSAccessibilityRowRole; } if (role_string == "tree") { return NSAccessibilityOutlineRole; } if (role_string == "unknown") { return NSAccessibilityUnknownRole; } if (role_string == "tooltip") { return NSAccessibilityGroupRole; } if (role_string == "video") { return NSAccessibilityGroupRole; } if (role_string == "window") { return NSAccessibilityWindowRole; } return [NSString stringWithUTF8String:role_string.c_str()]; } inline int MiddleX(const CefRect& rect) { return rect.x + rect.width / 2; } inline int MiddleY(const CefRect& rect) { return rect.y + rect.height / 2; } } // namespace // OsrAXNodeObject is sample implementation for the NSAccessibility protocol // for interacting with VoiceOver and other accessibility clients. @interface OsrAXNodeObject : NSObject { // OsrAXNode* proxy object client::OsrAXNode* node_; CefNativeAccessible* parent_; } - (id)init:(client::OsrAXNode*)node; + (OsrAXNodeObject*)elementWithNode:(client::OsrAXNode*)node; @end @implementation OsrAXNodeObject - (id)init:(client::OsrAXNode*)node { node_ = node; parent_ = node_->GetParentAccessibleObject(); if (!parent_) { parent_ = node_->GetWindowHandle(); } return self; } + (OsrAXNodeObject*)elementWithNode:(client::OsrAXNode*)node { // We manage the release ourself return [[OsrAXNodeObject alloc] init:node]; } - (BOOL)isEqual:(id)object { if ([object isKindOfClass:[OsrAXNodeObject self]]) { OsrAXNodeObject* other = object; return (node_ == other->node_); } else { return NO; } } // Utility methods to map AX information received from renderer // to platform properties - (NSString*)axRole { // Get the Role from CefAccessibilityHelper and Map to NSRole return AxRoleToNSAxRole(node_->AxRole()); } - (NSString*)axDescription { std::string desc = node_->AxDescription(); return [NSString stringWithUTF8String:desc.c_str()]; } - (NSString*)axName { std::string desc = node_->AxName(); return [NSString stringWithUTF8String:desc.c_str()]; } - (NSString*)axValue { std::string desc = node_->AxValue(); return [NSString stringWithUTF8String:desc.c_str()]; } - (void)doMouseClick:(cef_mouse_button_type_t)type { CefRefPtr browser = node_->GetBrowser(); if (browser) { CefMouseEvent mouse_event; const CefRect& rect = node_->AxLocation(); mouse_event.x = MiddleX(rect); mouse_event.y = MiddleY(rect); mouse_event.modifiers = 0; browser->GetHost()->SendMouseClickEvent(mouse_event, type, false, 1); browser->GetHost()->SendMouseClickEvent(mouse_event, type, true, 1); } } - (NSMutableArray*)getKids { int numChilds = node_->GetChildCount(); if (numChilds > 0) { NSMutableArray* kids = [NSMutableArray arrayWithCapacity:numChilds]; for (int index = 0; index < numChilds; index++) { client::OsrAXNode* child = node_->ChildAtIndex(index); [kids addObject:child ? CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT( child->GetNativeAccessibleObject(node_)) : nil]; } return kids; } return nil; } - (NSPoint)position { CefRect cef_rect = node_->AxLocation(); NSPoint origin = NSMakePoint(cef_rect.x, cef_rect.y); NSSize size = NSMakeSize(cef_rect.width, cef_rect.height); NSView* view = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(node_->GetWindowHandle()); origin.y = NSHeight([view bounds]) - origin.y; NSPoint originInWindow = [view convertPoint:origin toView:nil]; NSRect point_rect = NSMakeRect(originInWindow.x, originInWindow.y, 0, 0); NSPoint originInScreen = [[view window] convertRectToScreen:point_rect].origin; originInScreen.y = originInScreen.y - size.height; return originInScreen; } - (NSSize)size { CefRect cef_rect = node_->AxLocation(); NSRect rect = NSMakeRect(cef_rect.x, cef_rect.y, cef_rect.width, cef_rect.height); NSView* view = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(node_->GetWindowHandle()); rect = [[view window] convertRectToScreen:rect]; return rect.size; } // // accessibility protocol // // attributes - (BOOL)accessibilityIsIgnored { return NO; } - (NSArray*)accessibilityAttributeNames { static NSArray* attributes = nil; if (attributes == nil) { attributes = [[NSArray alloc] initWithObjects:NSAccessibilityRoleAttribute, NSAccessibilityRoleDescriptionAttribute, NSAccessibilityChildrenAttribute, NSAccessibilityValueAttribute, NSAccessibilityTitleAttribute, NSAccessibilityDescriptionAttribute, NSAccessibilityFocusedAttribute, NSAccessibilityParentAttribute, NSAccessibilityWindowAttribute, NSAccessibilityTopLevelUIElementAttribute, NSAccessibilityPositionAttribute, NSAccessibilitySizeAttribute, nil]; } return attributes; } - (id)accessibilityAttributeValue:(NSString*)attribute { NSObject* typed_parent = CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT(parent_); if (!node_) { return nil; } if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) { return [self axRole]; } else if ([attribute isEqualToString:NSAccessibilityRoleDescriptionAttribute]) { return NSAccessibilityRoleDescription([self axRole], nil); } else if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { // Just check if the app thinks we're focused. id focusedElement = [NSApp accessibilityAttributeValue:NSAccessibilityFocusedUIElementAttribute]; return [NSNumber numberWithBool:[focusedElement isEqual:self]]; } else if ([attribute isEqualToString:NSAccessibilityParentAttribute]) { return NSAccessibilityUnignoredAncestor(typed_parent); } else if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) { return NSAccessibilityUnignoredChildren([self getKids]); } else if ([attribute isEqualToString:NSAccessibilityWindowAttribute]) { // We're in the same window as our parent. return [typed_parent accessibilityAttributeValue:NSAccessibilityWindowAttribute]; } else if ([attribute isEqualToString:NSAccessibilityTopLevelUIElementAttribute]) { // We're in the same top level element as our parent. return [typed_parent accessibilityAttributeValue:NSAccessibilityTopLevelUIElementAttribute]; } else if ([attribute isEqualToString:NSAccessibilityPositionAttribute]) { return [NSValue valueWithPoint:[self position]]; } else if ([attribute isEqualToString:NSAccessibilitySizeAttribute]) { return [NSValue valueWithSize:[self size]]; } else if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) { return [self axDescription]; } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { return [self axValue]; } else if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) { return [self axName]; } return nil; } - (id)accessibilityHitTest:(NSPoint)point { return NSAccessibilityUnignoredAncestor(self); } - (NSArray*)accessibilityActionNames { return [NSArray arrayWithObject:NSAccessibilityPressAction]; } - (NSString*)accessibilityActionDescription:(NSString*)action { return NSAccessibilityActionDescription(action); } - (void)accessibilityPerformAction:(NSString*)action { if ([action isEqualToString:NSAccessibilityPressAction]) { // Do Click on Default action [self doMouseClick:MBT_LEFT]; } else if ([action isEqualToString:NSAccessibilityShowMenuAction]) { // Right click for Context Menu [self doMouseClick:MBT_RIGHT]; } } - (id)accessibilityFocusedUIElement { return NSAccessibilityUnignoredAncestor(self); } - (BOOL)accessibilityNotifiesWhenDestroyed { // Indicate that BrowserAccessibilityCocoa will post a notification when it's // destroyed (see -detach). This allows VoiceOver to do some internal things // more efficiently. return YES; } @end namespace client { void OsrAXNode::NotifyAccessibilityEvent(std::string event_type) const { NSView* view = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(GetWindowHandle()); if (event_type == "focus") { NSAccessibilityPostNotification( view, NSAccessibilityFocusedUIElementChangedNotification); } else if (event_type == "textChanged") { NSAccessibilityPostNotification(view, NSAccessibilityTitleChangedNotification); } else if (event_type == "valueChanged") { NSAccessibilityPostNotification(view, NSAccessibilityValueChangedNotification); } else if (event_type == "textSelectionChanged") { NSAccessibilityPostNotification(view, NSAccessibilityValueChangedNotification); } } void OsrAXNode::Destroy() { if (platform_accessibility_) { NSAccessibilityPostNotification( CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT(platform_accessibility_), NSAccessibilityUIElementDestroyedNotification); } delete this; } // Create and return NSAccessibility Implementation Object for Mac CefNativeAccessible* OsrAXNode::GetNativeAccessibleObject( client::OsrAXNode* parent) { if (!platform_accessibility_) { platform_accessibility_ = CAST_NSOBJECT_TO_CEF_NATIVE_ACCESSIBLE( [OsrAXNodeObject elementWithNode:this]); SetParent(parent); } return platform_accessibility_; } } // namespace client