// 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" #import "base/logging.h" #import "content/browser/accessibility/browser_accessibility_cocoa.h" #import "content/browser/accessibility/browser_accessibility_mac.h" #include "content/common/accessibility_messages.h" namespace content { // static BrowserAccessibilityManager* BrowserAccessibilityManager::Create( const SimpleAXTreeUpdate& initial_tree, BrowserAccessibilityDelegate* delegate, BrowserAccessibilityFactory* factory) { return new BrowserAccessibilityManagerMac( NULL, initial_tree, delegate, factory); } BrowserAccessibilityManagerMac::BrowserAccessibilityManagerMac( NSView* parent_view, const SimpleAXTreeUpdate& initial_tree, BrowserAccessibilityDelegate* delegate, BrowserAccessibilityFactory* factory) : BrowserAccessibilityManager(delegate, factory), parent_view_(parent_view) { Initialize(initial_tree); } // static SimpleAXTreeUpdate BrowserAccessibilityManagerMac::GetEmptyDocument() { ui::AXNodeData empty_document; empty_document.id = 0; empty_document.role = ui::AX_ROLE_ROOT_WEB_AREA; empty_document.state = 1 << ui::AX_STATE_READ_ONLY; SimpleAXTreeUpdate update; update.nodes.push_back(empty_document); return update; } BrowserAccessibility* BrowserAccessibilityManagerMac::GetFocus( BrowserAccessibility* root) { // On Mac, list boxes should always get focus on the whole list, otherwise // information about the number of selected items will never be reported. BrowserAccessibility* node = BrowserAccessibilityManager::GetFocus(root); if (node && node->GetRole() == ui::AX_ROLE_LIST_BOX) return node; // For other roles, follow the active descendant. return GetActiveDescendantFocus(root); } void BrowserAccessibilityManagerMac::NotifyAccessibilityEvent( ui::AXEvent event_type, BrowserAccessibility* node) { if (!node->IsNative()) return; if (event_type == ui::AX_EVENT_FOCUS && node->GetRole() == ui::AX_ROLE_LIST_BOX_OPTION && node->HasState(ui::AX_STATE_SELECTED) && node->GetParent() && node->GetParent()->GetRole() == ui::AX_ROLE_LIST_BOX) { node = node->GetParent(); SetFocus(node, false); } // Refer to AXObjectCache.mm (webkit). NSString* event_id = @""; switch (event_type) { case ui::AX_EVENT_ACTIVEDESCENDANTCHANGED: if (node->GetRole() == ui::AX_ROLE_TREE) { event_id = NSAccessibilitySelectedRowsChangedNotification; } else { event_id = NSAccessibilityFocusedUIElementChangedNotification; BrowserAccessibility* active_descendant_focus = GetActiveDescendantFocus(GetRoot()); if (active_descendant_focus) node = active_descendant_focus; } break; case ui::AX_EVENT_ALERT: // Not used on Mac. return; case ui::AX_EVENT_BLUR: // A no-op on Mac. return; case ui::AX_EVENT_CHECKED_STATE_CHANGED: // Not used on Mac. return; case ui::AX_EVENT_CHILDREN_CHANGED: // TODO(dtseng): no clear equivalent on Mac. return; case ui::AX_EVENT_FOCUS: event_id = NSAccessibilityFocusedUIElementChangedNotification; break; case ui::AX_EVENT_LAYOUT_COMPLETE: event_id = @"AXLayoutComplete"; break; case ui::AX_EVENT_LIVE_REGION_CHANGED: event_id = @"AXLiveRegionChanged"; break; case ui::AX_EVENT_LOAD_COMPLETE: event_id = @"AXLoadComplete"; break; case ui::AX_EVENT_MENU_LIST_VALUE_CHANGED: // Not used on Mac. return; case ui::AX_EVENT_ROW_COUNT_CHANGED: event_id = NSAccessibilityRowCountChangedNotification; break; case ui::AX_EVENT_ROW_COLLAPSED: event_id = @"AXRowCollapsed"; break; case ui::AX_EVENT_ROW_EXPANDED: event_id = @"AXRowExpanded"; break; case ui::AX_EVENT_SCROLLED_TO_ANCHOR: // Not used on Mac. return; case ui::AX_EVENT_SELECTED_CHILDREN_CHANGED: event_id = NSAccessibilitySelectedChildrenChangedNotification; break; case ui::AX_EVENT_TEXT_SELECTION_CHANGED: event_id = NSAccessibilitySelectedTextChangedNotification; break; case ui::AX_EVENT_VALUE_CHANGED: event_id = NSAccessibilityValueChangedNotification; break; case ui::AX_EVENT_ARIA_ATTRIBUTE_CHANGED: // Not used on Mac. return; case ui::AX_EVENT_AUTOCORRECTION_OCCURED: // Not used on Mac. return; case ui::AX_EVENT_INVALID_STATUS_CHANGED: // Not used on Mac. return; case ui::AX_EVENT_LOCATION_CHANGED: // Not used on Mac. return; case ui::AX_EVENT_MENU_LIST_ITEM_SELECTED: // Not used on Mac. return; case ui::AX_EVENT_TEXT_CHANGED: // Not used on Mac. return; default: LOG(WARNING) << "Unknown accessibility event: " << event_type; return; } BrowserAccessibilityCocoa* native_node = node->ToBrowserAccessibilityCocoa(); DCHECK(native_node); NSAccessibilityPostNotification(native_node, event_id); } void BrowserAccessibilityManagerMac::OnAtomicUpdateFinished( ui::AXTree* tree, bool root_changed, const std::vector<ui::AXTreeDelegate::Change>& changes) { BrowserAccessibilityManager::OnAtomicUpdateFinished( tree, root_changed, changes); bool created_live_region = false; for (size_t i = 0; i < changes.size(); ++i) { if (changes[i].type != NODE_CREATED && changes[i].type != SUBTREE_CREATED) continue; BrowserAccessibility* obj = GetFromAXNode(changes[i].node); if (obj && obj->HasStringAttribute(ui::AX_ATTR_LIVE_STATUS)) { created_live_region = true; break; } } if (!created_live_region) return; // This code is to work around a bug in VoiceOver, where a new live // region that gets added is ignored. VoiceOver seems to only scan the // page for live regions once. By recreating the NSAccessibility // object for the root of the tree, we force VoiceOver to clear out its // internal state and find newly-added live regions this time. BrowserAccessibilityMac* root = static_cast<BrowserAccessibilityMac*>(GetRoot()); if (root) { root->RecreateNativeObject(); NotifyAccessibilityEvent(ui::AX_EVENT_CHILDREN_CHANGED, root); } } } // namespace content