| // Copyright 2014 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. |
| |
| #import "ui/accessibility/platform/ax_platform_node_mac.h" |
| |
| #import <Cocoa/Cocoa.h> |
| #include <stddef.h> |
| |
| #include "base/macros.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "ui/accessibility/ax_action_data.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| #include "ui/accessibility/platform/ax_platform_node.h" |
| #include "ui/accessibility/platform/ax_platform_node_delegate.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #import "ui/gfx/mac/coordinate_conversion.h" |
| #include "ui/strings/grit/ui_strings.h" |
| |
| namespace { |
| |
| using RoleMap = std::map<ui::AXRole, NSString*>; |
| using EventMap = std::map<ui::AXEvent, NSString*>; |
| using ActionList = std::vector<std::pair<ui::AXAction, NSString*>>; |
| |
| RoleMap BuildRoleMap() { |
| const RoleMap::value_type roles[] = { |
| {ui::AX_ROLE_ABBR, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_ALERT, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_ALERT_DIALOG, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_ANCHOR, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_ANNOTATION, NSAccessibilityUnknownRole}, |
| {ui::AX_ROLE_APPLICATION, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_ARTICLE, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_AUDIO, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_BANNER, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_BLOCKQUOTE, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_BUTTON, NSAccessibilityButtonRole}, |
| {ui::AX_ROLE_CANVAS, NSAccessibilityImageRole}, |
| {ui::AX_ROLE_CAPTION, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_CELL, @"AXCell"}, |
| {ui::AX_ROLE_CHECK_BOX, NSAccessibilityCheckBoxRole}, |
| {ui::AX_ROLE_COLOR_WELL, NSAccessibilityColorWellRole}, |
| {ui::AX_ROLE_COLUMN, NSAccessibilityColumnRole}, |
| {ui::AX_ROLE_COLUMN_HEADER, @"AXCell"}, |
| {ui::AX_ROLE_COMBO_BOX_GROUPING, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_COMBO_BOX_MENU_BUTTON, NSAccessibilityButtonRole}, |
| {ui::AX_ROLE_COMPLEMENTARY, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_CONTENT_INFO, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_DATE, @"AXDateField"}, |
| {ui::AX_ROLE_DATE_TIME, @"AXDateField"}, |
| {ui::AX_ROLE_DEFINITION, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_DESCRIPTION_LIST_DETAIL, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_DESCRIPTION_LIST, NSAccessibilityListRole}, |
| {ui::AX_ROLE_DESCRIPTION_LIST_TERM, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_DIALOG, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_DETAILS, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_DIRECTORY, NSAccessibilityListRole}, |
| // If Mac supports AXExpandedChanged event with |
| // NSAccessibilityDisclosureTriangleRole, We should update |
| // AX_ROLE_DISCLOSURE_TRIANGLE mapping to |
| // NSAccessibilityDisclosureTriangleRole. http://crbug.com/558324 |
| {ui::AX_ROLE_DISCLOSURE_TRIANGLE, NSAccessibilityButtonRole}, |
| {ui::AX_ROLE_DOCUMENT, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_EMBEDDED_OBJECT, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_FIGCAPTION, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_FIGURE, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_FOOTER, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_FORM, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_GENERIC_CONTAINER, NSAccessibilityGroupRole}, |
| // Should be NSAccessibilityGridRole but VoiceOver treating it like |
| // a list as of 10.12.6, so following WebKit and using table role: |
| {ui::AX_ROLE_GRID, NSAccessibilityTableRole}, // crbug.com/753925 |
| {ui::AX_ROLE_GROUP, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_HEADING, @"AXHeading"}, |
| {ui::AX_ROLE_IFRAME, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_IFRAME_PRESENTATIONAL, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_IGNORED, NSAccessibilityUnknownRole}, |
| {ui::AX_ROLE_IMAGE, NSAccessibilityImageRole}, |
| {ui::AX_ROLE_IMAGE_MAP, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_INPUT_TIME, @"AXTimeField"}, |
| {ui::AX_ROLE_LABEL_TEXT, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_LEGEND, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_LINE_BREAK, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_LINK, NSAccessibilityLinkRole}, |
| {ui::AX_ROLE_LIST, NSAccessibilityListRole}, |
| {ui::AX_ROLE_LIST_BOX, NSAccessibilityListRole}, |
| {ui::AX_ROLE_LIST_BOX_OPTION, NSAccessibilityStaticTextRole}, |
| {ui::AX_ROLE_LIST_ITEM, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_LIST_MARKER, @"AXListMarker"}, |
| {ui::AX_ROLE_LOG, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_MAIN, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_MARK, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_MARQUEE, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_MATH, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_MENU, NSAccessibilityMenuRole}, |
| {ui::AX_ROLE_MENU_BAR, NSAccessibilityMenuBarRole}, |
| {ui::AX_ROLE_MENU_BUTTON, NSAccessibilityButtonRole}, |
| {ui::AX_ROLE_MENU_ITEM, NSAccessibilityMenuItemRole}, |
| {ui::AX_ROLE_MENU_ITEM_CHECK_BOX, NSAccessibilityMenuItemRole}, |
| {ui::AX_ROLE_MENU_ITEM_RADIO, NSAccessibilityMenuItemRole}, |
| {ui::AX_ROLE_MENU_LIST_OPTION, NSAccessibilityMenuItemRole}, |
| {ui::AX_ROLE_MENU_LIST_POPUP, NSAccessibilityUnknownRole}, |
| {ui::AX_ROLE_METER, NSAccessibilityProgressIndicatorRole}, |
| {ui::AX_ROLE_NAVIGATION, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_NONE, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_NOTE, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_PARAGRAPH, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_POP_UP_BUTTON, NSAccessibilityPopUpButtonRole}, |
| {ui::AX_ROLE_PRE, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_PRESENTATIONAL, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_PROGRESS_INDICATOR, NSAccessibilityProgressIndicatorRole}, |
| {ui::AX_ROLE_RADIO_BUTTON, NSAccessibilityRadioButtonRole}, |
| {ui::AX_ROLE_RADIO_GROUP, NSAccessibilityRadioGroupRole}, |
| {ui::AX_ROLE_REGION, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_ROOT_WEB_AREA, @"AXWebArea"}, |
| {ui::AX_ROLE_ROW, NSAccessibilityRowRole}, |
| {ui::AX_ROLE_ROW_HEADER, @"AXCell"}, |
| {ui::AX_ROLE_SCROLL_BAR, NSAccessibilityScrollBarRole}, |
| {ui::AX_ROLE_SEARCH, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_SEARCH_BOX, NSAccessibilityTextFieldRole}, |
| {ui::AX_ROLE_SLIDER, NSAccessibilitySliderRole}, |
| {ui::AX_ROLE_SLIDER_THUMB, NSAccessibilityValueIndicatorRole}, |
| {ui::AX_ROLE_SPIN_BUTTON, NSAccessibilityIncrementorRole}, |
| {ui::AX_ROLE_SPLITTER, NSAccessibilitySplitterRole}, |
| {ui::AX_ROLE_STATIC_TEXT, NSAccessibilityStaticTextRole}, |
| {ui::AX_ROLE_STATUS, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_SVG_ROOT, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_SWITCH, NSAccessibilityCheckBoxRole}, |
| {ui::AX_ROLE_TAB, NSAccessibilityRadioButtonRole}, |
| {ui::AX_ROLE_TABLE, NSAccessibilityTableRole}, |
| {ui::AX_ROLE_TABLE_HEADER_CONTAINER, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_TAB_LIST, NSAccessibilityTabGroupRole}, |
| {ui::AX_ROLE_TAB_PANEL, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_TERM, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_TEXT_FIELD, NSAccessibilityTextFieldRole}, |
| {ui::AX_ROLE_TEXT_FIELD_WITH_COMBO_BOX, NSAccessibilityComboBoxRole}, |
| {ui::AX_ROLE_TIME, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_TIMER, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_TOGGLE_BUTTON, NSAccessibilityCheckBoxRole}, |
| {ui::AX_ROLE_TOOLBAR, NSAccessibilityToolbarRole}, |
| {ui::AX_ROLE_TOOLTIP, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_TREE, NSAccessibilityOutlineRole}, |
| {ui::AX_ROLE_TREE_GRID, NSAccessibilityTableRole}, |
| {ui::AX_ROLE_TREE_ITEM, NSAccessibilityRowRole}, |
| {ui::AX_ROLE_VIDEO, NSAccessibilityGroupRole}, |
| {ui::AX_ROLE_WEB_AREA, @"AXWebArea"}, |
| {ui::AX_ROLE_WINDOW, NSAccessibilityWindowRole}, |
| }; |
| |
| return RoleMap(begin(roles), end(roles)); |
| } |
| |
| RoleMap BuildSubroleMap() { |
| const RoleMap::value_type subroles[] = { |
| {ui::AX_ROLE_ALERT, @"AXApplicationAlert"}, |
| {ui::AX_ROLE_ALERT_DIALOG, @"AXApplicationAlertDialog"}, |
| {ui::AX_ROLE_APPLICATION, @"AXLandmarkApplication"}, |
| {ui::AX_ROLE_ARTICLE, @"AXDocumentArticle"}, |
| {ui::AX_ROLE_BANNER, @"AXLandmarkBanner"}, |
| {ui::AX_ROLE_COMPLEMENTARY, @"AXLandmarkComplementary"}, |
| {ui::AX_ROLE_CONTENT_INFO, @"AXLandmarkContentInfo"}, |
| {ui::AX_ROLE_DEFINITION, @"AXDefinition"}, |
| {ui::AX_ROLE_DESCRIPTION_LIST_DETAIL, @"AXDefinition"}, |
| {ui::AX_ROLE_DESCRIPTION_LIST_TERM, @"AXTerm"}, |
| {ui::AX_ROLE_DIALOG, @"AXApplicationDialog"}, |
| {ui::AX_ROLE_DOCUMENT, @"AXDocument"}, |
| {ui::AX_ROLE_FOOTER, @"AXLandmarkContentInfo"}, |
| {ui::AX_ROLE_FORM, @"AXLandmarkForm"}, |
| {ui::AX_ROLE_LOG, @"AXApplicationLog"}, |
| {ui::AX_ROLE_MAIN, @"AXLandmarkMain"}, |
| {ui::AX_ROLE_MARQUEE, @"AXApplicationMarquee"}, |
| {ui::AX_ROLE_MATH, @"AXDocumentMath"}, |
| {ui::AX_ROLE_NAVIGATION, @"AXLandmarkNavigation"}, |
| {ui::AX_ROLE_NOTE, @"AXDocumentNote"}, |
| {ui::AX_ROLE_REGION, @"AXDocumentRegion"}, |
| {ui::AX_ROLE_SEARCH, @"AXLandmarkSearch"}, |
| {ui::AX_ROLE_SEARCH_BOX, @"AXSearchField"}, |
| {ui::AX_ROLE_STATUS, @"AXApplicationStatus"}, |
| {ui::AX_ROLE_SWITCH, @"AXSwitch"}, |
| {ui::AX_ROLE_TAB_PANEL, @"AXTabPanel"}, |
| {ui::AX_ROLE_TERM, @"AXTerm"}, |
| {ui::AX_ROLE_TIMER, @"AXApplicationTimer"}, |
| {ui::AX_ROLE_TOGGLE_BUTTON, @"AXToggleButton"}, |
| {ui::AX_ROLE_TOOLTIP, @"AXUserInterfaceTooltip"}, |
| {ui::AX_ROLE_TREE_ITEM, NSAccessibilityOutlineRowSubrole}, |
| }; |
| |
| return RoleMap(begin(subroles), end(subroles)); |
| } |
| |
| EventMap BuildEventMap() { |
| const EventMap::value_type events[] = { |
| {ui::AX_EVENT_FOCUS, NSAccessibilityFocusedUIElementChangedNotification}, |
| {ui::AX_EVENT_TEXT_CHANGED, NSAccessibilityTitleChangedNotification}, |
| {ui::AX_EVENT_VALUE_CHANGED, NSAccessibilityValueChangedNotification}, |
| {ui::AX_EVENT_TEXT_SELECTION_CHANGED, |
| NSAccessibilitySelectedTextChangedNotification}, |
| // TODO(patricialor): Add more events. |
| }; |
| |
| return EventMap(begin(events), end(events)); |
| } |
| |
| ActionList BuildActionList() { |
| const ActionList::value_type entries[] = { |
| // NSAccessibilityPressAction must come first in this list. |
| {ui::AX_ACTION_DO_DEFAULT, NSAccessibilityPressAction}, |
| |
| {ui::AX_ACTION_DECREMENT, NSAccessibilityDecrementAction}, |
| {ui::AX_ACTION_INCREMENT, NSAccessibilityIncrementAction}, |
| {ui::AX_ACTION_SHOW_CONTEXT_MENU, NSAccessibilityShowMenuAction}, |
| }; |
| return ActionList(begin(entries), end(entries)); |
| } |
| |
| const ActionList& GetActionList() { |
| CR_DEFINE_STATIC_LOCAL(const ActionList, action_map, (BuildActionList())); |
| return action_map; |
| } |
| |
| void NotifyMacEvent(AXPlatformNodeCocoa* target, ui::AXEvent event_type) { |
| NSAccessibilityPostNotification( |
| target, [AXPlatformNodeCocoa nativeNotificationFromAXEvent:event_type]); |
| } |
| |
| // Returns true if |action| should be added implicitly for |data|. |
| bool HasImplicitAction(const ui::AXNodeData& data, ui::AXAction action) { |
| return action == ui::AX_ACTION_DO_DEFAULT && ui::IsRoleClickable(data.role); |
| } |
| |
| // For roles that show a menu for the default action, ensure "show menu" also |
| // appears in available actions, but only if that's not already used for a |
| // context menu. It will be mapped back to the default action when performed. |
| bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) { |
| return HasImplicitAction(data, ui::AX_ACTION_DO_DEFAULT) && |
| !data.HasAction(ui::AX_ACTION_SHOW_CONTEXT_MENU) && |
| data.role == ui::AX_ROLE_POP_UP_BUTTON; |
| } |
| |
| } // namespace |
| |
| @interface AXPlatformNodeCocoa () |
| // Helper function for string attributes that don't require extra processing. |
| - (NSString*)getStringAttribute:(ui::AXStringAttribute)attribute; |
| // Returns AXValue, or nil if AXValue isn't an NSString. |
| - (NSString*)getAXValueAsString; |
| @end |
| |
| @implementation AXPlatformNodeCocoa { |
| ui::AXPlatformNodeBase* node_; // Weak. Retains us. |
| } |
| |
| @synthesize node = node_; |
| |
| + (NSString*)nativeRoleFromAXRole:(ui::AXRole)role { |
| CR_DEFINE_STATIC_LOCAL(const RoleMap, role_map, (BuildRoleMap())); |
| RoleMap::const_iterator it = role_map.find(role); |
| return it != role_map.end() ? it->second : NSAccessibilityUnknownRole; |
| } |
| |
| + (NSString*)nativeSubroleFromAXRole:(ui::AXRole)role { |
| CR_DEFINE_STATIC_LOCAL(const RoleMap, subrole_map, (BuildSubroleMap())); |
| RoleMap::const_iterator it = subrole_map.find(role); |
| return it != subrole_map.end() ? it->second : nil; |
| } |
| |
| + (NSString*)nativeNotificationFromAXEvent:(ui::AXEvent)event { |
| CR_DEFINE_STATIC_LOCAL(const EventMap, event_map, (BuildEventMap())); |
| EventMap::const_iterator it = event_map.find(event); |
| return it != event_map.end() ? it->second : nil; |
| } |
| |
| - (instancetype)initWithNode:(ui::AXPlatformNodeBase*)node { |
| if ((self = [super init])) { |
| node_ = node; |
| } |
| return self; |
| } |
| |
| - (void)detach { |
| if (!node_) |
| return; |
| NSAccessibilityPostNotification( |
| self, NSAccessibilityUIElementDestroyedNotification); |
| node_ = nil; |
| } |
| |
| - (NSRect)boundsInScreen { |
| if (!node_) |
| return NSZeroRect; |
| return gfx::ScreenRectToNSRect(node_->GetBoundsInScreen()); |
| } |
| |
| - (NSString*)getStringAttribute:(ui::AXStringAttribute)attribute { |
| std::string attributeValue; |
| if (node_->GetStringAttribute(attribute, &attributeValue)) |
| return base::SysUTF8ToNSString(attributeValue); |
| return nil; |
| } |
| |
| - (NSString*)getAXValueAsString { |
| id value = [self AXValue]; |
| return [value isKindOfClass:[NSString class]] ? value : nil; |
| } |
| |
| // NSAccessibility informal protocol implementation. |
| |
| - (BOOL)accessibilityIsIgnored { |
| if (!node_) |
| return YES; |
| |
| return [[self AXRole] isEqualToString:NSAccessibilityUnknownRole] || |
| node_->GetData().HasState(ui::AX_STATE_INVISIBLE); |
| } |
| |
| - (id)accessibilityHitTest:(NSPoint)point { |
| for (AXPlatformNodeCocoa* child in [self AXChildren]) { |
| if (![child accessibilityIsIgnored] && |
| NSPointInRect(point, child.boundsInScreen)) { |
| return [child accessibilityHitTest:point]; |
| } |
| } |
| return NSAccessibilityUnignoredAncestor(self); |
| } |
| |
| - (BOOL)accessibilityNotifiesWhenDestroyed { |
| return YES; |
| } |
| |
| - (id)accessibilityFocusedUIElement { |
| return node_ ? node_->GetDelegate()->GetFocus() : nil; |
| } |
| |
| - (NSArray*)accessibilityActionNames { |
| if (!node_) |
| return @[]; |
| |
| base::scoped_nsobject<NSMutableArray> axActions( |
| [[NSMutableArray alloc] init]); |
| |
| const ui::AXNodeData& data = node_->GetData(); |
| const ActionList& action_list = GetActionList(); |
| |
| // VoiceOver expects the "press" action to be first. Note that some roles |
| // should be given a press action implicitly. |
| DCHECK([action_list[0].second isEqualToString:NSAccessibilityPressAction]); |
| for (const auto item : action_list) { |
| if (data.HasAction(item.first) || HasImplicitAction(data, item.first)) |
| [axActions addObject:item.second]; |
| } |
| |
| if (AlsoUseShowMenuActionForDefaultAction(data)) |
| [axActions addObject:NSAccessibilityShowMenuAction]; |
| |
| return axActions.autorelease(); |
| } |
| |
| - (void)accessibilityPerformAction:(NSString*)action { |
| // Actions are performed asynchronously, so it's always possible for an object |
| // to change its mind after previously reporting an action as available. |
| if (![[self accessibilityActionNames] containsObject:action]) |
| return; |
| |
| ui::AXActionData data; |
| if ([action isEqualToString:NSAccessibilityShowMenuAction] && |
| AlsoUseShowMenuActionForDefaultAction(node_->GetData())) { |
| data.action = ui::AX_ACTION_DO_DEFAULT; |
| } else { |
| for (const ActionList::value_type& entry : GetActionList()) { |
| if ([action isEqualToString:entry.second]) { |
| data.action = entry.first; |
| break; |
| } |
| } |
| } |
| |
| // Note ui::AX_ACTIONs which are just overwriting an accessibility attribute |
| // are already implemented in -accessibilitySetValue:forAttribute:, so ignore |
| // those here. |
| |
| if (data.action != ui::AX_ACTION_NONE) |
| node_->GetDelegate()->AccessibilityPerformAction(data); |
| } |
| |
| - (NSArray*)accessibilityAttributeNames { |
| if (!node_) |
| return @[]; |
| |
| // These attributes are required on all accessibility objects. |
| NSArray* const kAllRoleAttributes = @[ |
| NSAccessibilityChildrenAttribute, |
| NSAccessibilityParentAttribute, |
| NSAccessibilityPositionAttribute, |
| NSAccessibilityRoleAttribute, |
| NSAccessibilitySizeAttribute, |
| NSAccessibilitySubroleAttribute, |
| |
| // Title is required for most elements. Cocoa asks for the value even if it |
| // is omitted here, but won't present it to accessibility APIs without this. |
| NSAccessibilityTitleAttribute, |
| |
| // Attributes which are not required, but are general to all roles. |
| NSAccessibilityRoleDescriptionAttribute, |
| NSAccessibilityEnabledAttribute, |
| NSAccessibilityFocusedAttribute, |
| NSAccessibilityHelpAttribute, |
| NSAccessibilityTopLevelUIElementAttribute, |
| NSAccessibilityWindowAttribute, |
| ]; |
| |
| // Attributes required for user-editable controls. |
| NSArray* const kValueAttributes = @[ NSAccessibilityValueAttribute ]; |
| |
| // Attributes required for unprotected textfields and labels. |
| NSArray* const kUnprotectedTextAttributes = @[ |
| NSAccessibilityInsertionPointLineNumberAttribute, |
| NSAccessibilityNumberOfCharactersAttribute, |
| NSAccessibilitySelectedTextAttribute, |
| NSAccessibilitySelectedTextRangeAttribute, |
| NSAccessibilityVisibleCharacterRangeAttribute, |
| ]; |
| |
| // Required for all text, including protected textfields. |
| NSString* const kTextAttributes = NSAccessibilityPlaceholderValueAttribute; |
| |
| base::scoped_nsobject<NSMutableArray> axAttributes( |
| [[NSMutableArray alloc] init]); |
| |
| [axAttributes addObjectsFromArray:kAllRoleAttributes]; |
| |
| switch (node_->GetData().role) { |
| case ui::AX_ROLE_TEXT_FIELD: |
| case ui::AX_ROLE_TEXT_FIELD_WITH_COMBO_BOX: |
| case ui::AX_ROLE_STATIC_TEXT: |
| [axAttributes addObject:kTextAttributes]; |
| if (!node_->GetData().HasState(ui::AX_STATE_PROTECTED)) |
| [axAttributes addObjectsFromArray:kUnprotectedTextAttributes]; |
| // Fallthrough. |
| case ui::AX_ROLE_CHECK_BOX: |
| case ui::AX_ROLE_COMBO_BOX_MENU_BUTTON: |
| case ui::AX_ROLE_MENU_ITEM_CHECK_BOX: |
| case ui::AX_ROLE_MENU_ITEM_RADIO: |
| case ui::AX_ROLE_RADIO_BUTTON: |
| case ui::AX_ROLE_SEARCH_BOX: |
| case ui::AX_ROLE_SLIDER: |
| case ui::AX_ROLE_SLIDER_THUMB: |
| case ui::AX_ROLE_TOGGLE_BUTTON: |
| [axAttributes addObjectsFromArray:kValueAttributes]; |
| break; |
| // TODO(tapted): Add additional attributes based on role. |
| default: |
| break; |
| } |
| return axAttributes.autorelease(); |
| } |
| |
| - (NSArray*)accessibilityParameterizedAttributeNames { |
| if (!node_) |
| return @[]; |
| |
| static NSArray* const kSelectableTextAttributes = [@[ |
| NSAccessibilityLineForIndexParameterizedAttribute, |
| NSAccessibilityRangeForLineParameterizedAttribute, |
| NSAccessibilityStringForRangeParameterizedAttribute, |
| NSAccessibilityRangeForPositionParameterizedAttribute, |
| NSAccessibilityRangeForIndexParameterizedAttribute, |
| NSAccessibilityBoundsForRangeParameterizedAttribute, |
| NSAccessibilityRTFForRangeParameterizedAttribute, |
| NSAccessibilityStyleRangeForIndexParameterizedAttribute, |
| NSAccessibilityAttributedStringForRangeParameterizedAttribute, |
| ] retain]; |
| |
| switch (node_->GetData().role) { |
| case ui::AX_ROLE_TEXT_FIELD: |
| case ui::AX_ROLE_STATIC_TEXT: |
| return kSelectableTextAttributes; |
| default: |
| break; |
| } |
| return nil; |
| } |
| |
| - (BOOL)accessibilityIsAttributeSettable:(NSString*)attributeName { |
| if (!node_) |
| return NO; |
| |
| const int restriction = |
| node_->GetData().GetIntAttribute(ui::AX_ATTR_RESTRICTION); |
| if (restriction == ui::AX_RESTRICTION_DISABLED) |
| return NO; |
| |
| // Allow certain attributes to be written via an accessibility client. A |
| // writable attribute will only appear as such if the accessibility element |
| // has a value set for that attribute. |
| if ([attributeName |
| isEqualToString:NSAccessibilitySelectedChildrenAttribute] || |
| [attributeName |
| isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) { |
| return NO; |
| } |
| |
| if ([attributeName isEqualToString:NSAccessibilityValueAttribute]) { |
| // Since tabs use the Radio Button role on Mac, the standard way to set |
| // them is via the value attribute rather than the selected attribute. |
| if (node_->GetData().role == ui::AX_ROLE_TAB) |
| return !node_->GetData().HasState(ui::AX_STATE_SELECTED); |
| |
| return restriction != ui::AX_RESTRICTION_READ_ONLY; |
| } |
| |
| // Readonly fields and selected text operations: |
| // - Selecting different text via NSAccessibilitySelectedTextRangeAttribute |
| // should work but it does not - see http://crbug.com/692362 . |
| // - Changing the actual text contents in the selection via |
| // NSAccessibilitySelectedTextAttribute is prevented, which is correct. |
| if ([attributeName isEqualToString:NSAccessibilitySelectedTextAttribute] || |
| [attributeName isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) |
| return restriction != ui::AX_RESTRICTION_READ_ONLY; |
| |
| if ([attributeName isEqualToString:NSAccessibilityFocusedAttribute]) { |
| return node_->GetData().HasState(ui::AX_STATE_FOCUSABLE); |
| } |
| |
| // TODO(patricialor): Add callbacks for updating the above attributes except |
| // NSAccessibilityValueAttribute and return YES. |
| return NO; |
| } |
| |
| - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { |
| if (!node_) |
| return; |
| |
| ui::AXActionData data; |
| |
| // Check for attributes first. Only the |data.action| should be set here - any |
| // type-specific information, if needed, should be set below. |
| if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { |
| data.action = node_->GetData().role == ui::AX_ROLE_TAB |
| ? ui::AX_ACTION_SET_SELECTION |
| : ui::AX_ACTION_SET_VALUE; |
| } else if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) { |
| data.action = ui::AX_ACTION_REPLACE_SELECTED_TEXT; |
| } else if ([attribute |
| isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) { |
| data.action = ui::AX_ACTION_SET_SELECTION; |
| } else if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { |
| if ([value isKindOfClass:[NSNumber class]]) { |
| data.action = |
| [value boolValue] ? ui::AX_ACTION_FOCUS : ui::AX_ACTION_BLUR; |
| } |
| } |
| |
| // Set type-specific information as necessary for actions set above. |
| if ([value isKindOfClass:[NSString class]]) { |
| data.value = base::SysNSStringToUTF16(value); |
| } else if (data.action == ui::AX_ACTION_SET_SELECTION && |
| [value isKindOfClass:[NSValue class]]) { |
| NSRange range = [value rangeValue]; |
| data.anchor_offset = range.location; |
| data.focus_offset = NSMaxRange(range); |
| } |
| |
| if (data.action != ui::AX_ACTION_NONE) |
| node_->GetDelegate()->AccessibilityPerformAction(data); |
| |
| // TODO(patricialor): Plumb through all the other writable attributes as |
| // specified in accessibilityIsAttributeSettable. |
| } |
| |
| - (id)accessibilityAttributeValue:(NSString*)attribute { |
| if (!node_) |
| return nil; // Return nil when detached. Even for AXRole. |
| |
| SEL selector = NSSelectorFromString(attribute); |
| if ([self respondsToSelector:selector]) |
| return [self performSelector:selector]; |
| return nil; |
| } |
| |
| - (id)accessibilityAttributeValue:(NSString*)attribute |
| forParameter:(id)parameter { |
| if (!node_) |
| return nil; |
| |
| SEL selector = NSSelectorFromString([attribute stringByAppendingString:@":"]); |
| if ([self respondsToSelector:selector]) |
| return [self performSelector:selector withObject:parameter]; |
| return nil; |
| } |
| |
| // NSAccessibility attributes. Order them according to |
| // NSAccessibilityConstants.h, or see https://crbug.com/678898. |
| |
| - (NSString*)AXRole { |
| if (!node_) |
| return nil; |
| |
| return [[self class] nativeRoleFromAXRole:node_->GetData().role]; |
| } |
| |
| - (NSString*)AXRoleDescription { |
| switch (node_->GetData().role) { |
| case ui::AX_ROLE_TAB: |
| // There is no NSAccessibilityTabRole or similar (AXRadioButton is used |
| // instead). Do the same as NSTabView and put "tab" in the description. |
| return [l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB_ROLE_DESCRIPTION) |
| lowercaseString]; |
| case ui::AX_ROLE_DISCLOSURE_TRIANGLE: |
| return [l10n_util::GetNSStringWithFixup( |
| IDS_ACCNAME_DISCLOSURE_TRIANGLE_ROLE_DESCRIPTION) lowercaseString]; |
| default: |
| break; |
| } |
| return NSAccessibilityRoleDescription([self AXRole], [self AXSubrole]); |
| } |
| |
| - (NSString*)AXSubrole { |
| ui::AXRole role = node_->GetData().role; |
| switch (role) { |
| case ui::AX_ROLE_TEXT_FIELD: |
| if (node_->GetData().HasState(ui::AX_STATE_PROTECTED)) |
| return NSAccessibilitySecureTextFieldSubrole; |
| break; |
| default: |
| break; |
| } |
| return [AXPlatformNodeCocoa nativeSubroleFromAXRole:role]; |
| } |
| |
| - (NSString*)AXHelp { |
| // TODO(aleventhal) Key shortcuts attribute should eventually get |
| // its own field. Follow what WebKit does for aria-keyshortcuts, see |
| // https://bugs.webkit.org/show_bug.cgi?id=159215 (WebKit bug). |
| NSString* desc = [self getStringAttribute:ui::AX_ATTR_DESCRIPTION]; |
| NSString* key = [self getStringAttribute:ui::AX_ATTR_KEY_SHORTCUTS]; |
| if (!desc.length) |
| return key.length ? key : @""; |
| if (!key.length) |
| return desc; |
| return [NSString stringWithFormat:@"%@ %@", desc, key]; |
| } |
| |
| - (id)AXValue { |
| ui::AXRole role = node_->GetData().role; |
| if (role == ui::AX_ROLE_TAB) |
| return [self AXSelected]; |
| |
| if (ui::IsNameExposedInAXValueForRole(role)) |
| return [self getStringAttribute:ui::AX_ATTR_NAME]; |
| |
| return [self getStringAttribute:ui::AX_ATTR_VALUE]; |
| } |
| |
| - (NSNumber*)AXEnabled { |
| return @(node_->GetData().GetIntAttribute(ui::AX_ATTR_RESTRICTION) != |
| ui::AX_RESTRICTION_DISABLED); |
| } |
| |
| - (NSNumber*)AXFocused { |
| if (node_->GetData().HasState(ui::AX_STATE_FOCUSABLE)) |
| return |
| @(node_->GetDelegate()->GetFocus() == node_->GetNativeViewAccessible()); |
| return @NO; |
| } |
| |
| - (id)AXParent { |
| if (!node_) |
| return nil; |
| return NSAccessibilityUnignoredAncestor(node_->GetParent()); |
| } |
| |
| - (NSArray*)AXChildren { |
| if (!node_) |
| return @[]; |
| |
| int count = node_->GetChildCount(); |
| NSMutableArray* children = [NSMutableArray arrayWithCapacity:count]; |
| for (int i = 0; i < count; ++i) |
| [children addObject:node_->ChildAtIndex(i)]; |
| return NSAccessibilityUnignoredChildren(children); |
| } |
| |
| - (id)AXWindow { |
| return node_->GetDelegate()->GetTopLevelWidget(); |
| } |
| |
| - (id)AXTopLevelUIElement { |
| return [self AXWindow]; |
| } |
| |
| - (NSValue*)AXPosition { |
| return [NSValue valueWithPoint:self.boundsInScreen.origin]; |
| } |
| |
| - (NSValue*)AXSize { |
| return [NSValue valueWithSize:self.boundsInScreen.size]; |
| } |
| |
| - (NSString*)AXTitle { |
| if (ui::IsNameExposedInAXValueForRole(node_->GetData().role)) |
| return @""; |
| |
| return [self getStringAttribute:ui::AX_ATTR_NAME]; |
| } |
| |
| // Misc attributes. |
| |
| - (NSNumber*)AXSelected { |
| return @(node_->GetData().HasState(ui::AX_STATE_SELECTED)); |
| } |
| |
| - (NSString*)AXPlaceholderValue { |
| return [self getStringAttribute:ui::AX_ATTR_PLACEHOLDER]; |
| } |
| |
| // Text-specific attributes. |
| |
| - (NSString*)AXSelectedText { |
| NSRange selectedTextRange; |
| [[self AXSelectedTextRange] getValue:&selectedTextRange]; |
| return [[self getAXValueAsString] substringWithRange:selectedTextRange]; |
| } |
| |
| - (NSValue*)AXSelectedTextRange { |
| // Selection might not be supported. Return (NSRange){0,0} in that case. |
| int start = 0, end = 0; |
| node_->GetIntAttribute(ui::AX_ATTR_TEXT_SEL_START, &start); |
| node_->GetIntAttribute(ui::AX_ATTR_TEXT_SEL_END, &end); |
| |
| // NSRange cannot represent the direction the text was selected in. |
| return [NSValue valueWithRange:{std::min(start, end), abs(end - start)}]; |
| } |
| |
| - (NSNumber*)AXNumberOfCharacters { |
| return @([[self getAXValueAsString] length]); |
| } |
| |
| - (NSValue*)AXVisibleCharacterRange { |
| return [NSValue valueWithRange:{0, [[self getAXValueAsString] length]}]; |
| } |
| |
| - (NSNumber*)AXInsertionPointLineNumber { |
| // Multiline is not supported on views. |
| return @0; |
| } |
| |
| // Parameterized text-specific attributes. |
| |
| - (id)AXLineForIndex:(id)parameter { |
| DCHECK([parameter isKindOfClass:[NSNumber class]]); |
| // Multiline is not supported on views. |
| return @0; |
| } |
| |
| - (id)AXRangeForLine:(id)parameter { |
| DCHECK([parameter isKindOfClass:[NSNumber class]]); |
| DCHECK_EQ(0, [parameter intValue]); |
| return [NSValue valueWithRange:{0, [[self getAXValueAsString] length]}]; |
| } |
| |
| - (id)AXStringForRange:(id)parameter { |
| DCHECK([parameter isKindOfClass:[NSValue class]]); |
| return [[self getAXValueAsString] substringWithRange:[parameter rangeValue]]; |
| } |
| |
| - (id)AXRangeForPosition:(id)parameter { |
| DCHECK([parameter isKindOfClass:[NSValue class]]); |
| // TODO(tapted): Hit-test [parameter pointValue] and return an NSRange. |
| NOTIMPLEMENTED(); |
| return nil; |
| } |
| |
| - (id)AXRangeForIndex:(id)parameter { |
| DCHECK([parameter isKindOfClass:[NSNumber class]]); |
| NOTIMPLEMENTED(); |
| return nil; |
| } |
| |
| - (id)AXBoundsForRange:(id)parameter { |
| DCHECK([parameter isKindOfClass:[NSValue class]]); |
| // TODO(tapted): Provide an accessor on AXPlatformNodeDelegate to obtain this |
| // from ui::TextInputClient::GetCompositionCharacterBounds(). |
| NOTIMPLEMENTED(); |
| return nil; |
| } |
| |
| - (id)AXRTFForRange:(id)parameter { |
| DCHECK([parameter isKindOfClass:[NSValue class]]); |
| NOTIMPLEMENTED(); |
| return nil; |
| } |
| |
| - (id)AXStyleRangeForIndex:(id)parameter { |
| DCHECK([parameter isKindOfClass:[NSNumber class]]); |
| NOTIMPLEMENTED(); |
| return nil; |
| } |
| |
| - (id)AXAttributedStringForRange:(id)parameter { |
| DCHECK([parameter isKindOfClass:[NSValue class]]); |
| base::scoped_nsobject<NSAttributedString> attributedString( |
| [[NSAttributedString alloc] |
| initWithString:[self AXStringForRange:parameter]]); |
| // TODO(tapted): views::WordLookupClient has a way to obtain the actual |
| // decorations, and BridgedContentView has a conversion function that creates |
| // an NSAttributedString. Refactor things so they can be used here. |
| return attributedString.autorelease(); |
| } |
| |
| @end |
| |
| namespace ui { |
| |
| // static |
| AXPlatformNode* AXPlatformNode::Create(AXPlatformNodeDelegate* delegate) { |
| AXPlatformNodeBase* node = new AXPlatformNodeMac(); |
| node->Init(delegate); |
| return node; |
| } |
| |
| // static |
| AXPlatformNode* AXPlatformNode::FromNativeViewAccessible( |
| gfx::NativeViewAccessible accessible) { |
| if ([accessible isKindOfClass:[AXPlatformNodeCocoa class]]) |
| return [accessible node]; |
| return nullptr; |
| } |
| |
| AXPlatformNodeMac::AXPlatformNodeMac() { |
| } |
| |
| AXPlatformNodeMac::~AXPlatformNodeMac() { |
| } |
| |
| void AXPlatformNodeMac::Destroy() { |
| if (native_node_) |
| [native_node_ detach]; |
| AXPlatformNodeBase::Destroy(); |
| } |
| |
| gfx::NativeViewAccessible AXPlatformNodeMac::GetNativeViewAccessible() { |
| if (!native_node_) |
| native_node_.reset([[AXPlatformNodeCocoa alloc] initWithNode:this]); |
| return native_node_.get(); |
| } |
| |
| void AXPlatformNodeMac::NotifyAccessibilityEvent(ui::AXEvent event_type) { |
| GetNativeViewAccessible(); |
| // Add mappings between ui::AXEvent and NSAccessibility notifications using |
| // the EventMap above. This switch contains exceptions to those mappings. |
| switch (event_type) { |
| case ui::AX_EVENT_TEXT_CHANGED: |
| // If the view is a user-editable textfield, this should change the value. |
| if (GetData().role == ui::AX_ROLE_TEXT_FIELD) { |
| NotifyMacEvent(native_node_, ui::AX_EVENT_VALUE_CHANGED); |
| return; |
| } |
| break; |
| default: |
| break; |
| } |
| NotifyMacEvent(native_node_, event_type); |
| } |
| |
| int AXPlatformNodeMac::GetIndexInParent() { |
| // TODO(dmazzoni): implement this. http://crbug.com/396137 |
| return -1; |
| } |
| |
| bool IsNameExposedInAXValueForRole(AXRole role) { |
| switch (role) { |
| case AX_ROLE_LIST_BOX_OPTION: |
| case AX_ROLE_LIST_MARKER: |
| case AX_ROLE_MENU_LIST_OPTION: |
| case AX_ROLE_STATIC_TEXT: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| } // namespace ui |