// 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