blob: f798789bf05321e70e4dfe1543ddaa52bfd648f1 [file] [log] [blame]
// Copyright (c) 2012 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.
#include "content/browser/accessibility/browser_accessibility_manager_mac.h"
#include "base/bind.h"
#include "base/location.h"
#include "base/logging.h"
#import "base/mac/mac_util.h"
#import "base/mac/scoped_nsobject.h"
#import "base/mac/sdk_forward_declarations.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#import "content/browser/accessibility/browser_accessibility_cocoa.h"
#import "content/browser/accessibility/browser_accessibility_mac.h"
#include "content/common/accessibility_messages.h"
#include "content/public/browser/browser_thread.h"
#include "ui/accessibility/ax_role_properties.h"
namespace {
// Use same value as in Safari's WebKit.
const int kLiveRegionChangeIntervalMS = 20;
// Declare undocumented accessibility constants and enums only present in
// WebKit.
enum AXTextStateChangeType {
AXTextStateChangeTypeUnknown,
AXTextStateChangeTypeEdit,
AXTextStateChangeTypeSelectionMove,
AXTextStateChangeTypeSelectionExtend
};
enum AXTextSelectionDirection {
AXTextSelectionDirectionUnknown,
AXTextSelectionDirectionBeginning,
AXTextSelectionDirectionEnd,
AXTextSelectionDirectionPrevious,
AXTextSelectionDirectionNext,
AXTextSelectionDirectionDiscontiguous
};
enum AXTextSelectionGranularity {
AXTextSelectionGranularityUnknown,
AXTextSelectionGranularityCharacter,
AXTextSelectionGranularityWord,
AXTextSelectionGranularityLine,
AXTextSelectionGranularitySentence,
AXTextSelectionGranularityParagraph,
AXTextSelectionGranularityPage,
AXTextSelectionGranularityDocument,
AXTextSelectionGranularityAll
};
enum AXTextEditType {
AXTextEditTypeUnknown,
AXTextEditTypeDelete,
AXTextEditTypeInsert,
AXTextEditTypeTyping,
AXTextEditTypeDictation,
AXTextEditTypeCut,
AXTextEditTypePaste,
AXTextEditTypeAttributesChange
};
NSString* const NSAccessibilityAutocorrectionOccurredNotification =
@"AXAutocorrectionOccurred";
NSString* const NSAccessibilityLayoutCompleteNotification =
@"AXLayoutComplete";
NSString* const NSAccessibilityLoadCompleteNotification =
@"AXLoadComplete";
NSString* const NSAccessibilityInvalidStatusChangedNotification =
@"AXInvalidStatusChanged";
NSString* const NSAccessibilityLiveRegionCreatedNotification =
@"AXLiveRegionCreated";
NSString* const NSAccessibilityLiveRegionChangedNotification =
@"AXLiveRegionChanged";
NSString* const NSAccessibilityExpandedChanged =
@"AXExpandedChanged";
NSString* const NSAccessibilityMenuItemSelectedNotification =
@"AXMenuItemSelected";
// Attributes used for NSAccessibilitySelectedTextChangedNotification and
// NSAccessibilityValueChangedNotification.
NSString* const NSAccessibilityTextStateChangeTypeKey =
@"AXTextStateChangeType";
NSString* const NSAccessibilityTextStateSyncKey = @"AXTextStateSync";
NSString* const NSAccessibilityTextSelectionDirection =
@"AXTextSelectionDirection";
NSString* const NSAccessibilityTextSelectionGranularity =
@"AXTextSelectionGranularity";
NSString* const NSAccessibilityTextSelectionChangedFocus =
@"AXTextSelectionChangedFocus";
NSString* const NSAccessibilitySelectedTextMarkerRangeAttribute =
@"AXSelectedTextMarkerRange";
NSString* const NSAccessibilityTextChangeElement = @"AXTextChangeElement";
NSString* const NSAccessibilityTextEditType = @"AXTextEditType";
NSString* const NSAccessibilityTextChangeValue = @"AXTextChangeValue";
NSString* const NSAccessibilityTextChangeValueLength =
@"AXTextChangeValueLength";
NSString* const NSAccessibilityTextChangeValues = @"AXTextChangeValues";
} // namespace
namespace content {
// static
BrowserAccessibilityManager* BrowserAccessibilityManager::Create(
const ui::AXTreeUpdate& initial_tree,
BrowserAccessibilityDelegate* delegate,
BrowserAccessibilityFactory* factory) {
return new BrowserAccessibilityManagerMac(initial_tree, delegate, factory);
}
BrowserAccessibilityManagerMac*
BrowserAccessibilityManager::ToBrowserAccessibilityManagerMac() {
return static_cast<BrowserAccessibilityManagerMac*>(this);
}
BrowserAccessibilityManagerMac::BrowserAccessibilityManagerMac(
const ui::AXTreeUpdate& initial_tree,
BrowserAccessibilityDelegate* delegate,
BrowserAccessibilityFactory* factory)
: BrowserAccessibilityManager(delegate, factory) {
Initialize(initial_tree);
}
BrowserAccessibilityManagerMac::~BrowserAccessibilityManagerMac() {}
// static
ui::AXTreeUpdate
BrowserAccessibilityManagerMac::GetEmptyDocument() {
ui::AXNodeData empty_document;
empty_document.id = 0;
empty_document.role = ui::AX_ROLE_ROOT_WEB_AREA;
ui::AXTreeUpdate update;
update.root_id = empty_document.id;
update.nodes.push_back(empty_document);
return update;
}
BrowserAccessibility* BrowserAccessibilityManagerMac::GetFocus() {
BrowserAccessibility* focus = BrowserAccessibilityManager::GetFocus();
// On Mac, list boxes should always get focus on the whole list, otherwise
// information about the number of selected items will never be reported.
// For editable combo boxes, focus should stay on the combo box so the user
// will not be taken out of the combo box while typing.
if (focus && (focus->GetRole() == ui::AX_ROLE_LIST_BOX ||
(focus->GetRole() == ui::AX_ROLE_COMBO_BOX &&
focus->HasState(ui::AX_STATE_EDITABLE)))) {
return focus;
}
// For other roles, follow the active descendant.
return GetActiveDescendant(focus);
}
void BrowserAccessibilityManagerMac::NotifyAccessibilityEvent(
BrowserAccessibilityEvent::Source source,
ui::AXEvent event_type,
BrowserAccessibility* node) {
if (!node->IsNative())
return;
auto native_node = ToBrowserAccessibilityCocoa(node);
DCHECK(native_node);
// Refer to |AXObjectCache::postPlatformNotification| in WebKit source code.
NSString* mac_notification;
switch (event_type) {
case ui::AX_EVENT_ACTIVEDESCENDANTCHANGED:
if (node->GetRole() == ui::AX_ROLE_TREE) {
mac_notification = NSAccessibilitySelectedRowsChangedNotification;
} else if (node->GetRole() == ui::AX_ROLE_COMBO_BOX) {
// Even though the selected item in the combo box has changed, we don't
// want to post a focus change because this will take the focus out of
// the combo box where the user might be typing.
mac_notification = NSAccessibilitySelectedChildrenChangedNotification;
} else {
// In all other cases we should post
// |NSAccessibilityFocusedUIElementChangedNotification|, but this is
// handled elsewhere.
return;
}
break;
case ui::AX_EVENT_AUTOCORRECTION_OCCURED:
mac_notification = NSAccessibilityAutocorrectionOccurredNotification;
break;
case ui::AX_EVENT_FOCUS:
mac_notification = NSAccessibilityFocusedUIElementChangedNotification;
break;
case ui::AX_EVENT_LAYOUT_COMPLETE:
mac_notification = NSAccessibilityLayoutCompleteNotification;
break;
case ui::AX_EVENT_LOAD_COMPLETE:
// This notification should only be fired on the top document.
// Iframes should use |AX_EVENT_LAYOUT_COMPLETE| to signify that they have
// finished loading.
if (IsRootTree()) {
mac_notification = NSAccessibilityLoadCompleteNotification;
} else {
mac_notification = NSAccessibilityLayoutCompleteNotification;
}
break;
case ui::AX_EVENT_INVALID_STATUS_CHANGED:
mac_notification = NSAccessibilityInvalidStatusChangedNotification;
break;
case ui::AX_EVENT_SELECTED_CHILDREN_CHANGED:
if (ui::IsTableLikeRole(node->GetRole())) {
mac_notification = NSAccessibilitySelectedRowsChangedNotification;
} else {
mac_notification = NSAccessibilitySelectedChildrenChangedNotification;
}
break;
case ui::AX_EVENT_DOCUMENT_SELECTION_CHANGED: {
mac_notification = NSAccessibilitySelectedTextChangedNotification;
// WebKit fires a notification both on the focused object and the page
// root.
BrowserAccessibility* focus = GetFocus();
if (!focus)
break; // Just fire a notification on the root.
if (base::mac::IsAtLeastOS10_11()) {
// |NSAccessibilityPostNotificationWithUserInfo| should be used on OS X
// 10.11 or later to notify Voiceover about text selection changes. This
// API has been present on versions of OS X since 10.7 but doesn't
// appear to be needed by Voiceover before version 10.11.
NSDictionary* user_info =
GetUserInfoForSelectedTextChangedNotification();
BrowserAccessibilityManager* root_manager = GetRootManager();
if (!root_manager)
return;
BrowserAccessibility* root = root_manager->GetRoot();
if (!root)
return;
NSAccessibilityPostNotificationWithUserInfo(
ToBrowserAccessibilityCocoa(focus), mac_notification, user_info);
NSAccessibilityPostNotificationWithUserInfo(
ToBrowserAccessibilityCocoa(root), mac_notification, user_info);
return;
} else {
NSAccessibilityPostNotification(ToBrowserAccessibilityCocoa(focus),
mac_notification);
}
break;
}
case ui::AX_EVENT_CHECKED_STATE_CHANGED:
mac_notification = NSAccessibilityValueChangedNotification;
break;
case ui::AX_EVENT_VALUE_CHANGED:
mac_notification = NSAccessibilityValueChangedNotification;
if (base::mac::IsAtLeastOS10_11() && !text_edits_.empty()) {
base::string16 deleted_text;
base::string16 inserted_text;
int32_t id = node->GetId();
const auto iterator = text_edits_.find(id);
if (iterator != text_edits_.end()) {
AXTextEdit text_edit = iterator->second;
deleted_text = text_edit.deleted_text;
inserted_text = text_edit.inserted_text;
}
NSDictionary* user_info = GetUserInfoForValueChangedNotification(
native_node, deleted_text, inserted_text);
BrowserAccessibility* root = GetRoot();
if (!root)
return;
NSAccessibilityPostNotificationWithUserInfo(
native_node, mac_notification, user_info);
NSAccessibilityPostNotificationWithUserInfo(
ToBrowserAccessibilityCocoa(root), mac_notification, user_info);
return;
}
break;
case ui::AX_EVENT_LIVE_REGION_CREATED:
mac_notification = NSAccessibilityLiveRegionCreatedNotification;
break;
case ui::AX_EVENT_ALERT:
NSAccessibilityPostNotification(
native_node, NSAccessibilityLiveRegionCreatedNotification);
// Voiceover requires a live region changed notification to actually
// announce the live region.
NotifyAccessibilityEvent(BrowserAccessibilityEvent::FromTreeChange,
ui::AX_EVENT_LIVE_REGION_CHANGED, node);
return;
case ui::AX_EVENT_LIVE_REGION_CHANGED: {
// Voiceover seems to drop live region changed notifications if they come
// too soon after a live region created notification.
// TODO(nektar): Limit the number of changed notifications as well.
if (never_suppress_or_delay_events_for_testing_) {
NSAccessibilityPostNotification(
native_node, NSAccessibilityLiveRegionChangedNotification);
return;
}
base::scoped_nsobject<BrowserAccessibilityCocoa> retained_node(
[native_node retain]);
BrowserThread::PostDelayedTask(
BrowserThread::UI, FROM_HERE,
base::Bind(
[](base::scoped_nsobject<BrowserAccessibilityCocoa> node) {
if (node && [node instanceActive]) {
NSAccessibilityPostNotification(
node, NSAccessibilityLiveRegionChangedNotification);
}
},
std::move(retained_node)),
base::TimeDelta::FromMilliseconds(kLiveRegionChangeIntervalMS));
}
return;
case ui::AX_EVENT_ROW_COUNT_CHANGED:
mac_notification = NSAccessibilityRowCountChangedNotification;
break;
case ui::AX_EVENT_ROW_EXPANDED:
mac_notification = NSAccessibilityRowExpandedNotification;
break;
case ui::AX_EVENT_ROW_COLLAPSED:
mac_notification = NSAccessibilityRowCollapsedNotification;
break;
// TODO(nektar): Add events for busy.
case ui::AX_EVENT_EXPANDED_CHANGED:
mac_notification = NSAccessibilityExpandedChanged;
break;
// TODO(nektar): Support menu open/close notifications.
case ui::AX_EVENT_MENU_LIST_ITEM_SELECTED:
mac_notification = NSAccessibilityMenuItemSelectedNotification;
break;
// These events are not used on Mac for now.
case ui::AX_EVENT_TEXT_CHANGED:
case ui::AX_EVENT_CHILDREN_CHANGED:
case ui::AX_EVENT_MENU_LIST_VALUE_CHANGED:
case ui::AX_EVENT_SCROLL_POSITION_CHANGED:
case ui::AX_EVENT_SCROLLED_TO_ANCHOR:
case ui::AX_EVENT_ARIA_ATTRIBUTE_CHANGED:
case ui::AX_EVENT_LOCATION_CHANGED:
return;
// Deprecated events.
case ui::AX_EVENT_BLUR:
case ui::AX_EVENT_HIDE:
case ui::AX_EVENT_HOVER:
case ui::AX_EVENT_TEXT_SELECTION_CHANGED:
case ui::AX_EVENT_SHOW:
return;
default:
DLOG(WARNING) << "Unknown accessibility event: " << event_type;
return;
}
NSAccessibilityPostNotification(native_node, mac_notification);
}
void BrowserAccessibilityManagerMac::OnAccessibilityEvents(
const std::vector<AXEventNotificationDetails>& details) {
text_edits_.clear();
// Call the base method last as it might delete the tree if it receives an
// invalid message.
BrowserAccessibilityManager::OnAccessibilityEvents(details);
}
void BrowserAccessibilityManagerMac::OnTreeDataChanged(
ui::AXTree* tree,
const ui::AXTreeData& old_tree_data,
const ui::AXTreeData& new_tree_data) {
BrowserAccessibilityManager::OnTreeDataChanged(tree, old_tree_data,
new_tree_data);
if (new_tree_data.loaded && !old_tree_data.loaded)
tree_events_[tree->root()->id()].insert(ui::AX_EVENT_LOAD_COMPLETE);
if (new_tree_data.sel_anchor_object_id !=
old_tree_data.sel_anchor_object_id ||
new_tree_data.sel_anchor_offset != old_tree_data.sel_anchor_offset ||
new_tree_data.sel_anchor_affinity != old_tree_data.sel_anchor_affinity ||
new_tree_data.sel_focus_object_id != old_tree_data.sel_focus_object_id ||
new_tree_data.sel_focus_offset != old_tree_data.sel_focus_offset ||
new_tree_data.sel_focus_affinity != old_tree_data.sel_focus_affinity) {
tree_events_[tree->root()->id()].insert(
ui::AX_EVENT_DOCUMENT_SELECTION_CHANGED);
}
}
void BrowserAccessibilityManagerMac::OnStateChanged(ui::AXTree* tree,
ui::AXNode* node,
ui::AXState state,
bool new_value) {
BrowserAccessibilityManager::OnStateChanged(tree, node, state, new_value);
if (state == ui::AX_STATE_EXPANDED) {
if (node->data().role == ui::AX_ROLE_ROW ||
node->data().role == ui::AX_ROLE_TREE_ITEM) {
if (new_value)
tree_events_[node->id()].insert(ui::AX_EVENT_ROW_EXPANDED);
else
tree_events_[node->id()].insert(ui::AX_EVENT_ROW_COLLAPSED);
ui::AXNode* container = node;
while (container && !ui::IsRowContainer(container->data().role))
container = container->parent();
if (container)
tree_events_[container->id()].insert(ui::AX_EVENT_ROW_COUNT_CHANGED);
} else {
tree_events_[node->id()].insert(ui::AX_EVENT_EXPANDED_CHANGED);
}
}
if (state == ui::AX_STATE_SELECTED) {
ui::AXNode* container = node;
while (container &&
!ui::IsContainerWithSelectableChildrenRole(container->data().role))
container = container->parent();
if (container)
tree_events_[container->id()].insert(
ui::AX_EVENT_SELECTED_CHILDREN_CHANGED);
}
}
void BrowserAccessibilityManagerMac::OnStringAttributeChanged(
ui::AXTree* tree,
ui::AXNode* node,
ui::AXStringAttribute attr,
const std::string& old_value,
const std::string& new_value) {
BrowserAccessibilityManager::OnStringAttributeChanged(tree, node, attr,
old_value, new_value);
switch (attr) {
case ui::AX_ATTR_VALUE:
tree_events_[node->id()].insert(ui::AX_EVENT_VALUE_CHANGED);
break;
case ui::AX_ATTR_ARIA_INVALID_VALUE:
tree_events_[node->id()].insert(ui::AX_EVENT_INVALID_STATUS_CHANGED);
break;
case ui::AX_ATTR_LIVE_STATUS:
tree_events_[node->id()].insert(ui::AX_EVENT_LIVE_REGION_CREATED);
break;
default:
break;
}
}
void BrowserAccessibilityManagerMac::OnIntAttributeChanged(
ui::AXTree* tree,
ui::AXNode* node,
ui::AXIntAttribute attr,
int32_t old_value,
int32_t new_value) {
BrowserAccessibilityManager::OnIntAttributeChanged(tree, node, attr,
old_value, new_value);
switch (attr) {
case ui::AX_ATTR_CHECKED_STATE:
tree_events_[node->id()].insert(ui::AX_EVENT_VALUE_CHANGED);
break;
case ui::AX_ATTR_ACTIVEDESCENDANT_ID:
tree_events_[node->id()].insert(ui::AX_EVENT_ACTIVEDESCENDANTCHANGED);
break;
case ui::AX_ATTR_INVALID_STATE:
tree_events_[node->id()].insert(ui::AX_EVENT_INVALID_STATUS_CHANGED);
break;
default:
break;
}
}
void BrowserAccessibilityManagerMac::OnFloatAttributeChanged(
ui::AXTree* tree,
ui::AXNode* node,
ui::AXFloatAttribute attr,
float old_value,
float new_value) {
BrowserAccessibilityManager::OnFloatAttributeChanged(tree, node, attr,
old_value, new_value);
if (attr == ui::AX_ATTR_VALUE_FOR_RANGE)
tree_events_[node->id()].insert(ui::AX_EVENT_VALUE_CHANGED);
}
void BrowserAccessibilityManagerMac::OnAtomicUpdateFinished(
ui::AXTree* tree,
bool root_changed,
const std::vector<Change>& changes) {
BrowserAccessibilityManager::OnAtomicUpdateFinished(tree, root_changed,
changes);
std::set<const BrowserAccessibilityCocoa*> changed_editable_roots;
for (const auto& change : changes) {
const BrowserAccessibility* obj = GetFromAXNode(change.node);
if (obj && obj->IsNative() && obj->HasState(ui::AX_STATE_EDITABLE)) {
const BrowserAccessibilityCocoa* editable_root =
[ToBrowserAccessibilityCocoa(obj) editableAncestor];
if (editable_root && [editable_root instanceActive])
changed_editable_roots.insert(editable_root);
}
if ((change.type == NODE_CREATED || change.type == SUBTREE_CREATED) &&
change.node->data().HasStringAttribute(ui::AX_ATTR_LIVE_STATUS)) {
if (change.node->data().role == ui::AX_ROLE_ALERT)
tree_events_[change.node->id()].insert(ui::AX_EVENT_ALERT);
else
tree_events_[change.node->id()].insert(
ui::AX_EVENT_LIVE_REGION_CREATED);
continue;
}
if (change.node->data().HasStringAttribute(
ui::AX_ATTR_CONTAINER_LIVE_STATUS)) {
ui::AXNode* live_root = change.node;
while (live_root &&
!live_root->data().HasStringAttribute(ui::AX_ATTR_LIVE_STATUS))
live_root = live_root->parent();
if (live_root)
tree_events_[live_root->id()].insert(ui::AX_EVENT_LIVE_REGION_CHANGED);
}
}
for (const BrowserAccessibilityCocoa* obj : changed_editable_roots) {
DCHECK(obj);
const AXTextEdit text_edit = [obj computeTextEdit];
if (!text_edit.IsEmpty())
text_edits_[[obj browserAccessibility]->GetId()] = text_edit;
}
if (root_changed && tree->data().loaded)
tree_events_[tree->root()->id()].insert(ui::AX_EVENT_LOAD_COMPLETE);
}
NSDictionary* BrowserAccessibilityManagerMac::
GetUserInfoForSelectedTextChangedNotification() {
NSMutableDictionary* user_info = [[[NSMutableDictionary alloc] init]
autorelease];
[user_info setObject:@YES forKey:NSAccessibilityTextStateSyncKey];
[user_info setObject:@(AXTextStateChangeTypeUnknown)
forKey:NSAccessibilityTextStateChangeTypeKey];
[user_info setObject:@(AXTextSelectionDirectionUnknown)
forKey:NSAccessibilityTextSelectionDirection];
[user_info setObject:@(AXTextSelectionGranularityUnknown)
forKey:NSAccessibilityTextSelectionGranularity];
[user_info setObject:@YES forKey:NSAccessibilityTextSelectionChangedFocus];
int32_t focus_id = GetTreeData().sel_focus_object_id;
BrowserAccessibility* focus_object = GetFromID(focus_id);
if (focus_object) {
focus_object = focus_object->GetClosestPlatformObject();
auto native_focus_object = ToBrowserAccessibilityCocoa(focus_object);
if (native_focus_object && [native_focus_object instanceActive]) {
[user_info setObject:native_focus_object
forKey:NSAccessibilityTextChangeElement];
id selected_text = [native_focus_object selectedTextMarkerRange];
if (selected_text) {
[user_info setObject:selected_text
forKey:NSAccessibilitySelectedTextMarkerRangeAttribute];
}
}
}
return user_info;
}
NSDictionary*
BrowserAccessibilityManagerMac::GetUserInfoForValueChangedNotification(
const BrowserAccessibilityCocoa* native_node,
const base::string16& deleted_text,
const base::string16& inserted_text) const {
DCHECK(native_node);
if (deleted_text.empty() && inserted_text.empty())
return nil;
NSMutableArray* changes = [[[NSMutableArray alloc] init] autorelease];
if (!deleted_text.empty()) {
[changes addObject:@{
NSAccessibilityTextEditType : @(AXTextEditTypeDelete),
NSAccessibilityTextChangeValueLength : @(deleted_text.length()),
NSAccessibilityTextChangeValue : base::SysUTF16ToNSString(deleted_text)
}];
}
if (!inserted_text.empty()) {
// TODO(nektar): Figure out if this is a paste operation instead of typing.
// Changes to Blink would be required.
[changes addObject:@{
NSAccessibilityTextEditType : @(AXTextEditTypeTyping),
NSAccessibilityTextChangeValueLength : @(inserted_text.length()),
NSAccessibilityTextChangeValue : base::SysUTF16ToNSString(inserted_text)
}];
}
return @{
NSAccessibilityTextStateChangeTypeKey : @(AXTextStateChangeTypeEdit),
NSAccessibilityTextChangeValues : changes,
NSAccessibilityTextChangeElement : native_node
};
}
NSView* BrowserAccessibilityManagerMac::GetParentView() {
return delegate() ? delegate()->AccessibilityGetAcceleratedWidget() : nullptr;
}
} // namespace content