chromium/ui/accessibility/platform/browser_accessibility_mac.mm

// 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