| // 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. |
| |
| #import "content/browser/accessibility/browser_accessibility_mac.h" |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #include "base/debug/stack_trace.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/scoped_policy.h" |
| #import "base/task/single_thread_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #import "content/browser/accessibility/browser_accessibility_cocoa.h" |
| #include "content/browser/accessibility/browser_accessibility_manager_mac.h" |
| |
| namespace content { |
| |
| // static |
| std::unique_ptr<BrowserAccessibility> BrowserAccessibility::Create( |
| BrowserAccessibilityManager* manager, |
| ui::AXNode* node) { |
| return base::WrapUnique(new BrowserAccessibilityMac(manager, node)); |
| } |
| |
| BrowserAccessibilityMac::BrowserAccessibilityMac( |
| BrowserAccessibilityManager* manager, |
| ui::AXNode* node) |
| : BrowserAccessibility(manager, node) {} |
| |
| BrowserAccessibilityMac::~BrowserAccessibilityMac() { |
| if (platform_node_) { |
| // `Destroy()` also deletes the object. |
| platform_node_.ExtractAsDangling()->Destroy(); |
| } |
| } |
| |
| BrowserAccessibilityCocoa* BrowserAccessibilityMac::GetNativeWrapper() const { |
| return platform_node_ ? static_cast<BrowserAccessibilityCocoa*>( |
| platform_node_->GetNativeWrapper()) |
| : nullptr; |
| } |
| |
| void BrowserAccessibilityMac::OnDataChanged() { |
| BrowserAccessibility::OnDataChanged(); |
| |
| if (GetNativeWrapper()) { |
| [GetNativeWrapper() childrenChanged]; |
| return; |
| } |
| |
| CreatePlatformNodes(); |
| } |
| |
| // Replace a native object and refocus if it had focus. |
| // This will force VoiceOver to re-announce it, and refresh Braille output. |
| void BrowserAccessibilityMac::ReplaceNativeObject() { |
| // Since our native wrapper is owned by a platform node, in order to replace |
| // the wrapper, a platform node should always be present. In other words, we |
| // could have never called this method without a platform node having been |
| // created. |
| if (!platform_node_) { |
| NOTREACHED() << "No platform node exists, so there should not be any " |
| "native wrapper to replace."; |
| return; |
| } |
| |
| // We need to keep the old native wrapper alive until we set up the new one |
| // because we need to retrieve some information from the old wrapper in order |
| // to add it to the new one, e.g. its list of children. |
| AXPlatformNodeCocoa* old_native_obj = platform_node_->ReleaseNativeWrapper(); |
| |
| // We should have never called this method if a native wrapper has not been |
| // created, but keep a null check just in case. |
| if (!old_native_obj) { |
| NOTREACHED() << "No native wrapper exists, so there is nothing to replace."; |
| return; |
| } |
| |
| // Replace child in parent. |
| BrowserAccessibility* parent = PlatformGetParent(); |
| if (!parent) |
| return; |
| |
| // Re-create native wrapper and also take ownership of that wrapper in |
| // `platform_node_` relinquishing the ownership of the old wrapper. |
| BrowserAccessibilityCocoa* new_native_obj = CreateNativeWrapper(); |
| |
| // Rebuild children to pick up a newly created cocoa object. |
| [parent->GetNativeViewAccessible() childrenChanged]; |
| |
| // If focused, fire a focus notification on the new native object. |
| if (manager_->GetFocus() == this) { |
| NSAccessibilityPostNotification( |
| new_native_obj, NSAccessibilityFocusedUIElementChangedNotification); |
| } |
| |
| // Postpone the old native wrapper destruction. It will be destroyed after |
| // a delay so that VO is securely on the new focus first (otherwise the focus |
| // event will not be announced). |
| // We use 1000ms; however, this magic number isn't necessary to avoid |
| // use-after-free or anything scary like that. The worst case scenario if this |
| // gets destroyed too early is that VoiceOver announces the wrong thing once. |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](AXPlatformNodeCocoa* destroyed) { |
| if (destroyed && [destroyed instanceActive]) { |
| // Follow destruction pattern from NativeReleaseReference(). |
| [destroyed detach]; |
| } |
| }, |
| old_native_obj), |
| base::Milliseconds(1000)); |
| } |
| |
| size_t BrowserAccessibilityMac::PlatformChildCount() const { |
| size_t child_count = BrowserAccessibility::PlatformChildCount(); |
| |
| // If this is a table, include the extra fake nodes generated by |
| // AXTableInfo, for the column nodes and the table header container, all of |
| // which are only important on macOS. |
| const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>* extra_mac_nodes = |
| node()->GetExtraMacNodes(); |
| if (!extra_mac_nodes) |
| return child_count; |
| |
| return child_count + extra_mac_nodes->size(); |
| } |
| |
| BrowserAccessibility* BrowserAccessibilityMac::PlatformGetChild( |
| size_t child_index) const { |
| if (child_index < BrowserAccessibility::PlatformChildCount()) |
| return BrowserAccessibility::PlatformGetChild(child_index); |
| |
| if (child_index >= PlatformChildCount()) |
| return nullptr; |
| |
| // If this is a table, include the extra fake nodes generated by |
| // AXTableInfo, for the column nodes and the table header container, all of |
| // which are only important on macOS. |
| const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>* extra_mac_nodes = |
| node()->GetExtraMacNodes(); |
| if (!extra_mac_nodes || extra_mac_nodes->empty()) |
| return nullptr; |
| |
| child_index -= BrowserAccessibility::PlatformChildCount(); |
| if (child_index < extra_mac_nodes->size()) |
| return manager_->GetFromAXNode((*extra_mac_nodes)[child_index]); |
| |
| return nullptr; |
| } |
| |
| BrowserAccessibility* BrowserAccessibilityMac::PlatformGetFirstChild() const { |
| return PlatformGetChild(0); |
| } |
| |
| BrowserAccessibility* BrowserAccessibilityMac::PlatformGetLastChild() const { |
| const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>* extra_mac_nodes = |
| node()->GetExtraMacNodes(); |
| if (extra_mac_nodes && !extra_mac_nodes->empty()) |
| return manager_->GetFromAXNode(extra_mac_nodes->back()); |
| return BrowserAccessibility::PlatformGetLastChild(); |
| } |
| |
| BrowserAccessibility* BrowserAccessibilityMac::PlatformGetNextSibling() const { |
| BrowserAccessibility* parent = PlatformGetParent(); |
| if (parent) { |
| size_t next_child_index = node()->GetUnignoredIndexInParent() + 1; |
| if (next_child_index >= parent->InternalChildCount() && |
| next_child_index < parent->PlatformChildCount()) { |
| // Get the extra_mac_node. |
| return parent->PlatformGetChild(next_child_index); |
| } else if (next_child_index >= parent->PlatformChildCount()) { |
| return nullptr; |
| } |
| } |
| return BrowserAccessibility::PlatformGetNextSibling(); |
| } |
| |
| BrowserAccessibility* BrowserAccessibilityMac::PlatformGetPreviousSibling() |
| const { |
| BrowserAccessibility* parent = PlatformGetParent(); |
| if (parent) { |
| size_t child_index = node()->GetUnignoredIndexInParent(); |
| if (child_index > parent->InternalChildCount() && |
| child_index <= parent->PlatformChildCount()) { |
| // Get the extra_mac_node. |
| return parent->PlatformGetChild(child_index - 1); |
| } else if (child_index == 0) { |
| return nullptr; |
| } |
| } |
| return BrowserAccessibility::PlatformGetPreviousSibling(); |
| } |
| |
| gfx::NativeViewAccessible BrowserAccessibilityMac::GetNativeViewAccessible() { |
| return GetNativeWrapper(); |
| } |
| |
| ui::AXPlatformNode* BrowserAccessibilityMac::GetAXPlatformNode() const { |
| return platform_node_; |
| } |
| |
| void BrowserAccessibilityMac::CreatePlatformNodes() { |
| DCHECK(!platform_node_); |
| platform_node_ = |
| static_cast<ui::AXPlatformNodeMac*>(ui::AXPlatformNode::Create(this)); |
| CreateNativeWrapper(); |
| } |
| |
| BrowserAccessibilityCocoa* BrowserAccessibilityMac::CreateNativeWrapper() { |
| DCHECK(platform_node_); |
| |
| BrowserAccessibilityCocoa* node_cocoa = |
| [[BrowserAccessibilityCocoa alloc] initWithObject:this |
| withPlatformNode:platform_node_]; |
| |
| platform_node_->SetNativeWrapper(node_cocoa); |
| return node_cocoa; |
| } |
| |
| } // namespace content |