| // Copyright 2012 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "ui/accessibility/platform/browser_accessibility_manager_mac.h" | |
| #include "base/apple/foundation_util.h" | |
| #include "base/check.h" | |
| #include "base/functional/bind.h" | |
| #include "base/location.h" | |
| #import "base/mac/mac_util.h" | |
| #include "base/strings/sys_string_conversions.h" | |
| #include "base/strings/utf_string_conversions.h" | |
| #include "base/task/single_thread_task_runner.h" | |
| #include "base/task/task_traits.h" | |
| #include "base/time/time.h" | |
| #include "ui/accelerated_widget_mac/accelerated_widget_mac.h" | |
| #include "ui/accessibility/accessibility_features.h" | |
| #include "ui/accessibility/ax_role_properties.h" | |
| #include "ui/accessibility/ax_table_info.h" | |
| #include "ui/accessibility/platform/ax_platform_tree_manager_delegate.h" | |
| #include "ui/accessibility/platform/ax_private_webkit_constants_mac.h" | |
| #import "ui/accessibility/platform/browser_accessibility_cocoa.h" | |
| #import "ui/accessibility/platform/browser_accessibility_mac.h" | |
| #include "ui/base/cocoa/remote_accessibility_api.h" | |
| namespace { | |
| // Use same value as in Safari's WebKit. | |
| const int kLiveRegionChangeIntervalMS = 20; | |
| } // namespace | |
| namespace ui { | |
| // static | |
| BrowserAccessibilityManager* BrowserAccessibilityManager::Create( | |
| const AXTreeUpdate& initial_tree, | |
| AXNodeIdDelegate& node_id_delegate, | |
| AXPlatformTreeManagerDelegate* delegate) { | |
| return new BrowserAccessibilityManagerMac(initial_tree, node_id_delegate, | |
| delegate); | |
| } | |
| // static | |
| BrowserAccessibilityManager* BrowserAccessibilityManager::Create( | |
| AXNodeIdDelegate& node_id_delegate, | |
| AXPlatformTreeManagerDelegate* delegate) { | |
| return new BrowserAccessibilityManagerMac( | |
| BrowserAccessibilityManagerMac::GetEmptyDocument(), node_id_delegate, | |
| delegate); | |
| } | |
| BrowserAccessibilityManagerMac* | |
| BrowserAccessibilityManager::ToBrowserAccessibilityManagerMac() { | |
| return static_cast<BrowserAccessibilityManagerMac*>(this); | |
| } | |
| BrowserAccessibilityManagerMac::BrowserAccessibilityManagerMac( | |
| const AXTreeUpdate& initial_tree, | |
| AXNodeIdDelegate& node_id_delegate, | |
| AXPlatformTreeManagerDelegate* delegate) | |
| : BrowserAccessibilityManager(node_id_delegate, delegate) { | |
| Initialize(initial_tree); | |
| } | |
| BrowserAccessibilityManagerMac::~BrowserAccessibilityManagerMac() = default; | |
| // static | |
| AXTreeUpdate BrowserAccessibilityManagerMac::GetEmptyDocument() { | |
| AXNodeData empty_document; | |
| empty_document.id = kInitialEmptyDocumentRootNodeID; | |
| empty_document.role = ax::mojom::Role::kRootWebArea; | |
| AXTreeUpdate update; | |
| update.root_id = empty_document.id; | |
| update.nodes.push_back(empty_document); | |
| return update; | |
| } | |
| void BrowserAccessibilityManagerMac::FireFocusEvent(AXNode* node) { | |
| AXTreeManager::FireFocusEvent(node); | |
| if (BrowserAccessibility* wrapper = GetFromAXNode(node)) { | |
| FireNativeMacNotification( | |
| NSAccessibilityFocusedUIElementChangedNotification, *wrapper); | |
| } | |
| } | |
| void BrowserAccessibilityManagerMac::FireSourceEvent( | |
| ax::mojom::Event event_type, | |
| BrowserAccessibility* node, | |
| int action_request_id) { | |
| DCHECK(node); | |
| BrowserAccessibilityManager::FireSourceEvent(event_type, node, | |
| action_request_id); | |
| NSString* mac_notification = nullptr; | |
| switch (event_type) { | |
| case ax::mojom::Event::kAutocorrectionOccured: | |
| mac_notification = NSAccessibilityAutocorrectionOccurredNotification; | |
| break; | |
| case ax::mojom::Event::kLoadComplete: | |
| if (!ShouldFireLoadCompleteNotification()) | |
| return; | |
| mac_notification = NSAccessibilityLoadCompleteNotification; | |
| break; | |
| default: | |
| return; | |
| } | |
| FireNativeMacNotification(mac_notification, *node); | |
| } | |
| void PostAnnouncementNotification(NSString* announcement, | |
| id element, | |
| NSAccessibilityPriorityLevel priorityLevel) { | |
| NSDictionary* notification_info = @{ | |
| NSAccessibilityAnnouncementKey : announcement, | |
| NSAccessibilityPriorityKey : @(priorityLevel) | |
| }; | |
| // Trigger VoiceOver speech and show on Braille display, if available. | |
| // The Braille will only appear for a few seconds, and then will be replaced | |
| // with the previous announcement. | |
| NSAccessibilityPostNotificationWithUserInfo( | |
| element, NSAccessibilityAnnouncementRequestedNotification, | |
| notification_info); | |
| } | |
| // Check whether the current batch of events contains the event type. | |
| bool BrowserAccessibilityManagerMac::IsInGeneratedEventBatch( | |
| AXEventGenerator::Event event_type) const { | |
| for (const auto& event : event_generator()) { | |
| if (event.event_params->event == event_type) { | |
| return true; // Any side effects will have already been handled. | |
| } | |
| } | |
| return false; | |
| } | |
| void BrowserAccessibilityManagerMac::FireGeneratedEvent( | |
| AXEventGenerator::Event event_type, | |
| const AXNode* node) { | |
| BrowserAccessibilityManager::FireGeneratedEvent(event_type, node); | |
| BrowserAccessibility* wrapper = GetFromAXNode(node); | |
| DCHECK(wrapper); | |
| BrowserAccessibilityCocoa* native_node = | |
| base::apple::ObjCCastStrict<BrowserAccessibilityCocoa>( | |
| wrapper->GetNativeViewAccessible().Get()); | |
| // Refer to |AXObjectCache::postPlatformNotification| in WebKit source code. | |
| NSString* mac_notification = nullptr; | |
| switch (event_type) { | |
| case AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED: | |
| if (wrapper->GetRole() == ax::mojom::Role::kTree) { | |
| mac_notification = NSAccessibilitySelectedRowsChangedNotification; | |
| } else if (wrapper->GetRole() == | |
| ax::mojom::Role::kTextFieldWithComboBox) { | |
| // 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 AXEventGenerator::Event::ALERT: | |
| NSAccessibilityPostNotification( | |
| native_node, NSAccessibilityLiveRegionCreatedNotification); | |
| // Voiceover requires a live region changed notification to actually | |
| // announce the live region. | |
| FireGeneratedEvent(AXEventGenerator::Event::LIVE_REGION_CHANGED, node); | |
| return; | |
| case AXEventGenerator::Event::ARIA_CURRENT_CHANGED: | |
| // TODO(accessibility) Ask Apple for a notification. | |
| // There currently is none: | |
| // https://www.w3.org/TR/core-aam-1.2/#details-id-186 | |
| return; | |
| case AXEventGenerator::Event::BUSY_CHANGED: | |
| mac_notification = NSAccessibilityElementBusyChangedNotification; | |
| break; | |
| case AXEventGenerator::Event::CHECKED_STATE_CHANGED: | |
| mac_notification = NSAccessibilityValueChangedNotification; | |
| break; | |
| case AXEventGenerator::Event::COLLAPSED: | |
| if (wrapper->GetRole() == ax::mojom::Role::kRow || | |
| wrapper->GetRole() == ax::mojom::Role::kTreeItem) { | |
| mac_notification = NSAccessibilityRowCollapsedNotification; | |
| } else { | |
| mac_notification = NSAccessibilityExpandedChanged; | |
| } | |
| break; | |
| case AXEventGenerator::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. | |
| NSDictionary* user_info = GetUserInfoForSelectedTextChangedNotification(); | |
| BrowserAccessibilityManager* root_manager = GetManagerForRootFrame(); | |
| if (!root_manager) | |
| return; | |
| BrowserAccessibility* root = root_manager->GetBrowserAccessibilityRoot(); | |
| if (!root) | |
| return; | |
| NSAccessibilityPostNotificationWithUserInfo( | |
| focus->GetNativeViewAccessible().Get(), mac_notification, user_info); | |
| NSAccessibilityPostNotificationWithUserInfo( | |
| root->GetNativeViewAccessible().Get(), mac_notification, user_info); | |
| return; | |
| } | |
| case AXEventGenerator::Event::EXPANDED: | |
| if (wrapper->GetRole() == ax::mojom::Role::kRow || | |
| wrapper->GetRole() == ax::mojom::Role::kTreeItem) { | |
| mac_notification = NSAccessibilityRowExpandedNotification; | |
| } else { | |
| mac_notification = NSAccessibilityExpandedChanged; | |
| } | |
| break; | |
| case AXEventGenerator::Event::INVALID_STATUS_CHANGED: | |
| mac_notification = NSAccessibilityInvalidStatusChangedNotification; | |
| break; | |
| case AXEventGenerator::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; | |
| } | |
| BrowserAccessibilityManager* root_manager = GetManagerForRootFrame(); | |
| if (root_manager) { | |
| BrowserAccessibilityManagerMac* root_manager_mac = | |
| root_manager->ToBrowserAccessibilityManagerMac(); | |
| id window = root_manager_mac->GetWindow(); | |
| if ([window isKindOfClass:[NSAccessibilityRemoteUIElement class]]) { | |
| // NSAccessibilityLiveRegionChangedNotification seems to require | |
| // application be active. Use the announcement API to get around on | |
| // PWA. Announcement requires active window, so send the announcement | |
| // notification to the PWA related window. same work around like | |
| // https://chromium-review.googlesource.com/c/chromium/src/+/3257815 | |
| std::string live_status = | |
| node->GetStringAttribute(ax::mojom::StringAttribute::kLiveStatus); | |
| NSAccessibilityPriorityLevel priority_level = | |
| live_status == "assertive" ? NSAccessibilityPriorityHigh | |
| : NSAccessibilityPriorityMedium; | |
| PostAnnouncementNotification( | |
| base::SysUTF16ToNSString(wrapper->GetTextContentUTF16()), | |
| [root_manager_mac->GetParentView() window], priority_level); | |
| return; | |
| } | |
| } | |
| // Use native VoiceOver support for live regions. | |
| BrowserAccessibilityCocoa* retained_node = native_node; | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( | |
| FROM_HERE, | |
| base::BindOnce( | |
| [](BrowserAccessibilityCocoa* wrapper) { | |
| if (wrapper && [wrapper instanceActive]) { | |
| NSAccessibilityPostNotification( | |
| wrapper, NSAccessibilityLiveRegionChangedNotification); | |
| } | |
| }, | |
| retained_node), | |
| base::Milliseconds(kLiveRegionChangeIntervalMS)); | |
| return; | |
| } | |
| case AXEventGenerator::Event::LIVE_REGION_CREATED: | |
| mac_notification = NSAccessibilityLiveRegionCreatedNotification; | |
| break; | |
| case AXEventGenerator::Event::MENU_POPUP_END: | |
| // Calling NSAccessibilityPostNotification on a menu which is about to be | |
| // closed/destroyed is possible, but the event does not appear to be | |
| // emitted reliably by the NSAccessibility stack. If VoiceOver is not | |
| // notified that a given menu has been closed, it might fail to present | |
| // subsequent changes to the user. WebKit seems to address this by firing | |
| // AXMenuClosed on the document itself when an accessible menu is being | |
| // detached. See WebKit's AccessibilityObject::detachRemoteParts | |
| if (BrowserAccessibilityManager* root_manager = | |
| GetManagerForRootFrame()) { | |
| if (BrowserAccessibility* root = | |
| root_manager->GetBrowserAccessibilityRoot()) { | |
| FireNativeMacNotification((NSString*)kAXMenuClosedNotification, | |
| *root); | |
| } | |
| } | |
| return; | |
| case AXEventGenerator::Event::MENU_POPUP_START: | |
| mac_notification = (NSString*)kAXMenuOpenedNotification; | |
| break; | |
| case AXEventGenerator::Event::MENU_ITEM_SELECTED: | |
| mac_notification = NSAccessibilityMenuItemSelectedNotification; | |
| break; | |
| case AXEventGenerator::Event::RANGE_VALUE_CHANGED: | |
| DCHECK(wrapper->GetData().IsRangeValueSupported()) | |
| << "Range value changed but range values are not supported: " | |
| << wrapper; | |
| mac_notification = NSAccessibilityValueChangedNotification; | |
| break; | |
| case AXEventGenerator::Event::ROW_COUNT_CHANGED: | |
| mac_notification = NSAccessibilityRowCountChangedNotification; | |
| break; | |
| case AXEventGenerator::Event::SELECTED_CHILDREN_CHANGED: | |
| if (IsTableLike(wrapper->GetRole())) { | |
| mac_notification = NSAccessibilitySelectedRowsChangedNotification; | |
| } else { | |
| // VoiceOver does not read anything if selection changes on the | |
| // currently focused object, and the focus did not move. Fire a | |
| // selection change if the focus did not change. | |
| BrowserAccessibility* focus = GetFocus(); | |
| BrowserAccessibility* container = | |
| focus ? focus->PlatformGetSelectionContainer() : nullptr; | |
| if (focus && wrapper == container && | |
| container->HasState(ax::mojom::State::kMultiselectable) && | |
| !IsInGeneratedEventBatch( | |
| AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED) && | |
| !IsInGeneratedEventBatch(AXEventGenerator::Event::FOCUS_CHANGED)) { | |
| // Force announcement of current focus / activedescendant, even though | |
| // it's not changing. This way, the user can hear the new selection | |
| // state of the current object. Because VoiceOver ignores focus events | |
| // to an already focused object, this is done by destroying the native | |
| // object and creating a new one that receives focus. | |
| static_cast<BrowserAccessibilityMac*>(focus)->ReplaceNativeObject(); | |
| // Don't fire selected children change, it will sometimes override | |
| // announcement of current focus. | |
| return; | |
| } | |
| mac_notification = NSAccessibilitySelectedChildrenChangedNotification; | |
| } | |
| break; | |
| case AXEventGenerator::Event::SELECTED_VALUE_CHANGED: | |
| DCHECK(IsSelectElement(wrapper->GetRole())); | |
| mac_notification = NSAccessibilityValueChangedNotification; | |
| break; | |
| case AXEventGenerator::Event::VALUE_IN_TEXT_FIELD_CHANGED: | |
| DCHECK(wrapper->IsTextField()); | |
| mac_notification = NSAccessibilityValueChangedNotification; | |
| if (!text_edits_.empty()) { | |
| std::u16string deleted_text; | |
| std::u16string inserted_text; | |
| int32_t node_id = wrapper->GetId(); | |
| const auto iterator = text_edits_.find(node_id); | |
| id edit_text_marker = nil; | |
| if (iterator != text_edits_.end()) { | |
| AXTextEdit text_edit = iterator->second; | |
| deleted_text = text_edit.deleted_text; | |
| inserted_text = text_edit.inserted_text; | |
| edit_text_marker = text_edit.edit_text_marker; | |
| } | |
| NSDictionary* user_info = GetUserInfoForValueChangedNotification( | |
| native_node, deleted_text, inserted_text, edit_text_marker); | |
| BrowserAccessibility* root = GetBrowserAccessibilityRoot(); | |
| if (!root) | |
| return; | |
| NSAccessibilityPostNotificationWithUserInfo( | |
| native_node, mac_notification, user_info); | |
| NSAccessibilityPostNotificationWithUserInfo( | |
| root->GetNativeViewAccessible().Get(), mac_notification, user_info); | |
| return; | |
| } | |
| break; | |
| case AXEventGenerator::Event::NAME_CHANGED: | |
| mac_notification = NSAccessibilityTitleChangedNotification; | |
| break; | |
| case AXEventGenerator::Event::CHILDREN_CHANGED: | |
| [native_node childrenChanged]; | |
| return; | |
| // Currently unused events on this platform. | |
| case AXEventGenerator::Event::NONE: | |
| case AXEventGenerator::Event::ACCESS_KEY_CHANGED: | |
| case AXEventGenerator::Event::ARIA_NOTIFICATIONS_POSTED: | |
| case AXEventGenerator::Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED: | |
| case AXEventGenerator::Event::ATOMIC_CHANGED: | |
| case AXEventGenerator::Event::AUTO_COMPLETE_CHANGED: | |
| case AXEventGenerator::Event::AUTOFILL_AVAILABILITY_CHANGED: | |
| case AXEventGenerator::Event::CARET_BOUNDS_CHANGED: | |
| case AXEventGenerator::Event::CHECKED_STATE_DESCRIPTION_CHANGED: | |
| case AXEventGenerator::Event::CONTROLS_CHANGED: | |
| case AXEventGenerator::Event::DEFAULT_ACTION_VERB_CHANGED: | |
| case AXEventGenerator::Event::DETAILS_CHANGED: | |
| case AXEventGenerator::Event::DESCRIBED_BY_CHANGED: | |
| case AXEventGenerator::Event::DESCRIPTION_CHANGED: | |
| case AXEventGenerator::Event::DOCUMENT_TITLE_CHANGED: | |
| case AXEventGenerator::Event::EDITABLE_TEXT_CHANGED: | |
| case AXEventGenerator::Event::ENABLED_CHANGED: | |
| case AXEventGenerator::Event::FOCUS_CHANGED: | |
| case AXEventGenerator::Event::FLOW_FROM_CHANGED: | |
| case AXEventGenerator::Event::FLOW_TO_CHANGED: | |
| case AXEventGenerator::Event::GRAMMAR_MARKER_CHANGED: | |
| case AXEventGenerator::Event::HASPOPUP_CHANGED: | |
| case AXEventGenerator::Event::HIERARCHICAL_LEVEL_CHANGED: | |
| case AXEventGenerator::Event::HIGHLIGHT_MARKER_CHANGED: | |
| case AXEventGenerator::Event::IGNORED_CHANGED: | |
| case AXEventGenerator::Event::IMAGE_ANNOTATION_CHANGED: | |
| case AXEventGenerator::Event::KEY_SHORTCUTS_CHANGED: | |
| case AXEventGenerator::Event::LABELED_BY_CHANGED: | |
| case AXEventGenerator::Event::LANGUAGE_CHANGED: | |
| case AXEventGenerator::Event::LAYOUT_INVALIDATED: | |
| case AXEventGenerator::Event::LIVE_REGION_NODE_CHANGED: | |
| case AXEventGenerator::Event::LIVE_RELEVANT_CHANGED: | |
| case AXEventGenerator::Event::LIVE_STATUS_CHANGED: | |
| case AXEventGenerator::Event::MULTILINE_STATE_CHANGED: | |
| case AXEventGenerator::Event::MULTISELECTABLE_STATE_CHANGED: | |
| case AXEventGenerator::Event::OBJECT_ATTRIBUTE_CHANGED: | |
| case AXEventGenerator::Event::ORIENTATION_CHANGED: | |
| case AXEventGenerator::Event::PARENT_CHANGED: | |
| case AXEventGenerator::Event::PLACEHOLDER_CHANGED: | |
| case AXEventGenerator::Event::POSITION_IN_SET_CHANGED: | |
| case AXEventGenerator::Event::RANGE_VALUE_MAX_CHANGED: | |
| case AXEventGenerator::Event::RANGE_VALUE_MIN_CHANGED: | |
| case AXEventGenerator::Event::RANGE_VALUE_STEP_CHANGED: | |
| case AXEventGenerator::Event::READONLY_CHANGED: | |
| case AXEventGenerator::Event::RELATED_NODE_CHANGED: | |
| case AXEventGenerator::Event::REQUIRED_STATE_CHANGED: | |
| case AXEventGenerator::Event::ROLE_CHANGED: | |
| case AXEventGenerator::Event::SCROLL_HORIZONTAL_POSITION_CHANGED: | |
| case AXEventGenerator::Event::SCROLL_VERTICAL_POSITION_CHANGED: | |
| case AXEventGenerator::Event::SELECTED_CHANGED: | |
| case AXEventGenerator::Event::SET_SIZE_CHANGED: | |
| case AXEventGenerator::Event::SORT_CHANGED: | |
| case AXEventGenerator::Event::SPELLING_MARKER_CHANGED: | |
| case AXEventGenerator::Event::STATE_CHANGED: | |
| case AXEventGenerator::Event::SUBTREE_CREATED: | |
| case AXEventGenerator::Event::TEXT_ATTRIBUTE_CHANGED: | |
| case AXEventGenerator::Event::TEXT_SELECTION_CHANGED: | |
| case AXEventGenerator::Event::WIN_IACCESSIBLE_STATE_CHANGED: | |
| return; | |
| } | |
| FireNativeMacNotification(mac_notification, *wrapper); | |
| } | |
| void BrowserAccessibilityManagerMac::FireSentinelEventForTesting() { | |
| // The application deactivated event is used as an end-of-test signal because | |
| // it never occurs in tests. | |
| if (BrowserAccessibility* root = GetBrowserAccessibilityRoot()) { | |
| FireNativeMacNotification(NSAccessibilityApplicationDeactivatedNotification, | |
| *root); | |
| } | |
| } | |
| void BrowserAccessibilityManagerMac::FireAriaNotificationEvent( | |
| BrowserAccessibility* node, | |
| const std::string& announcement, | |
| ax::mojom::AriaNotificationPriority priority_property, | |
| ax::mojom::AriaNotificationInterrupt interrupt_property, | |
| const std::string& type) { | |
| CHECK(node); | |
| auto MapPropertiesToNSAccessibilityPriorityLevel = | |
| [&]() -> NSAccessibilityPriorityLevel { | |
| switch (priority_property) { | |
| case ax::mojom::AriaNotificationPriority::kNormal: | |
| return NSAccessibilityPriorityMedium; | |
| case ax::mojom::AriaNotificationPriority::kHigh: | |
| return NSAccessibilityPriorityHigh; | |
| } | |
| NOTREACHED(); | |
| }; | |
| PostAnnouncementNotification(base::SysUTF8ToNSString(announcement), | |
| node->GetNativeViewAccessible().Get(), | |
| MapPropertiesToNSAccessibilityPriorityLevel()); | |
| } | |
| void BrowserAccessibilityManagerMac::FireNativeMacNotification( | |
| NSString* mac_notification, | |
| BrowserAccessibility& node) { | |
| DCHECK(mac_notification); | |
| BrowserAccessibilityCocoa* native_node = | |
| base::apple::ObjCCastStrict<BrowserAccessibilityCocoa>( | |
| node.GetNativeViewAccessible().Get()); | |
| // The native node should not be null, but could theoretically be null if | |
| // events fire during tree mutations before platform nodes are fully | |
| // initialized. Events can fire early in AXTree::Unserialize via | |
| // AXTree::NotifyNodeWillBeReparentedOrDeleted, but platform node | |
| // initialization happens later via | |
| // BrowserAccessibilityManager::OnAtomicUpdateFinished. DCHECK to catch if | |
| // this occurs. | |
| DCHECK(native_node); | |
| if (!native_node) { | |
| return; | |
| } | |
| // TODO(accessibility) We should look into why background tabs return null for | |
| // GetWindow. Is it safe to fire notifications when there is no window? We've | |
| // had trouble in the past with "Chrome is not responding" lockups in AppKit | |
| // with VoiceOver, when firing events in detached documents. | |
| // DCHECK(GetWindow()); | |
| NSAccessibilityPostNotification(native_node, mac_notification); | |
| } | |
| bool BrowserAccessibilityManagerMac::OnAccessibilityEvents( | |
| AXUpdatesAndEvents& details) { | |
| text_edits_.clear(); | |
| return BrowserAccessibilityManager::OnAccessibilityEvents(details); | |
| } | |
| void BrowserAccessibilityManagerMac::OnAtomicUpdateFinished( | |
| 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) { | |
| if (change.node->HasState(ax::mojom::State::kEditable)) { | |
| auto* ancestor = change.node->GetTextFieldAncestor(); | |
| if (ancestor) { | |
| BrowserAccessibility* obj = GetFromAXNode(ancestor); | |
| const BrowserAccessibilityCocoa* editable_root = | |
| base::apple::ObjCCastStrict<BrowserAccessibilityCocoa>( | |
| obj->GetNativeViewAccessible().Get()); | |
| if ([editable_root instanceActive]) { | |
| changed_editable_roots.insert(editable_root); | |
| } | |
| } | |
| } | |
| #if defined(AX_EXTRA_MAC_NODES) | |
| // Update cached native children for tables. | |
| if (ui::IsTableLike(change.node->GetRole()) && | |
| change.type == AXTreeObserver::NODE_CHANGED) { | |
| BrowserAccessibilityMac* browser_accessibility = | |
| static_cast<BrowserAccessibilityMac*>(GetFromID(change.node->id())); | |
| [browser_accessibility->GetNativeWrapper() childrenChanged]; | |
| } | |
| #endif | |
| } | |
| for (const BrowserAccessibilityCocoa* obj : changed_editable_roots) { | |
| DCHECK(obj); | |
| const AXTextEdit text_edit = [obj computeTextEdit]; | |
| if (!text_edit.IsEmpty()) | |
| text_edits_[[obj owner]->GetId()] = text_edit; | |
| } | |
| } | |
| void BrowserAccessibilityManagerMac::OnNodeDataChanged( | |
| AXTree* tree, | |
| const AXNodeData& old_node_data, | |
| const AXNodeData& new_node_data) { | |
| BrowserAccessibilityMac* node = | |
| static_cast<BrowserAccessibilityMac*>(GetFromID(new_node_data.id)); | |
| CHECK(node); | |
| if (old_node_data.GetIntListAttribute( | |
| ax::mojom::IntListAttribute::kIndirectChildIds) != | |
| new_node_data.GetIntListAttribute( | |
| ax::mojom::IntListAttribute::kIndirectChildIds)) { | |
| [node->GetNativeWrapper() childrenChanged]; | |
| } | |
| if (old_node_data.IsInvisible() != new_node_data.IsInvisible()) { | |
| // Visibility changes whether a native node is ignored via | |
| // isAccessibilityElement(). | |
| BrowserAccessibilityMac* node_mac = | |
| static_cast<BrowserAccessibilityMac*>(node); | |
| BrowserAccessibilityCocoa* node_cocoa = node_mac->GetNativeWrapper(); | |
| if ([node_cocoa isAccessibilityElement]) { | |
| [node_cocoa childrenChanged]; | |
| } | |
| } | |
| } | |
| void BrowserAccessibilityManagerMac::OnSubtreeWillBeReparented(AXTree* tree, | |
| AXNode* node) { | |
| BrowserAccessibilityMac* node_mac = | |
| static_cast<BrowserAccessibilityMac*>(GetFromID(node->id())); | |
| BrowserAccessibilityCocoa* node_cocoa = node_mac->GetNativeWrapper(); | |
| if ([node_cocoa isAccessibilityElement]) { | |
| [node_cocoa childrenChanged]; | |
| } | |
| } | |
| NSDictionary* BrowserAccessibilityManagerMac:: | |
| GetUserInfoForSelectedTextChangedNotification() { | |
| NSMutableDictionary* user_info = [NSMutableDictionary dictionary]; | |
| user_info[NSAccessibilityTextStateSyncKey] = @YES; | |
| user_info[NSAccessibilityTextSelectionDirection] = | |
| @(AXTextSelectionDirectionUnknown); | |
| user_info[NSAccessibilityTextSelectionGranularity] = | |
| @(AXTextSelectionGranularityUnknown); | |
| user_info[NSAccessibilityTextSelectionChangedFocus] = @YES; | |
| // Try to detect when the text selection changes due to a focus change. | |
| // This is necessary so that VoiceOver also announces information about the | |
| // element that contains this selection. | |
| // TODO(mrobinson): Determine definitively what the type of this text | |
| // selection change is. This requires passing this information here from | |
| // blink. | |
| BrowserAccessibility* focus_object = GetFocus(); | |
| DCHECK(focus_object); | |
| if (focus_object != GetFromAXNode(GetLastFocusedNode())) { | |
| user_info[NSAccessibilityTextStateChangeTypeKey] = | |
| @(AXTextStateChangeTypeSelectionMove); | |
| } else { | |
| user_info[NSAccessibilityTextStateChangeTypeKey] = | |
| @(AXTextStateChangeTypeUnknown); | |
| } | |
| focus_object = focus_object->PlatformGetLowestPlatformAncestor(); | |
| BrowserAccessibilityCocoa* native_focus_object = | |
| base::apple::ObjCCast<BrowserAccessibilityCocoa>( | |
| focus_object->GetNativeViewAccessible().Get()); | |
| if (native_focus_object && [native_focus_object instanceActive]) { | |
| user_info[NSAccessibilityTextChangeElement] = native_focus_object; | |
| id selected_text = [native_focus_object selectedTextMarkerRange]; | |
| if (selected_text) { | |
| NSString* const NSAccessibilitySelectedTextMarkerRangeAttribute = | |
| @"AXSelectedTextMarkerRange"; | |
| user_info[NSAccessibilitySelectedTextMarkerRangeAttribute] = | |
| selected_text; | |
| } | |
| } | |
| return user_info; | |
| } | |
| NSDictionary* | |
| BrowserAccessibilityManagerMac::GetUserInfoForValueChangedNotification( | |
| const BrowserAccessibilityCocoa* native_node, | |
| const std::u16string& deleted_text, | |
| const std::u16string& inserted_text, | |
| id edit_text_marker) const { | |
| DCHECK(native_node); | |
| if (deleted_text.empty() && inserted_text.empty()) | |
| return nil; | |
| NSMutableArray* changes = [NSMutableArray array]; | |
| if (!deleted_text.empty()) { | |
| NSMutableDictionary* change = | |
| [NSMutableDictionary dictionaryWithDictionary:@{ | |
| NSAccessibilityTextEditType : @(AXTextEditTypeDelete), | |
| NSAccessibilityTextChangeValueLength : @(deleted_text.length()), | |
| NSAccessibilityTextChangeValue : | |
| base::SysUTF16ToNSString(deleted_text) | |
| }]; | |
| if (edit_text_marker) { | |
| change[NSAccessibilityChangeValueStartMarker] = edit_text_marker; | |
| } | |
| [changes addObject:change]; | |
| } | |
| if (!inserted_text.empty()) { | |
| // TODO(nektar): Figure out if this is a paste, insertion or typing. | |
| // Changes to Blink would be required. A heuristic is currently used. | |
| auto edit_type = inserted_text.length() > 1 ? @(AXTextEditTypeInsert) | |
| : @(AXTextEditTypeTyping); | |
| NSMutableDictionary* change = | |
| [NSMutableDictionary dictionaryWithDictionary:@{ | |
| NSAccessibilityTextEditType : edit_type, | |
| NSAccessibilityTextChangeValueLength : @(inserted_text.length()), | |
| NSAccessibilityTextChangeValue : | |
| base::SysUTF16ToNSString(inserted_text) | |
| }]; | |
| if (edit_text_marker) { | |
| change[NSAccessibilityChangeValueStartMarker] = edit_text_marker; | |
| } | |
| [changes addObject:change]; | |
| } | |
| return @{ | |
| NSAccessibilityTextStateChangeTypeKey : @(AXTextStateChangeTypeEdit), | |
| NSAccessibilityTextChangeValues : changes, | |
| NSAccessibilityTextChangeElement : native_node | |
| }; | |
| } | |
| id BrowserAccessibilityManagerMac::GetParentView() { | |
| return delegate()->AccessibilityGetNativeViewAccessible().Get(); | |
| } | |
| id BrowserAccessibilityManagerMac::GetWindow() { | |
| return delegate()->AccessibilityGetNativeViewAccessibleForWindow().Get(); | |
| } | |
| bool BrowserAccessibilityManagerMac::ShouldFireLoadCompleteNotification() { | |
| // If it's not the top-level document, we shouldn't fire AXLoadComplete. | |
| if (!IsRootFrameManager()) { | |
| return false; | |
| } | |
| // Voiceover moves focus to the web content when it receives an | |
| // AXLoadComplete event. On Chrome's new tab page, focus should stay | |
| // in the omnibox, so we purposefully do not fire the AXLoadComplete | |
| // event in this case. | |
| if (delegate()->ShouldSuppressAXLoadComplete()) { | |
| return false; | |
| } | |
| CHECK(delegate()->AccessibilityIsWebContentSource()) | |
| << "We should never get here in non-web content sourced managers."; | |
| // We also check that the window is focused because VoiceOver responds | |
| // to this notification by changing focus and possibly reading the entire | |
| // page contents, sometimes even when the window is minimized or another | |
| // Chrome window is active/focused. | |
| id window = GetWindow(); | |
| if (!window) { | |
| return false; | |
| } | |
| if ([NSApp isActive]) { | |
| return window == [NSApp accessibilityFocusedWindow]; | |
| } | |
| // TODO(accessibility): We need a solution to the problem described below. | |
| // If the window is NSAccessibilityRemoteUIElement, there are some challenges: | |
| // 1. NSApp is the browser which spawned the PWA, and what it considers the | |
| // accessibilityFocusedWindow is the last browser window which was focused | |
| // prior to the PWA gaining focus; not the potentially-focused PWA window. | |
| // 2. Unlike the BrowserNativeWidgetWindow, NSAccessibilityRemoteUIElement is | |
| // not an NSWindow and doesn't respond to the selector isKeyWindow. So we | |
| // cannot simply verify we have the key window for the currently running | |
| // application. | |
| // 3. NSAccessibilityRemoteUIElement does not conform to the NSAccessibility | |
| // protocol, so we cannot ask it for any properties that might let us | |
| // verify this window is focused. | |
| // 4. AppKit does not allow us to access the actual NSWindow instances of | |
| // other NSRunningApplications (i.e. the shim process); just window | |
| // information, which does not appear to include details regarding what | |
| // is active/focused. | |
| // 5. Attempting to get at the accessibility tree of the shim process via | |
| // AXUIElementCreateApplication is possible, but the objects retrieved | |
| // in that fashion do not conform to the NSAccessibility protocol. | |
| // For now we'll return true to preserve current behavior. Note, however, | |
| // that this does not necessarily mean the event will be presented by | |
| // VoiceOver in the same way it would present a normal browser window. | |
| // This may be due to the issues described above, or the fact that one | |
| // cannot ascend the accessibility tree all the way to the parent window | |
| // from within the app shim content. | |
| if ([window isKindOfClass:[NSAccessibilityRemoteUIElement class]]) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| } // namespace ui |