blob: 1d77e94ab4abf05cb550c5e7a44c661a23fb86e8 [file] [log] [blame]
// 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/mac/foundation_util.h"
#include "base/macros.h"
#include "base/no_destructor.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 {
// Same length as web content/WebKit.
static int kLiveRegionDebounceMillis = 20;
using RoleMap = std::map<ax::mojom::Role, NSString*>;
using EventMap = std::map<ax::mojom::Event, NSString*>;
using ActionList = std::vector<std::pair<ax::mojom::Action, NSString*>>;
struct AnnouncementSpec {
base::scoped_nsobject<NSString> announcement;
base::scoped_nsobject<NSWindow> window;
bool is_polite;
};
RoleMap BuildRoleMap() {
const RoleMap::value_type roles[] = {
{ax::mojom::Role::kAbbr, NSAccessibilityGroupRole},
{ax::mojom::Role::kAlert, NSAccessibilityGroupRole},
{ax::mojom::Role::kAlertDialog, NSAccessibilityGroupRole},
{ax::mojom::Role::kAnchor, NSAccessibilityGroupRole},
{ax::mojom::Role::kApplication, NSAccessibilityGroupRole},
{ax::mojom::Role::kArticle, NSAccessibilityGroupRole},
{ax::mojom::Role::kAudio, NSAccessibilityGroupRole},
{ax::mojom::Role::kBanner, NSAccessibilityGroupRole},
{ax::mojom::Role::kBlockquote, NSAccessibilityGroupRole},
{ax::mojom::Role::kButton, NSAccessibilityButtonRole},
{ax::mojom::Role::kCanvas, NSAccessibilityImageRole},
{ax::mojom::Role::kCaption, NSAccessibilityGroupRole},
{ax::mojom::Role::kCell, @"AXCell"},
{ax::mojom::Role::kCheckBox, NSAccessibilityCheckBoxRole},
{ax::mojom::Role::kCode, NSAccessibilityGroupRole},
{ax::mojom::Role::kColorWell, NSAccessibilityColorWellRole},
{ax::mojom::Role::kColumn, NSAccessibilityColumnRole},
{ax::mojom::Role::kColumnHeader, @"AXCell"},
{ax::mojom::Role::kComboBoxGrouping, NSAccessibilityGroupRole},
{ax::mojom::Role::kComboBoxMenuButton, NSAccessibilityPopUpButtonRole},
{ax::mojom::Role::kComment, NSAccessibilityGroupRole},
{ax::mojom::Role::kComplementary, NSAccessibilityGroupRole},
{ax::mojom::Role::kContentDeletion, NSAccessibilityGroupRole},
{ax::mojom::Role::kContentInsertion, NSAccessibilityGroupRole},
{ax::mojom::Role::kContentInfo, NSAccessibilityGroupRole},
{ax::mojom::Role::kDate, @"AXDateField"},
{ax::mojom::Role::kDateTime, @"AXDateField"},
{ax::mojom::Role::kDefinition, NSAccessibilityGroupRole},
{ax::mojom::Role::kDescriptionListDetail, NSAccessibilityGroupRole},
{ax::mojom::Role::kDescriptionList, NSAccessibilityListRole},
{ax::mojom::Role::kDescriptionListTerm, NSAccessibilityGroupRole},
{ax::mojom::Role::kDialog, NSAccessibilityGroupRole},
{ax::mojom::Role::kDetails, NSAccessibilityGroupRole},
{ax::mojom::Role::kDirectory, NSAccessibilityListRole},
// If Mac supports AXExpandedChanged event with
// NSAccessibilityDisclosureTriangleRole, We should update
// ax::mojom::Role::kDisclosureTriangle mapping to
// NSAccessibilityDisclosureTriangleRole. http://crbug.com/558324
{ax::mojom::Role::kDisclosureTriangle, NSAccessibilityButtonRole},
{ax::mojom::Role::kDocAbstract, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocAcknowledgments, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocAfterword, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocAppendix, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocBackLink, NSAccessibilityLinkRole},
{ax::mojom::Role::kDocBiblioEntry, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocBibliography, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocBiblioRef, NSAccessibilityLinkRole},
{ax::mojom::Role::kDocChapter, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocColophon, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocConclusion, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocCover, NSAccessibilityImageRole},
{ax::mojom::Role::kDocCredit, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocCredits, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocDedication, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocEndnote, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocEndnotes, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocEpigraph, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocEpilogue, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocErrata, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocExample, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocFootnote, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocForeword, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocGlossary, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocGlossRef, NSAccessibilityLinkRole},
{ax::mojom::Role::kDocIndex, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocIntroduction, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocNoteRef, NSAccessibilityLinkRole},
{ax::mojom::Role::kDocNotice, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocPageBreak, NSAccessibilitySplitterRole},
{ax::mojom::Role::kDocPageFooter, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocPageHeader, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocPageList, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocPart, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocPreface, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocPrologue, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocPullquote, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocQna, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocSubtitle, @"AXHeading"},
{ax::mojom::Role::kDocTip, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocToc, NSAccessibilityGroupRole},
{ax::mojom::Role::kDocument, NSAccessibilityGroupRole},
{ax::mojom::Role::kEmbeddedObject, NSAccessibilityGroupRole},
{ax::mojom::Role::kEmphasis, NSAccessibilityGroupRole},
{ax::mojom::Role::kFigcaption, NSAccessibilityGroupRole},
{ax::mojom::Role::kFigure, NSAccessibilityGroupRole},
{ax::mojom::Role::kFooter, NSAccessibilityGroupRole},
{ax::mojom::Role::kFooterAsNonLandmark, NSAccessibilityGroupRole},
{ax::mojom::Role::kForm, NSAccessibilityGroupRole},
{ax::mojom::Role::kGenericContainer, NSAccessibilityGroupRole},
{ax::mojom::Role::kGraphicsDocument, NSAccessibilityGroupRole},
{ax::mojom::Role::kGraphicsObject, NSAccessibilityGroupRole},
{ax::mojom::Role::kGraphicsSymbol, NSAccessibilityImageRole},
// Should be NSAccessibilityGridRole but VoiceOver treating it like
// a list as of 10.12.6, so following WebKit and using table role:
{ax::mojom::Role::kGrid, NSAccessibilityTableRole}, // crbug.com/753925
{ax::mojom::Role::kGroup, NSAccessibilityGroupRole},
{ax::mojom::Role::kHeader, NSAccessibilityGroupRole},
{ax::mojom::Role::kHeaderAsNonLandmark, NSAccessibilityGroupRole},
{ax::mojom::Role::kHeading, @"AXHeading"},
{ax::mojom::Role::kIframe, NSAccessibilityGroupRole},
{ax::mojom::Role::kIframePresentational, NSAccessibilityGroupRole},
{ax::mojom::Role::kIgnored, NSAccessibilityUnknownRole},
{ax::mojom::Role::kImage, NSAccessibilityImageRole},
{ax::mojom::Role::kImageMap, NSAccessibilityGroupRole},
{ax::mojom::Role::kInputTime, @"AXTimeField"},
{ax::mojom::Role::kLabelText, NSAccessibilityGroupRole},
{ax::mojom::Role::kLayoutTable, NSAccessibilityGroupRole},
{ax::mojom::Role::kLayoutTableCell, NSAccessibilityGroupRole},
{ax::mojom::Role::kLayoutTableRow, NSAccessibilityGroupRole},
{ax::mojom::Role::kLegend, NSAccessibilityGroupRole},
{ax::mojom::Role::kLineBreak, NSAccessibilityGroupRole},
{ax::mojom::Role::kLink, NSAccessibilityLinkRole},
{ax::mojom::Role::kList, NSAccessibilityListRole},
{ax::mojom::Role::kListBox, NSAccessibilityListRole},
{ax::mojom::Role::kListBoxOption, NSAccessibilityStaticTextRole},
{ax::mojom::Role::kListItem, NSAccessibilityGroupRole},
{ax::mojom::Role::kListMarker, @"AXListMarker"},
{ax::mojom::Role::kLog, NSAccessibilityGroupRole},
{ax::mojom::Role::kMain, NSAccessibilityGroupRole},
{ax::mojom::Role::kMark, NSAccessibilityGroupRole},
{ax::mojom::Role::kMarquee, NSAccessibilityGroupRole},
{ax::mojom::Role::kMath, NSAccessibilityGroupRole},
{ax::mojom::Role::kMenu, NSAccessibilityMenuRole},
{ax::mojom::Role::kMenuBar, NSAccessibilityMenuBarRole},
{ax::mojom::Role::kMenuItem, NSAccessibilityMenuItemRole},
{ax::mojom::Role::kMenuItemCheckBox, NSAccessibilityMenuItemRole},
{ax::mojom::Role::kMenuItemRadio, NSAccessibilityMenuItemRole},
{ax::mojom::Role::kMenuListOption, NSAccessibilityMenuItemRole},
{ax::mojom::Role::kMenuListPopup, NSAccessibilityMenuRole},
{ax::mojom::Role::kMeter, NSAccessibilityLevelIndicatorRole},
{ax::mojom::Role::kNavigation, NSAccessibilityGroupRole},
{ax::mojom::Role::kNone, NSAccessibilityGroupRole},
{ax::mojom::Role::kNote, NSAccessibilityGroupRole},
{ax::mojom::Role::kParagraph, NSAccessibilityGroupRole},
{ax::mojom::Role::kPdfActionableHighlight, NSAccessibilityButtonRole},
{ax::mojom::Role::kPluginObject, NSAccessibilityGroupRole},
{ax::mojom::Role::kPopUpButton, NSAccessibilityPopUpButtonRole},
{ax::mojom::Role::kPortal, NSAccessibilityButtonRole},
{ax::mojom::Role::kPre, NSAccessibilityGroupRole},
{ax::mojom::Role::kPresentational, NSAccessibilityGroupRole},
{ax::mojom::Role::kProgressIndicator,
NSAccessibilityProgressIndicatorRole},
{ax::mojom::Role::kRadioButton, NSAccessibilityRadioButtonRole},
{ax::mojom::Role::kRadioGroup, NSAccessibilityRadioGroupRole},
{ax::mojom::Role::kRegion, NSAccessibilityGroupRole},
{ax::mojom::Role::kRootWebArea, @"AXWebArea"},
{ax::mojom::Role::kRow, NSAccessibilityRowRole},
{ax::mojom::Role::kRowGroup, NSAccessibilityGroupRole},
{ax::mojom::Role::kRowHeader, @"AXCell"},
// TODO(accessibility) What should kRuby be? It's not listed? Any others
// missing? Maybe use switch statement so that compiler doesn't allow us
// to miss any.
{ax::mojom::Role::kRubyAnnotation, NSAccessibilityUnknownRole},
{ax::mojom::Role::kScrollBar, NSAccessibilityScrollBarRole},
{ax::mojom::Role::kSearch, NSAccessibilityGroupRole},
{ax::mojom::Role::kSearchBox, NSAccessibilityTextFieldRole},
{ax::mojom::Role::kSection, NSAccessibilityGroupRole},
{ax::mojom::Role::kSlider, NSAccessibilitySliderRole},
{ax::mojom::Role::kSliderThumb, NSAccessibilityValueIndicatorRole},
{ax::mojom::Role::kSpinButton, NSAccessibilityIncrementorRole},
{ax::mojom::Role::kSplitter, NSAccessibilitySplitterRole},
{ax::mojom::Role::kStaticText, NSAccessibilityStaticTextRole},
{ax::mojom::Role::kStatus, NSAccessibilityGroupRole},
{ax::mojom::Role::kSuggestion, NSAccessibilityGroupRole},
{ax::mojom::Role::kSvgRoot, NSAccessibilityGroupRole},
{ax::mojom::Role::kSwitch, NSAccessibilityCheckBoxRole},
{ax::mojom::Role::kStrong, NSAccessibilityGroupRole},
{ax::mojom::Role::kTab, NSAccessibilityRadioButtonRole},
{ax::mojom::Role::kTable, NSAccessibilityTableRole},
{ax::mojom::Role::kTableHeaderContainer, NSAccessibilityGroupRole},
{ax::mojom::Role::kTabList, NSAccessibilityTabGroupRole},
{ax::mojom::Role::kTabPanel, NSAccessibilityGroupRole},
{ax::mojom::Role::kTerm, NSAccessibilityGroupRole},
{ax::mojom::Role::kTextField, NSAccessibilityTextFieldRole},
{ax::mojom::Role::kTextFieldWithComboBox, NSAccessibilityComboBoxRole},
{ax::mojom::Role::kTime, NSAccessibilityGroupRole},
{ax::mojom::Role::kTimer, NSAccessibilityGroupRole},
{ax::mojom::Role::kTitleBar, NSAccessibilityStaticTextRole},
{ax::mojom::Role::kToggleButton, NSAccessibilityCheckBoxRole},
{ax::mojom::Role::kToolbar, NSAccessibilityToolbarRole},
{ax::mojom::Role::kTooltip, NSAccessibilityGroupRole},
{ax::mojom::Role::kTree, NSAccessibilityOutlineRole},
{ax::mojom::Role::kTreeGrid, NSAccessibilityTableRole},
{ax::mojom::Role::kTreeItem, NSAccessibilityRowRole},
{ax::mojom::Role::kVideo, NSAccessibilityGroupRole},
{ax::mojom::Role::kWebArea, @"AXWebArea"},
// Use the group role as the BrowserNativeWidgetWindow already provides
// a kWindow role, and having extra window roles, which are treated
// specially by screen readers, can break their ability to find the
// content window. See http://crbug.com/875843 for more information.
{ax::mojom::Role::kWindow, NSAccessibilityGroupRole},
};
return RoleMap(begin(roles), end(roles));
}
RoleMap BuildSubroleMap() {
const RoleMap::value_type subroles[] = {
{ax::mojom::Role::kAlert, @"AXApplicationAlert"},
{ax::mojom::Role::kAlertDialog, @"AXApplicationAlertDialog"},
{ax::mojom::Role::kApplication, @"AXLandmarkApplication"},
{ax::mojom::Role::kArticle, @"AXDocumentArticle"},
{ax::mojom::Role::kBanner, @"AXLandmarkBanner"},
{ax::mojom::Role::kCode, @"AXCodeStyleGroup"},
{ax::mojom::Role::kComplementary, @"AXLandmarkComplementary"},
{ax::mojom::Role::kContentDeletion, @"AXDeleteStyleGroup"},
{ax::mojom::Role::kContentInsertion, @"AXInsertStyleGroup"},
{ax::mojom::Role::kContentInfo, @"AXLandmarkContentInfo"},
{ax::mojom::Role::kDefinition, @"AXDefinition"},
{ax::mojom::Role::kDescriptionListDetail, @"AXDefinition"},
{ax::mojom::Role::kDescriptionListTerm, @"AXTerm"},
{ax::mojom::Role::kDialog, @"AXApplicationDialog"},
{ax::mojom::Role::kDocument, @"AXDocument"},
{ax::mojom::Role::kEmphasis, @"AXEmphasisStyleGroup"},
{ax::mojom::Role::kFooter, @"AXLandmarkContentInfo"},
{ax::mojom::Role::kForm, @"AXLandmarkForm"},
{ax::mojom::Role::kGraphicsDocument, @"AXDocument"},
{ax::mojom::Role::kHeader, @"AXLandmarkBanner"},
{ax::mojom::Role::kLog, @"AXApplicationLog"},
{ax::mojom::Role::kMain, @"AXLandmarkMain"},
{ax::mojom::Role::kMarquee, @"AXApplicationMarquee"},
{ax::mojom::Role::kMath, @"AXDocumentMath"},
{ax::mojom::Role::kNavigation, @"AXLandmarkNavigation"},
{ax::mojom::Role::kNote, @"AXDocumentNote"},
{ax::mojom::Role::kRegion, @"AXLandmarkRegion"},
{ax::mojom::Role::kSearch, @"AXLandmarkSearch"},
{ax::mojom::Role::kSearchBox, @"AXSearchField"},
{ax::mojom::Role::kSection, @"AXLandmarkRegion"},
{ax::mojom::Role::kStatus, @"AXApplicationStatus"},
{ax::mojom::Role::kStrong, @"AXStrongStyleGroup"},
{ax::mojom::Role::kSwitch, @"AXSwitch"},
{ax::mojom::Role::kTabPanel, @"AXTabPanel"},
{ax::mojom::Role::kTerm, @"AXTerm"},
{ax::mojom::Role::kTime, @"AXTimeGroup"},
{ax::mojom::Role::kTimer, @"AXApplicationTimer"},
{ax::mojom::Role::kToggleButton, @"AXToggleButton"},
{ax::mojom::Role::kTooltip, @"AXUserInterfaceTooltip"},
{ax::mojom::Role::kTreeItem, NSAccessibilityOutlineRowSubrole},
};
return RoleMap(begin(subroles), end(subroles));
}
EventMap BuildEventMap() {
const EventMap::value_type events[] = {
{ax::mojom::Event::kCheckedStateChanged,
NSAccessibilityValueChangedNotification},
{ax::mojom::Event::kFocus,
NSAccessibilityFocusedUIElementChangedNotification},
{ax::mojom::Event::kFocusContext,
NSAccessibilityFocusedUIElementChangedNotification},
{ax::mojom::Event::kTextChanged, NSAccessibilityTitleChangedNotification},
{ax::mojom::Event::kValueChanged,
NSAccessibilityValueChangedNotification},
{ax::mojom::Event::kTextSelectionChanged,
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.
{ax::mojom::Action::kDoDefault, NSAccessibilityPressAction},
{ax::mojom::Action::kDecrement, NSAccessibilityDecrementAction},
{ax::mojom::Action::kIncrement, NSAccessibilityIncrementAction},
{ax::mojom::Action::kShowContextMenu, NSAccessibilityShowMenuAction},
};
return ActionList(begin(entries), end(entries));
}
const ActionList& GetActionList() {
static const base::NoDestructor<ActionList> action_map(BuildActionList());
return *action_map;
}
void PostAnnouncementNotification(NSString* announcement,
NSWindow* window,
bool is_polite) {
NSAccessibilityPriorityLevel priority =
is_polite ? NSAccessibilityPriorityMedium : NSAccessibilityPriorityHigh;
NSDictionary* notification_info = @{
NSAccessibilityAnnouncementKey : announcement,
NSAccessibilityPriorityKey : @(priority)
};
// On Mojave, announcements from an inactive window aren't spoken.
NSAccessibilityPostNotificationWithUserInfo(
window, NSAccessibilityAnnouncementRequestedNotification,
notification_info);
}
void NotifyMacEvent(AXPlatformNodeCocoa* target, ax::mojom::Event event_type) {
NSString* notification =
[AXPlatformNodeCocoa nativeNotificationFromAXEvent:event_type];
if (notification)
NSAccessibilityPostNotification(target, notification);
}
// Returns true if |action| should be added implicitly for |data|.
bool HasImplicitAction(const ui::AXNodeData& data, ax::mojom::Action action) {
return action == ax::mojom::Action::kDoDefault && data.IsClickable();
}
// 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, ax::mojom::Action::kDoDefault) &&
!data.HasAction(ax::mojom::Action::kShowContextMenu) &&
data.role == ax::mojom::Role::kPopUpButton;
}
} // namespace
@interface AXPlatformNodeCocoa (Private)
// Helper function for string attributes that don't require extra processing.
- (NSString*)getStringAttribute:(ax::mojom::StringAttribute)attribute;
// Returns AXValue, or nil if AXValue isn't an NSString.
- (NSString*)getAXValueAsString;
// Returns the data necessary to queue an NSAccessibility announcement if
// |eventType| should be announced, or nullptr otherwise.
- (std::unique_ptr<AnnouncementSpec>)announcementForEvent:
(ax::mojom::Event)eventType;
// Ask the system to announce |announcementText|. This is debounced to happen
// at most every |kLiveRegionDebounceMillis| per node, with only the most
// recent announcement text read, to account for situations with multiple
// notifications happening one after another (for example, results for
// find-in-page updating rapidly as they come in from subframes).
- (void)scheduleLiveRegionAnnouncement:
(std::unique_ptr<AnnouncementSpec>)announcement;
@end
@implementation AXPlatformNodeCocoa {
ui::AXPlatformNodeBase* _node; // Weak. Retains us.
std::unique_ptr<AnnouncementSpec> _pendingAnnouncement;
}
@synthesize node = _node;
+ (NSString*)nativeRoleFromAXRole:(ax::mojom::Role)role {
static const base::NoDestructor<RoleMap> role_map(BuildRoleMap());
RoleMap::const_iterator it = role_map->find(role);
return it != role_map->end() ? it->second : NSAccessibilityUnknownRole;
}
+ (NSString*)nativeSubroleFromAXRole:(ax::mojom::Role)role {
static const base::NoDestructor<RoleMap> subrole_map(BuildSubroleMap());
RoleMap::const_iterator it = subrole_map->find(role);
return it != subrole_map->end() ? it->second : nil;
}
+ (NSString*)nativeNotificationFromAXEvent:(ax::mojom::Event)event {
static const base::NoDestructor<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;
_node = nil;
NSAccessibilityPostNotification(
self, NSAccessibilityUIElementDestroyedNotification);
}
- (NSRect)boundsInScreen {
if (!_node || !_node->GetDelegate())
return NSZeroRect;
return gfx::ScreenRectToNSRect(_node->GetDelegate()->GetBoundsRect(
ui::AXCoordinateSystem::kScreenDIPs, ui::AXClippingBehavior::kClipped));
}
- (NSString*)getStringAttribute:(ax::mojom::StringAttribute)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;
}
- (NSString*)getName {
return base::SysUTF8ToNSString(_node->GetName());
}
- (std::unique_ptr<AnnouncementSpec>)announcementForEvent:
(ax::mojom::Event)eventType {
// Only alerts and live region changes should be announced.
DCHECK(eventType == ax::mojom::Event::kAlert ||
eventType == ax::mojom::Event::kLiveRegionChanged);
std::string liveStatus =
_node->GetStringAttribute(ax::mojom::StringAttribute::kLiveStatus);
// If live status is explicitly set to off, don't announce.
if (liveStatus == "off")
return nullptr;
NSString* name = [self getName];
NSString* announcementText =
[name length] > 0 ? name
: base::SysUTF16ToNSString(_node->GetInnerText());
if ([announcementText length] == 0)
return nullptr;
auto announcement = std::make_unique<AnnouncementSpec>();
announcement->announcement =
base::scoped_nsobject<NSString>([announcementText retain]);
announcement->window =
base::scoped_nsobject<NSWindow>([[self AXWindow] retain]);
announcement->is_polite = liveStatus != "assertive";
return announcement;
}
- (void)scheduleLiveRegionAnnouncement:
(std::unique_ptr<AnnouncementSpec>)announcement {
if (_pendingAnnouncement) {
// An announcement is already in flight, so just reset the contents. This is
// threadsafe because the dispatch is on the main queue.
_pendingAnnouncement = std::move(announcement);
return;
}
_pendingAnnouncement = std::move(announcement);
dispatch_after(kLiveRegionDebounceMillis * NSEC_PER_MSEC,
dispatch_get_main_queue(), ^{
if (!_pendingAnnouncement) {
return;
}
PostAnnouncementNotification(
_pendingAnnouncement->announcement,
_pendingAnnouncement->window,
_pendingAnnouncement->is_polite);
_pendingAnnouncement.reset();
});
}
// NSAccessibility informal protocol implementation.
- (BOOL)accessibilityIsIgnored {
if (!_node)
return YES;
return [[self AXRole] isEqualToString:NSAccessibilityUnknownRole] ||
_node->IsInvisibleOrIgnored();
}
- (id)accessibilityHitTest:(NSPoint)point {
if (!NSPointInRect(point, [self boundsInScreen]))
return nil;
for (id child in [[self AXChildren] reverseObjectEnumerator]) {
if (!NSPointInRect(point, [child accessibilityFrame]))
continue;
if (id foundChild = [child accessibilityHitTest:point])
return foundChild;
}
// Hit self, but not any child.
return NSAccessibilityUnignoredAncestor(self);
}
- (BOOL)accessibilityNotifiesWhenDestroyed {
return YES;
}
- (id)accessibilityFocusedUIElement {
return _node ? _node->GetDelegate()->GetFocus() : nil;
}
// This function and accessibilityPerformAction:, while deprecated, are a) still
// called by AppKit internally and b) not implemented by NSAccessibilityElement,
// so this class needs its own implementations.
- (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 = ax::mojom::Action::kDoDefault;
} 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 != ax::mojom::Action::kNone)
_node->GetDelegate()->AccessibilityPerformAction(data);
}
// This method, while deprecated, is still called internally by AppKit.
- (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 ax::mojom::Role::kTextField:
case ax::mojom::Role::kTextFieldWithComboBox:
case ax::mojom::Role::kStaticText:
[axAttributes addObject:kTextAttributes];
if (!_node->GetData().HasState(ax::mojom::State::kProtected))
[axAttributes addObjectsFromArray:kUnprotectedTextAttributes];
FALLTHROUGH;
case ax::mojom::Role::kCheckBox:
case ax::mojom::Role::kComboBoxMenuButton:
case ax::mojom::Role::kMenuItemCheckBox:
case ax::mojom::Role::kMenuItemRadio:
case ax::mojom::Role::kRadioButton:
case ax::mojom::Role::kSearchBox:
case ax::mojom::Role::kSlider:
case ax::mojom::Role::kSliderThumb:
case ax::mojom::Role::kToggleButton:
[axAttributes addObjectsFromArray:kValueAttributes];
break;
// TODO(tapted): Add additional attributes based on role.
default:
break;
}
if (_node->GetData().HasBoolAttribute(ax::mojom::BoolAttribute::kSelected)) {
[axAttributes addObjectsFromArray:@[ NSAccessibilitySelectedAttribute ]];
}
if (ui::IsMenuItem(_node->GetData().role)) {
[axAttributes addObjectsFromArray:@[ @"AXMenuItemMarkChar" ]];
}
return axAttributes.autorelease();
}
// Despite it being deprecated, AppKit internally calls this function sometimes
// in unclear circumstances. It is implemented in terms of the new a11y API
// here.
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
if (!_node)
return;
if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
[self setAccessibilityValue:value];
} else if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) {
[self setAccessibilitySelectedText:base::mac::ObjCCastStrict<NSString>(
value)];
} else if ([attribute
isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
[self setAccessibilitySelectedTextRange:base::mac::ObjCCastStrict<NSValue>(
value)
.rangeValue];
} else if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
[self setAccessibilityFocused:base::mac::ObjCCastStrict<NSNumber>(value)
.boolValue];
}
}
// This method, while deprecated, is still called internally by AppKit.
- (id)accessibilityAttributeValue:(NSString*)attribute {
if (!_node)
return nil; // Return nil when detached. Even for ax::mojom::Role.
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 ax::mojom::Role::kTab:
// 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 ax::mojom::Role::kDisclosureTriangle:
return [l10n_util::GetNSStringWithFixup(
IDS_ACCNAME_DISCLOSURE_TRIANGLE_ROLE_DESCRIPTION) lowercaseString];
default:
break;
}
return NSAccessibilityRoleDescription([self AXRole], [self AXSubrole]);
}
- (NSString*)AXSubrole {
ax::mojom::Role role = _node->GetData().role;
switch (role) {
case ax::mojom::Role::kTextField:
if (_node->GetData().HasState(ax::mojom::State::kProtected))
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:ax::mojom::StringAttribute::kDescription];
NSString* key =
[self getStringAttribute:ax::mojom::StringAttribute::kKeyShortcuts];
if (!desc.length)
return key.length ? key : @"";
if (!key.length)
return desc;
return [NSString stringWithFormat:@"%@ %@", desc, key];
}
- (id)AXValue {
ax::mojom::Role role = _node->GetData().role;
if (role == ax::mojom::Role::kTab)
return [self AXSelected];
if (ui::IsNameExposedInAXValueForRole(role))
return [self getName];
if (_node->IsPlatformCheckable()) {
// Mixed checkbox state not currently supported in views, but could be.
// See browser_accessibility_cocoa.mm for details.
const auto checkedState = static_cast<ax::mojom::CheckedState>(
_node->GetIntAttribute(ax::mojom::IntAttribute::kCheckedState));
return checkedState == ax::mojom::CheckedState::kTrue ? @1 : @0;
}
return base::SysUTF16ToNSString(_node->GetValueForControl());
}
- (NSNumber*)AXEnabled {
return
@(_node->GetData().GetRestriction() != ax::mojom::Restriction::kDisabled);
}
- (NSNumber*)AXFocused {
if (_node->GetData().HasState(ax::mojom::State::kFocusable))
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 (auto child_iterator_ptr = _node->GetDelegate()->ChildrenBegin();
*child_iterator_ptr != *_node->GetDelegate()->ChildrenEnd();
++(*child_iterator_ptr)) {
[children addObject:child_iterator_ptr->GetNativeViewAccessible()];
}
return NSAccessibilityUnignoredChildren(children);
}
- (id)AXWindow {
return _node->GetDelegate()->GetNSWindow();
}
- (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 getName];
}
// Misc attributes.
- (NSNumber*)AXSelected {
return
@(_node->GetData().GetBoolAttribute(ax::mojom::BoolAttribute::kSelected));
}
- (NSString*)AXPlaceholderValue {
return [self getStringAttribute:ax::mojom::StringAttribute::kPlaceholder];
}
- (NSString*)AXMenuItemMarkChar {
if (!ui::IsMenuItem(_node->GetData().role))
return nil;
const auto checkedState = static_cast<ax::mojom::CheckedState>(
_node->GetIntAttribute(ax::mojom::IntAttribute::kCheckedState));
if (checkedState == ax::mojom::CheckedState::kTrue) {
return @"\xE2\x9C\x93"; // UTF-8 for unicode 0x2713, "check mark"
}
return @"";
}
// 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;
if (_node->IsPlainTextField()) {
start = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart);
end = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd);
}
// 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();
}
- (NSString*)description {
return [NSString stringWithFormat:@"%@ - %@ (%@)", [super description],
[self AXTitle], [self AXRole]];
}
// The methods below implement the NSAccessibility protocol. These methods
// appear to be the minimum needed to avoid AppKit refusing to handle the
// element or crashing internally. Most of the remaining old API methods (the
// ones from NSObject) are implemented in terms of the new NSAccessibility
// methods.
//
// TODO(https://crbug.com/386671): Does this class need to implement the various
// accessibilityPerformFoo methods, or are the stub implementations from
// NSAccessibilityElement sufficient?
- (NSArray*)accessibilityChildren {
return [self AXChildren];
}
- (BOOL)isAccessibilityElement {
if (!_node)
return NO;
return (![[self AXRole] isEqualToString:NSAccessibilityUnknownRole] &&
!_node->IsInvisibleOrIgnored());
}
- (BOOL)isAccessibilityEnabled {
if (!_node)
return NO;
return _node->GetData().GetRestriction() != ax::mojom::Restriction::kDisabled;
}
- (NSRect)accessibilityFrame {
return [self boundsInScreen];
}
- (NSString*)accessibilityLabel {
// accessibilityLabel is "a short description of the accessibility element",
// and accessibilityTitle is "the title of the accessibility element"; at
// least in Chromium, the title usually is a short description of the element,
// so it also functions as a label.
return [self AXTitle];
}
- (NSString*)accessibilityTitle {
return [self AXTitle];
}
- (id)accessibilityValue {
return [self AXValue];
}
- (NSAccessibilityRole)accessibilityRole {
return [self AXRole];
}
- (NSAccessibilitySubrole)accessibilitySubrole {
return [self AXSubrole];
}
- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector {
if (!_node)
return NO;
const ax::mojom::Restriction restriction = _node->GetData().GetRestriction();
if (restriction == ax::mojom::Restriction::kDisabled)
return NO;
if (selector == @selector(setAccessibilityValue:)) {
// Tabs use the radio button role on Mac, so they are selected by calling
// setSelected on an individual tab, rather than by setting the selected
// element on the tabstrip as a whole.
if (_node->GetData().role == ax::mojom::Role::kTab) {
return !_node->GetData().GetBoolAttribute(
ax::mojom::BoolAttribute::kSelected);
}
return restriction != ax::mojom::Restriction::kReadOnly;
}
// TODO(https://crbug.com/692362): Once the underlying bug in
// views::Textfield::SetSelectionRange() described in that bug is fixed,
// remove the check here; right now, this check serves to prevent
// accessibility clients from trying to set the selection range, which won't
// work because of 692362.
if (selector == @selector(setAccessibilitySelectedText:) ||
selector == @selector(setAccessibilitySelectedTextRange:)) {
return restriction != ax::mojom::Restriction::kReadOnly;
}
if (selector == @selector(setAccessibilityFocused:))
return _node->GetData().HasState(ax::mojom::State::kFocusable);
// TODO(https://crbug.com/386671): What about role-specific selectors?
return [super isAccessibilitySelectorAllowed:selector];
}
- (void)setAccessibilityValue:(id)value {
if (!_node)
return;
ui::AXActionData data;
data.action = _node->GetData().role == ax::mojom::Role::kTab
? ax::mojom::Action::kSetSelection
: ax::mojom::Action::kSetValue;
if ([value isKindOfClass:[NSString class]]) {
data.value = base::SysNSStringToUTF8(value);
} else if ([value isKindOfClass:[NSValue class]]) {
// TODO(https://crbug.com/386671): Is this case actually needed? The
// NSObject accessibility implementation supported this, but can it actually
// occur?
NSRange range = [value rangeValue];
data.anchor_offset = range.location;
data.focus_offset = NSMaxRange(range);
}
_node->GetDelegate()->AccessibilityPerformAction(data);
}
- (void)setAccessibilityFocused:(BOOL)isFocused {
if (!_node)
return;
ui::AXActionData data;
data.action =
isFocused ? ax::mojom::Action::kFocus : ax::mojom::Action::kBlur;
_node->GetDelegate()->AccessibilityPerformAction(data);
}
- (void)setAccessibilitySelectedText:(NSString*)text {
if (!_node)
return;
ui::AXActionData data;
data.action = ax::mojom::Action::kReplaceSelectedText;
data.value = base::SysNSStringToUTF8(text);
_node->GetDelegate()->AccessibilityPerformAction(data);
}
- (void)setAccessibilitySelectedTextRange:(NSRange)range {
if (!_node)
return;
ui::AXActionData data;
data.action = ax::mojom::Action::kSetSelection;
data.anchor_offset = range.location;
data.focus_offset = NSMaxRange(range);
_node->GetDelegate()->AccessibilityPerformAction(data);
}
// "Configuring Text Elements" section of the NSAccessibility formal protocol.
// These are all "required" methods, although in practice the ones that are left
// NOTIMPLEMENTED() seem to not be called anywhere (and were NOTIMPLEMENTED in
// the old API as well).
- (NSInteger)accessibilityInsertionPointLineNumber {
return 0;
}
- (NSInteger)accessibilityNumberOfCharacters {
if (!_node)
return 0;
return [[self getAXValueAsString] length];
}
- (NSString*)accessibilityPlaceholderValue {
if (!_node)
return nil;
return [self AXPlaceholderValue];
}
- (NSString*)accessibilitySelectedText {
if (!_node)
return nil;
return [self AXSelectedText];
}
- (NSRange)accessibilitySelectedTextRange {
if (!_node)
return NSMakeRange(0, 0);
NSRange r;
[[self AXSelectedTextRange] getValue:&r];
return r;
}
- (NSArray*)accessibilitySelectedTextRanges {
if (!_node)
return nil;
return @[ [self AXSelectedTextRange] ];
}
- (NSRange)accessibilityVisibleCharacterRange {
if (!_node)
return NSMakeRange(0, 0);
return NSMakeRange(0, [self accessibilityNumberOfCharacters]);
}
- (NSString*)accessibilityStringForRange:(NSRange)range {
if (!_node)
return nil;
return [[self getAXValueAsString] substringWithRange:range];
}
- (NSAttributedString*)accessibilityAttributedStringForRange:(NSRange)range {
if (!_node)
return nil;
// TODO(https://crbug.com/958811): Implement this for real.
base::scoped_nsobject<NSAttributedString> attributedString(
[[NSAttributedString alloc]
initWithString:[self accessibilityStringForRange:range]]);
return attributedString.autorelease();
}
- (NSInteger)accessibilityLineForIndex:(NSInteger)index {
// Views textfields are single-line.
return 0;
}
- (NSRange)accessibilityRangeForIndex:(NSInteger)index {
NOTIMPLEMENTED();
return NSMakeRange(0, 0);
}
- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index {
if (!_node)
return NSMakeRange(0, 0);
// TODO(https://crbug.com/958811): Implement this for real.
return NSMakeRange(0, [self accessibilityNumberOfCharacters]);
}
- (NSRange)accessibilityRangeForLine:(NSInteger)line {
if (!_node)
return NSMakeRange(0, 0);
if (line != 0)
NOTIMPLEMENTED() << "Views textfields are single-line.";
return NSMakeRange(0, [self accessibilityNumberOfCharacters]);
}
- (NSRange)accessibilityRangeForPosition:(NSPoint)point {
NOTIMPLEMENTED();
return NSMakeRange(0, 0);
}
@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();
}
// On Mac, the checked state is mapped to AXValue.
bool AXPlatformNodeMac::IsPlatformCheckable() const {
if (GetData().role == ax::mojom::Role::kTab) {
// On Mac, tabs are exposed as radio buttons, and are treated as checkable.
// Also, the internal State::kSelected is be mapped to checked via AXValue.
return true;
}
return AXPlatformNodeBase::IsPlatformCheckable();
}
gfx::NativeViewAccessible AXPlatformNodeMac::GetNativeViewAccessible() {
if (!native_node_)
native_node_.reset([[AXPlatformNodeCocoa alloc] initWithNode:this]);
return native_node_.get();
}
void AXPlatformNodeMac::NotifyAccessibilityEvent(ax::mojom::Event event_type) {
AXPlatformNodeBase::NotifyAccessibilityEvent(event_type);
GetNativeViewAccessible();
// Handle special cases.
// Alerts and live regions go through the announcement API instead of the
// regular NSAccessibility notification system.
if (event_type == ax::mojom::Event::kAlert ||
event_type == ax::mojom::Event::kLiveRegionChanged) {
if (auto announcement = [native_node_ announcementForEvent:event_type]) {
[native_node_ scheduleLiveRegionAnnouncement:std::move(announcement)];
}
return;
}
if (event_type == ax::mojom::Event::kSelection) {
ax::mojom::Role role = GetData().role;
if (ui::IsMenuItem(role)) {
// On Mac, map menu item selection to a focus event.
NotifyMacEvent(native_node_, ax::mojom::Event::kFocus);
return;
} else if (ui::IsListItem(role)) {
if (AXPlatformNodeBase* container = GetSelectionContainer()) {
const ui::AXNodeData& data = container->GetData();
if (data.role == ax::mojom::Role::kListBox &&
!data.HasState(ax::mojom::State::kMultiselectable) &&
GetDelegate()->GetFocus() == GetNativeViewAccessible()) {
NotifyMacEvent(native_node_, ax::mojom::Event::kFocus);
return;
}
}
}
}
// Otherwise, use mappings between ax::mojom::Event and NSAccessibility
// notifications from the EventMap above.
NotifyMacEvent(native_node_, event_type);
}
void AXPlatformNodeMac::AnnounceText(const base::string16& text) {
PostAnnouncementNotification(base::SysUTF16ToNSString(text),
[native_node_ AXWindow], false);
}
bool IsNameExposedInAXValueForRole(ax::mojom::Role role) {
switch (role) {
case ax::mojom::Role::kListBoxOption:
case ax::mojom::Role::kListMarker:
case ax::mojom::Role::kMenuListOption:
case ax::mojom::Role::kStaticText:
case ax::mojom::Role::kTitleBar:
return true;
default:
return false;
}
}
void AXPlatformNodeMac::AddAttributeToList(const char* name,
const char* value,
PlatformAttributeList* attributes) {
NOTREACHED();
}
} // namespace ui