// 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 "ui/accessibility/platform/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 "ui/accessibility/platform/browser_accessibility_cocoa.h"
#include "ui/accessibility/platform/browser_accessibility_manager_mac.h"
namespace ui {
// static
std::unique_ptr<BrowserAccessibility> BrowserAccessibility::Create(
BrowserAccessibilityManager* manager,
AXNode* node) {
return base::WrapUnique(new BrowserAccessibilityMac(manager, node));
}
BrowserAccessibilityMac::BrowserAccessibilityMac(
BrowserAccessibilityManager* manager,
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_IN_MIGRATION()
<< "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_IN_MIGRATION()
<< "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<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<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<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();
}
AXPlatformNode* BrowserAccessibilityMac::GetAXPlatformNode() const {
return platform_node_;
}
void BrowserAccessibilityMac::CreatePlatformNodes() {
DCHECK(!platform_node_);
platform_node_ =
static_cast<AXPlatformNodeMac*>(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 ui