blob: 945801f69de6c8c70eb602c7f0ada73420136b94 [file] [log] [blame]
// Copyright 2014 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/ax_platform_node_base.h"
#include <algorithm>
#include <iomanip>
#include <limits>
#include <set>
#include <sstream>
#include <string>
#include <utility>
#include "base/check.h"
#include "base/check_deref.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/numerics/checked_math.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/trace_event/memory_allocator_dump.h"
#include "base/trace_event/memory_dump_manager.h"
#include "base/trace_event/memory_dump_provider.h"
#include "base/trace_event/process_memory_dump.h"
#include "build/build_config.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom-shared-internal.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_selection.h"
#include "ui/accessibility/ax_tree_data.h"
#include "ui/accessibility/platform/ax_platform_node_delegate.h"
#include "ui/accessibility/platform/compute_attributes.h"
#include "ui/gfx/geometry/rect_conversions.h"
namespace ui {
namespace {
using OnNotifyEventCallbackMap =
std::map<ax::mojom::Event,
// A function to call when focus changes, for testing only.
base::RepeatingClosure>;
OnNotifyEventCallbackMap& GetOnNotifyEventCallbackMap() {
static base::NoDestructor<OnNotifyEventCallbackMap>
on_notify_event_for_testing;
return *on_notify_event_for_testing;
}
// Check for descendant comment, using limited depth first search.
bool FindDescendantRoleWithMaxDepth(const AXPlatformNodeBase* node,
ax::mojom::Role descendant_role,
size_t max_depth,
size_t max_children_to_check) {
if (node->GetRole() == descendant_role)
return true;
if (max_depth <= 1)
return false;
size_t num_children_to_check =
std::min(node->GetChildCount(), max_children_to_check);
for (size_t index = 0; index < num_children_to_check; index++) {
auto* child = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(node->ChildAtIndex(index)));
if (child &&
FindDescendantRoleWithMaxDepth(child, descendant_role, max_depth - 1,
max_children_to_check)) {
return true;
}
}
return false;
}
// Map from each AXPlatformNode's unique id to its instance.
using UniqueIdMap =
absl::flat_hash_map<int32_t, raw_ptr<AXPlatformNode, CtnExperimental>>;
UniqueIdMap& GetUniqueIdMap() {
static base::NoDestructor<UniqueIdMap> map;
return *map;
}
// Adds process-wide statistics about accessibility objects to traces.
class AXPlatformNodeMemoryDumpProvider
: public base::trace_event::MemoryDumpProvider {
public:
AXPlatformNodeMemoryDumpProvider(const AXPlatformNodeMemoryDumpProvider&) =
delete;
AXPlatformNodeMemoryDumpProvider& operator=(
const AXPlatformNodeMemoryDumpProvider&) = delete;
// base::trace_event::MemoryDumpProvider:
bool OnMemoryDump(const base::trace_event::MemoryDumpArgs& args,
base::trace_event::ProcessMemoryDump* pmd) override;
private:
friend class base::NoDestructor<AXPlatformNodeMemoryDumpProvider>;
explicit AXPlatformNodeMemoryDumpProvider(const UniqueIdMap& id_to_node);
~AXPlatformNodeMemoryDumpProvider() override = default;
const raw_ref<const UniqueIdMap> id_to_node_;
};
bool AXPlatformNodeMemoryDumpProvider::OnMemoryDump(
const base::trace_event::MemoryDumpArgs& args,
base::trace_event::ProcessMemoryDump* pmd) {
auto* const dump = pmd->CreateAllocatorDump("accessibility/ax_platform_node");
dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameObjectCount,
base::trace_event::MemoryAllocatorDump::kUnitsObjects,
id_to_node_->size());
return true;
}
AXPlatformNodeMemoryDumpProvider::AXPlatformNodeMemoryDumpProvider(
const UniqueIdMap& id_to_node)
: id_to_node_(id_to_node) {
// Skip this in tests that don't set up a task runner on the main thread.
if (base::SingleThreadTaskRunner::HasCurrentDefault()) {
base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider(
this, "AXPlatformNode",
base::SingleThreadTaskRunner::GetCurrentDefault());
}
}
} // namespace
const char16_t AXPlatformNodeBase::kEmbeddedCharacter = u'\xfffc';
const std::string AXPlatformNodeBase::kAriaActionsPrefix = "custom";
// TODO(fxbug.dev/91030): Remove the !BUILDFLAG(IS_FUCHSIA) condition once
// fuchsia has native accessibility.
#if !BUILDFLAG(HAS_NATIVE_ACCESSIBILITY) && !BUILDFLAG(IS_FUCHSIA)
// static
AXPlatformNode::Pointer AXPlatformNode::Create(
AXPlatformNodeDelegate& delegate) {
AXPlatformNodeBase* node = new AXPlatformNodeBase();
node->Init(delegate);
return Pointer(node);
}
#endif
// static
AXPlatformNode* AXPlatformNodeBase::GetFromUniqueId(int32_t unique_id) {
UniqueIdMap& unique_ids = GetUniqueIdMap();
auto iter = unique_ids.find(unique_id);
if (iter != unique_ids.end()) {
return iter->second;
}
return nullptr;
}
// static
size_t AXPlatformNodeBase::GetInstanceCount() {
return GetUniqueIdMap().size();
}
// static
size_t AXPlatformNodeBase::ResetInstanceCountForTesting() {
auto& id_map = GetUniqueIdMap();
const auto result = id_map.size();
id_map.clear();
return result;
}
// static
void AXPlatformNodeBase::SetOnNotifyEventCallbackForTesting(
ax::mojom::Event event_type,
base::RepeatingClosure callback) {
OnNotifyEventCallbackMap& callback_map = GetOnNotifyEventCallbackMap();
callback_map[event_type] = std::move(callback);
}
AXPlatformNodeBase::AXPlatformNodeBase() = default;
AXPlatformNodeBase::~AXPlatformNodeBase() = default;
void AXPlatformNodeBase::Init(AXPlatformNodeDelegate& delegate) {
CHECK(!delegate_);
delegate_ = &delegate;
// This must be called after assigning our delegate.
GetUniqueIdMap()[GetUniqueId()] = this;
static base::NoDestructor<AXPlatformNodeMemoryDumpProvider> dump_provider(
GetUniqueIdMap());
}
const AXNodeData& AXPlatformNodeBase::GetData() const {
return GetDelegate()->GetData();
}
gfx::NativeViewAccessible AXPlatformNodeBase::GetFocus() const {
return GetDelegate()->GetFocus();
}
gfx::NativeViewAccessible AXPlatformNodeBase::GetParent() const {
return GetDelegate()->GetParent();
}
AXPlatformNodeBase* AXPlatformNodeBase::GetPlatformParent() const {
return FromNativeViewAccessible(GetDelegate()->GetParent());
}
AXPlatformNodeBase* AXPlatformNodeBase::GetPlatformTextFieldAncestor() const {
return FromNativeViewAccessible(GetDelegate()->GetTextFieldAncestor());
}
size_t AXPlatformNodeBase::GetChildCount() const {
return GetDelegate()->GetChildCount();
}
gfx::NativeViewAccessible AXPlatformNodeBase::ChildAtIndex(size_t index) const {
return GetDelegate()->ChildAtIndex(index);
}
std::string AXPlatformNodeBase::GetName() const {
AXPlatformNodeDelegate* const delegate = GetDelegate();
std::string name = delegate->GetName();
// Compute extra name based on the image annotation (generated alt text)
// results.
std::string extra_text;
ax::mojom::ImageAnnotationStatus status =
GetData().GetImageAnnotationStatus();
switch (status) {
case ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation:
case ax::mojom::ImageAnnotationStatus::kAnnotationPending:
case ax::mojom::ImageAnnotationStatus::kAnnotationEmpty:
case ax::mojom::ImageAnnotationStatus::kAnnotationAdult:
case ax::mojom::ImageAnnotationStatus::kAnnotationProcessFailed:
extra_text = base::UTF16ToUTF8(
delegate->GetLocalizedStringForImageAnnotationStatus(status));
break;
case ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded:
extra_text =
GetStringAttribute(ax::mojom::StringAttribute::kImageAnnotation);
break;
case ax::mojom::ImageAnnotationStatus::kNone:
case ax::mojom::ImageAnnotationStatus::kWillNotAnnotateDueToScheme:
case ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation:
case ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation:
break;
}
if (!extra_text.empty()) {
if (!name.empty()) {
name += ". ";
}
name += extra_text;
}
DCHECK(base::IsStringUTF8AllowingNoncharacters(name)) << "Invalid UTF8";
return name;
}
std::optional<size_t> AXPlatformNodeBase::GetIndexInParent() {
AXPlatformNodeBase* parent = FromNativeViewAccessible(GetParent());
if (!parent) {
return std::nullopt;
}
// If this is the webview, it is not in the child in the list of its parent's
// child.
// TODO(jkim): Check if we could remove this after making WebView ignored.
AXPlatformNodeDelegate* const delegate = GetDelegate();
if (delegate->GetNativeViewAccessible() != GetNativeViewAccessible()) {
return std::nullopt;
}
size_t child_count = parent->GetChildCount();
if (child_count == 0) {
// |child_count| could be 0 if the parent is IsLeaf.
DCHECK(parent->IsLeaf());
return std::nullopt;
}
// Ask the delegate for the index in parent, and return it if it's plausible.
//
// Delegates are allowed to not implement this (ViewsAXPlatformNodeDelegate
// returns -1). Also, delegates may not know the correct answer if this
// node is the root of a tree that's embedded in another tree, in which
// case the delegate should return -1 and we'll compute it.
auto index = delegate->GetIndexInParent();
if (index.has_value() && index.value() < child_count)
return index;
// Otherwise, search the parent's children.
gfx::NativeViewAccessible current = GetNativeViewAccessible();
for (size_t i = 0; i < child_count; i++) {
if (parent->ChildAtIndex(i) == current)
return i;
}
// If the parent has a modal dialog, it doesn't count other children.
if (parent->GetDelegate()->HasModalDialog()) {
return std::nullopt;
}
DCHECK(false)
<< "Unable to find the child in the list of its parent's children.";
return std::nullopt;
}
base::stack<gfx::NativeViewAccessible> AXPlatformNodeBase::GetAncestors() {
base::stack<gfx::NativeViewAccessible> ancestors;
gfx::NativeViewAccessible current_node = GetNativeViewAccessible();
while (current_node) {
AXPlatformNodeBase* current_platform_node =
FromNativeViewAccessible(current_node);
if (!current_platform_node) {
break;
}
ancestors.push(current_node);
current_node = current_platform_node->GetParent();
}
return ancestors;
}
std::optional<int> AXPlatformNodeBase::CompareTo(AXPlatformNodeBase& other) {
// We define two node's relative positions in the following way:
// 1. this->CompareTo(other) == 0:
// - |this| and |other| are the same node.
// 2. this->CompareTo(other) < 0:
// - |this| is an ancestor of |other|.
// - |this|'s first uncommon ancestor comes before |other|'s first uncommon
// ancestor. The first uncommon ancestor is defined as the immediate child
// of the lowest common ancestor of the two nodes. The first uncommon
// ancestor of |this| and |other| share the same parent (i.e. lowest common
// ancestor), so we can just compare the first uncommon ancestors' child
// indices to determine their relative positions.
// 3. this->CompareTo(other) == nullopt:
// - |this| and |other| are not comparable. E.g. they do not have a common
// ancestor.
//
// Another way to look at the nodes' relative positions/logical orders is that
// they are equivalent to pre-order traversal of the tree. If we pre-order
// traverse from the root, the node that we visited earlier is always going to
// be before (logically less) the node we visit later.
if (this == &other) {
return std::optional<int>(0);
}
// Compute the ancestor stacks of both positions and traverse them from the
// top most ancestor down, so we can discover the first uncommon ancestors.
// The first uncommon ancestor is the immediate child of the lowest common
// ancestor.
gfx::NativeViewAccessible common_ancestor = gfx::NativeViewAccessible();
base::stack<gfx::NativeViewAccessible> our_ancestors = GetAncestors();
base::stack<gfx::NativeViewAccessible> other_ancestors = other.GetAncestors();
// Start at the root and traverse down. Keep going until the |this|'s ancestor
// chain and |other|'s ancestor chain disagree. The last node before they
// disagree is the lowest common ancestor.
while (!our_ancestors.empty() && !other_ancestors.empty() &&
our_ancestors.top() == other_ancestors.top()) {
common_ancestor = our_ancestors.top();
our_ancestors.pop();
other_ancestors.pop();
}
// Nodes do not have a common ancestor, they are not comparable.
if (!common_ancestor) {
return std::nullopt;
}
// Compute the logical order when the common ancestor is |this| or |other|.
auto* common_ancestor_platform_node =
FromNativeViewAccessible(common_ancestor);
if (common_ancestor_platform_node == this) {
return std::optional<int>(-1);
}
if (common_ancestor_platform_node == &other) {
return std::optional<int>(1);
}
// Compute the logical order of |this| and |other| by using their first
// uncommon ancestors.
if (!our_ancestors.empty() && !other_ancestors.empty()) {
std::optional<int> this_index_in_parent =
FromNativeViewAccessible(our_ancestors.top())->GetIndexInParent();
std::optional<int> other_index_in_parent =
FromNativeViewAccessible(other_ancestors.top())->GetIndexInParent();
if (!this_index_in_parent || !other_index_in_parent) {
return std::nullopt;
}
int this_uncommon_ancestor_index = this_index_in_parent.value();
int other_uncommon_ancestor_index = other_index_in_parent.value();
DCHECK_NE(this_uncommon_ancestor_index, other_uncommon_ancestor_index)
<< "Deepest uncommon ancestors should truly be uncommon, i.e. not "
"the same.";
return std::optional<int>(this_uncommon_ancestor_index -
other_uncommon_ancestor_index);
}
return std::nullopt;
}
AXNodeID AXPlatformNodeBase::GetNodeId() const {
return GetDelegate()->GetData().id;
}
AXPlatformNodeBase* AXPlatformNodeBase::GetActiveDescendant() const {
AXNodeID active_descendant_id;
AXPlatformNodeBase* active_descendant = nullptr;
if (GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId,
&active_descendant_id)) {
active_descendant = static_cast<AXPlatformNodeBase*>(
GetDelegate()->GetFromNodeID(active_descendant_id));
}
if (GetRole() == ax::mojom::Role::kComboBoxSelect) {
AXPlatformNodeBase* child = GetFirstChild();
if (child && child->GetRole() == ax::mojom::Role::kMenuListPopup &&
!child->IsInvisibleOrIgnored()) {
// The active descendant is found on the menu list popup, i.e. on the
// actual list and not on the button that opens it.
// If there is no active descendant, focus should stay on the button so
// that Windows screen readers would enable their virtual cursor.
// Do not expose an activedescendant in a hidden/collapsed list, as
// screen readers expect the focus event to go to the button itself.
// Note that the AX hierarchy in this case is strange -- the active
// option is the only visible option, and is inside an invisible list.
if (child->GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId,
&active_descendant_id)) {
active_descendant = static_cast<AXPlatformNodeBase*>(
child->GetDelegate()->GetFromNodeID(active_descendant_id));
}
}
}
if (active_descendant && !active_descendant->IsInvisibleOrIgnored())
return active_descendant;
return nullptr;
}
// AXPlatformNode overrides.
void AXPlatformNodeBase::Destroy() {
GetUniqueIdMap().erase(GetUniqueId());
delegate_ = nullptr;
Dispose();
}
bool AXPlatformNodeBase::IsDestroyed() const {
return !delegate_;
}
void AXPlatformNodeBase::Dispose() {
delete this;
}
gfx::NativeViewAccessible AXPlatformNodeBase::GetNativeViewAccessible() {
return gfx::NativeViewAccessible();
}
void AXPlatformNodeBase::NotifyAccessibilityEvent(ax::mojom::Event event_type) {
if (event_type == ax::mojom::Event::kAlert) {
CHECK(IsAlert(GetRole()))
<< "On some platforms, the alert event does not work correctly unless "
"it is fired on an object with an alert role. Role was "
<< GetRole();
}
OnNotifyEventCallbackMap& callback_map = GetOnNotifyEventCallbackMap();
if (callback_map.find(event_type) != callback_map.end() &&
callback_map[event_type]) {
callback_map[event_type].Run();
}
}
#if BUILDFLAG(IS_APPLE)
void AXPlatformNodeBase::AnnounceTextAs(const std::u16string& text,
AnnouncementType announcement_type) {}
#endif
std::string AXPlatformNodeBase::GetRootURL() const {
return GetDelegate()->GetRootURL();
}
bool AXPlatformNodeBase::IsWebContent() const {
return GetDelegate()->IsWebContent();
}
AXPlatformNodeDelegate* AXPlatformNodeBase::GetDelegate() const {
return &CHECK_DEREF(delegate_.get());
}
bool AXPlatformNodeBase::IsDescendantOf(AXPlatformNode* ancestor) const {
if (!ancestor)
return false;
if (this == ancestor)
return true;
AXPlatformNodeBase* parent = FromNativeViewAccessible(GetParent());
if (!parent)
return false;
return parent->IsDescendantOf(ancestor);
}
AXPlatformNodeBase::AXPlatformNodeChildIterator
AXPlatformNodeBase::AXPlatformNodeChildrenBegin() const {
return AXPlatformNodeChildIterator(this, GetFirstChild());
}
AXPlatformNodeBase::AXPlatformNodeChildIterator
AXPlatformNodeBase::AXPlatformNodeChildrenEnd() const {
return AXPlatformNodeChildIterator(this, nullptr);
}
// Helpers.
AXPlatformNodeBase* AXPlatformNodeBase::GetPreviousSibling() const {
return FromNativeViewAccessible(GetDelegate()->GetPreviousSibling());
}
AXPlatformNodeBase* AXPlatformNodeBase::GetNextSibling() const {
return FromNativeViewAccessible(GetDelegate()->GetNextSibling());
}
AXPlatformNodeBase* AXPlatformNodeBase::GetFirstChild() const {
return FromNativeViewAccessible(GetDelegate()->GetFirstChild());
}
AXPlatformNodeBase* AXPlatformNodeBase::GetLastChild() const {
return FromNativeViewAccessible(GetDelegate()->GetLastChild());
}
ax::mojom::Role AXPlatformNodeBase::GetRole() const {
return GetDelegate()->GetRole();
}
bool AXPlatformNodeBase::HasBoolAttribute(
ax::mojom::BoolAttribute attribute) const {
return GetDelegate()->HasBoolAttribute(attribute);
}
bool AXPlatformNodeBase::GetBoolAttribute(
ax::mojom::BoolAttribute attribute) const {
return GetDelegate()->GetBoolAttribute(attribute);
}
bool AXPlatformNodeBase::GetBoolAttribute(ax::mojom::BoolAttribute attribute,
bool* value) const {
return GetDelegate()->GetBoolAttribute(attribute, value);
}
bool AXPlatformNodeBase::HasFloatAttribute(
ax::mojom::FloatAttribute attribute) const {
return GetDelegate()->HasFloatAttribute(attribute);
}
float AXPlatformNodeBase::GetFloatAttribute(
ax::mojom::FloatAttribute attribute) const {
return GetDelegate()->GetFloatAttribute(attribute);
}
bool AXPlatformNodeBase::GetFloatAttribute(ax::mojom::FloatAttribute attribute,
float* value) const {
return GetDelegate()->GetFloatAttribute(attribute, value);
}
const AXIntAttributes& AXPlatformNodeBase::GetIntAttributes() const {
return GetDelegate()->GetIntAttributes();
}
bool AXPlatformNodeBase::HasIntAttribute(
ax::mojom::IntAttribute attribute) const {
return GetDelegate()->HasIntAttribute(attribute);
}
int AXPlatformNodeBase::GetIntAttribute(
ax::mojom::IntAttribute attribute) const {
return GetDelegate()->GetIntAttribute(attribute);
}
bool AXPlatformNodeBase::GetIntAttribute(ax::mojom::IntAttribute attribute,
int* value) const {
return GetDelegate()->GetIntAttribute(attribute, value);
}
const AXStringAttributes& AXPlatformNodeBase::GetStringAttributes() const {
return GetDelegate()->GetStringAttributes();
}
bool AXPlatformNodeBase::HasStringAttribute(
ax::mojom::StringAttribute attribute) const {
return GetDelegate()->HasStringAttribute(attribute);
}
const std::string& AXPlatformNodeBase::GetStringAttribute(
ax::mojom::StringAttribute attribute) const {
return GetDelegate()->GetStringAttribute(attribute);
}
bool AXPlatformNodeBase::GetStringAttribute(
ax::mojom::StringAttribute attribute,
std::string* value) const {
return GetDelegate()->GetStringAttribute(attribute, value);
}
std::u16string AXPlatformNodeBase::GetString16Attribute(
ax::mojom::StringAttribute attribute) const {
return GetDelegate()->GetString16Attribute(attribute);
}
bool AXPlatformNodeBase::GetString16Attribute(
ax::mojom::StringAttribute attribute,
std::u16string* value) const {
return GetDelegate()->GetString16Attribute(attribute, value);
}
bool AXPlatformNodeBase::HasInheritedStringAttribute(
ax::mojom::StringAttribute attribute) const {
const AXPlatformNodeBase* current_node = this;
do {
if (current_node->HasStringAttribute(attribute)) {
return true;
}
current_node = FromNativeViewAccessible(current_node->GetParent());
} while (current_node);
return false;
}
const std::string& AXPlatformNodeBase::GetInheritedStringAttribute(
ax::mojom::StringAttribute attribute) const {
// TODO(nektar): Switch to using `AXNode::GetInheritedStringAttribute` after
// it has been modified to cross tree boundaries.
const AXPlatformNodeBase* current_node = this;
do {
if (current_node->HasStringAttribute(attribute)) {
return current_node->GetStringAttribute(attribute);
}
current_node = FromNativeViewAccessible(current_node->GetParent());
} while (current_node);
return base::EmptyString();
}
bool AXPlatformNodeBase::GetInheritedStringAttribute(
ax::mojom::StringAttribute attribute,
std::string* value) const {
// TODO(nektar): Switch to using `AXNode::GetInheritedStringAttribute` after
// it has been modified to cross tree boundaries.
const AXPlatformNodeBase* current_node = this;
do {
if (current_node->GetStringAttribute(attribute, value)) {
return true;
}
current_node = FromNativeViewAccessible(current_node->GetParent());
} while (current_node);
return false;
}
std::u16string AXPlatformNodeBase::GetInheritedString16Attribute(
ax::mojom::StringAttribute attribute) const {
// TODO(nektar): Switch to using `AXNode::GetInheritedString16Attribute` after
// it has been modified to cross tree boundaries.
return base::UTF8ToUTF16(GetInheritedStringAttribute(attribute));
}
bool AXPlatformNodeBase::GetInheritedString16Attribute(
ax::mojom::StringAttribute attribute,
std::u16string* value) const {
// TODO(nektar): Switch to using `AXNode::GetInheritedString16Attribute` after
// it has been modified to cross tree boundaries.
std::string value_utf8;
if (!GetInheritedStringAttribute(attribute, &value_utf8))
return false;
*value = base::UTF8ToUTF16(value_utf8);
return true;
}
const AXIntListAttributes& AXPlatformNodeBase::GetIntListAttributes() const {
return GetDelegate()->GetIntListAttributes();
}
bool AXPlatformNodeBase::HasIntListAttribute(
ax::mojom::IntListAttribute attribute) const {
return GetDelegate()->HasIntListAttribute(attribute);
}
const std::vector<int32_t>& AXPlatformNodeBase::GetIntListAttribute(
ax::mojom::IntListAttribute attribute) const {
return GetDelegate()->GetIntListAttribute(attribute);
}
bool AXPlatformNodeBase::GetIntListAttribute(
ax::mojom::IntListAttribute attribute,
std::vector<int32_t>* value) const {
return GetDelegate()->GetIntListAttribute(attribute, value);
}
bool AXPlatformNodeBase::HasStringListAttribute(
ax::mojom::StringListAttribute attribute) const {
return GetDelegate()->HasStringListAttribute(attribute);
}
const std::vector<std::string>& AXPlatformNodeBase::GetStringListAttribute(
ax::mojom::StringListAttribute attribute) const {
return GetDelegate()->GetStringListAttribute(attribute);
}
bool AXPlatformNodeBase::GetStringListAttribute(
ax::mojom::StringListAttribute attribute,
std::vector<std::string>* value) const {
return GetDelegate()->GetStringListAttribute(attribute, value);
}
const base::StringPairs& AXPlatformNodeBase::GetHtmlAttributes() const {
return GetDelegate()->GetHtmlAttributes();
}
AXTextAttributes AXPlatformNodeBase::GetTextAttributes() const {
return GetDelegate()->GetTextAttributes();
}
bool AXPlatformNodeBase::HasState(ax::mojom::State state) const {
return GetDelegate()->HasState(state);
}
bool AXPlatformNodeBase::HasAction(ax::mojom::Action action) const {
return GetDelegate()->HasAction(action);
}
bool AXPlatformNodeBase::HasTextStyle(ax::mojom::TextStyle text_style) const {
return GetDelegate()->HasTextStyle(text_style);
}
ax::mojom::NameFrom AXPlatformNodeBase::GetNameFrom() const {
return GetDelegate()->GetNameFrom();
}
bool AXPlatformNodeBase::HasNameFromOtherElement() const {
ax::mojom::NameFrom nameFrom = GetNameFrom();
return nameFrom == ax::mojom::NameFrom::kCaption ||
nameFrom == ax::mojom::NameFrom::kRelatedElement;
}
// static
AXPlatformNodeBase* AXPlatformNodeBase::FromNativeViewAccessible(
gfx::NativeViewAccessible accessible) {
return static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(accessible));
}
bool AXPlatformNodeBase::SetHypertextSelection(int start_offset,
int end_offset) {
return GetDelegate()->SetHypertextSelection(start_offset, end_offset);
}
bool AXPlatformNodeBase::IsPlatformDocument() const {
return GetDelegate()->IsPlatformDocument();
}
bool AXPlatformNodeBase::IsStructuredAnnotation() const {
// The node represents a structured annotation if it can trace back to a
// target node that is being annotated.
std::vector<AXPlatformNode*> reverse_relations =
GetDelegate()->GetSourceNodesForReverseRelations(
ax::mojom::IntListAttribute::kDetailsIds);
return !reverse_relations.empty();
}
bool AXPlatformNodeBase::IsTextField() const {
return GetData().IsTextField();
}
bool AXPlatformNodeBase::IsAtomicTextField() const {
return GetData().IsAtomicTextField();
}
bool AXPlatformNodeBase::IsNonAtomicTextField() const {
return GetData().IsNonAtomicTextField();
}
bool AXPlatformNodeBase::IsText() const {
return GetDelegate()->IsText();
}
std::u16string AXPlatformNodeBase::GetHypertext() const {
// Hypertext of platform leaves, which internally are composite objects, are
// represented with the text content of the internal composite object. These
// don't exist on non-web content.
if (IsChildOfLeaf())
return GetTextContentUTF16();
if (hypertext_.needs_update)
UpdateComputedHypertext();
return hypertext_.hypertext;
}
std::u16string AXPlatformNodeBase::GetTextContentUTF16() const {
return GetDelegate()->GetTextContentUTF16();
}
int AXPlatformNodeBase::GetTextContentLengthUTF16() const {
return GetDelegate()->GetTextContentLengthUTF16();
}
std::u16string
AXPlatformNodeBase::GetRoleDescriptionFromImageAnnotationStatusOrFromAttribute()
const {
if (GetRole() == ax::mojom::Role::kImage &&
(GetData().GetImageAnnotationStatus() ==
ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation ||
GetData().GetImageAnnotationStatus() ==
ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation)) {
return GetDelegate()->GetLocalizedRoleDescriptionForUnlabeledImage();
}
return GetString16Attribute(ax::mojom::StringAttribute::kRoleDescription);
}
std::u16string AXPlatformNodeBase::GetRoleDescription() const {
std::u16string role_description =
GetRoleDescriptionFromImageAnnotationStatusOrFromAttribute();
if (!role_description.empty()) {
return role_description;
}
return GetDelegate()->GetLocalizedStringForRoleDescription();
}
bool AXPlatformNodeBase::IsImageWithMap() const {
DCHECK_EQ(GetRole(), ax::mojom::Role::kImage)
<< "Only call IsImageWithMap() on an image";
return GetChildCount();
}
AXPlatformNodeBase* AXPlatformNodeBase::GetSelectionContainer() const {
return FromNativeViewAccessible(GetDelegate()->GetSelectionContainer());
}
AXPlatformNodeBase* AXPlatformNodeBase::GetTable() const {
return FromNativeViewAccessible(GetDelegate()->GetTableAncestor());
}
AXPlatformNodeBase* AXPlatformNodeBase::GetTableCaption() const {
return static_cast<AXPlatformNodeBase*>(GetDelegate()->GetTableCaption());
}
AXPlatformNodeBase* AXPlatformNodeBase::GetTableCell(int index) const {
std::optional<int32_t> cell_id = GetDelegate()->CellIndexToId(index);
if (!cell_id)
return nullptr;
return static_cast<AXPlatformNodeBase*>(
GetDelegate()->GetFromNodeID(*cell_id));
}
AXPlatformNodeBase* AXPlatformNodeBase::GetTableCell(int row,
int column) const {
std::optional<int32_t> cell_id = GetDelegate()->GetCellId(row, column);
if (!cell_id)
return nullptr;
return static_cast<AXPlatformNodeBase*>(
GetDelegate()->GetFromNodeID(*cell_id));
}
AXPlatformNodeBase* AXPlatformNodeBase::GetAriaTableCell(int aria_row,
int aria_column) const {
std::optional<int32_t> cell_id =
GetDelegate()->GetCellIdAriaCoords(aria_row, aria_column);
if (!cell_id) {
return nullptr;
}
return static_cast<AXPlatformNodeBase*>(
GetDelegate()->GetFromNodeID(*cell_id));
}
std::optional<int> AXPlatformNodeBase::GetTableCellIndex() const {
return GetDelegate()->GetTableCellIndex();
}
std::optional<int> AXPlatformNodeBase::GetTableColumn() const {
return GetDelegate()->GetTableCellColIndex();
}
std::optional<int> AXPlatformNodeBase::GetTableColumnCount() const {
return GetDelegate()->GetTableColCount();
}
std::optional<int> AXPlatformNodeBase::GetTableAriaColumnCount() const {
return GetDelegate()->GetTableAriaColCount();
}
std::optional<int> AXPlatformNodeBase::GetTableColumnSpan() const {
return GetDelegate()->GetTableCellColSpan();
}
std::optional<int> AXPlatformNodeBase::GetTableRow() const {
AXPlatformNodeDelegate* const delegate = GetDelegate();
if (delegate->IsTableRow()) {
return delegate->GetTableRowRowIndex();
}
if (delegate->IsTableCellOrHeader()) {
return delegate->GetTableCellRowIndex();
}
return std::nullopt;
}
std::optional<int> AXPlatformNodeBase::GetTableRowCount() const {
return GetDelegate()->GetTableRowCount();
}
std::optional<int> AXPlatformNodeBase::GetTableAriaRowCount() const {
return GetDelegate()->GetTableAriaRowCount();
}
std::optional<int> AXPlatformNodeBase::GetTableRowSpan() const {
return GetDelegate()->GetTableCellRowSpan();
}
std::optional<float> AXPlatformNodeBase::GetFontSizeInPoints() const {
float font_size;
// Attribute has no default value.
if (GetFloatAttribute(ax::mojom::FloatAttribute::kFontSize, &font_size)) {
// The IA2 Spec requires the value to be in pt, not in pixels.
// There are 72 points per inch.
// We assume that there are 96 pixels per inch on a standard display.
// TODO(nektar): Figure out the current value of pixels per inch.
float points = font_size * 72.0 / 96.0;
// Round to the nearest 0.5 points.
points = std::round(points * 2.0) / 2.0;
return points;
}
return std::nullopt;
}
bool AXPlatformNodeBase::HasVisibleCaretOrSelection() const {
return GetDelegate()->HasVisibleCaretOrSelection();
}
bool AXPlatformNodeBase::IsLeaf() const {
return GetDelegate()->IsLeaf();
}
bool AXPlatformNodeBase::IsChildOfLeaf() const {
return GetDelegate()->IsChildOfLeaf();
}
bool AXPlatformNodeBase::IsInvisibleOrIgnored() const {
if (!GetData().IsInvisibleOrIgnored())
return false;
// Never marked a focused node as invisible or ignored, otherwise screen
// reader users will not hear an announcement for it when it receives focus.
if (IsFocused())
return false;
return !HasVisibleCaretOrSelection();
}
bool AXPlatformNodeBase::IsFocused() const {
return FromNativeViewAccessible(GetDelegate()->GetFocus()) == this;
}
bool AXPlatformNodeBase::IsFocusable() const {
return GetDelegate()->IsFocusable();
}
bool AXPlatformNodeBase::IsScrollable() const {
return (HasIntAttribute(ax::mojom::IntAttribute::kScrollXMin) &&
HasIntAttribute(ax::mojom::IntAttribute::kScrollXMax) &&
HasIntAttribute(ax::mojom::IntAttribute::kScrollX)) ||
(HasIntAttribute(ax::mojom::IntAttribute::kScrollYMin) &&
HasIntAttribute(ax::mojom::IntAttribute::kScrollYMax) &&
HasIntAttribute(ax::mojom::IntAttribute::kScrollY));
}
bool AXPlatformNodeBase::IsHorizontallyScrollable() const {
DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollXMin), 0)
<< "Pixel sizes should be non-negative.";
DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollXMax), 0)
<< "Pixel sizes should be non-negative.";
return IsScrollable() &&
GetIntAttribute(ax::mojom::IntAttribute::kScrollXMin) <
GetIntAttribute(ax::mojom::IntAttribute::kScrollXMax);
}
bool AXPlatformNodeBase::IsVerticallyScrollable() const {
DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollYMin), 0)
<< "Pixel sizes should be non-negative.";
DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax), 0)
<< "Pixel sizes should be non-negative.";
return IsScrollable() &&
GetIntAttribute(ax::mojom::IntAttribute::kScrollYMin) <
GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax);
}
std::u16string AXPlatformNodeBase::GetValueForControl() const {
return GetDelegate()->GetValueForControl();
}
void AXPlatformNodeBase::ComputeAttributes(PlatformAttributeList* attributes) {
// Expose some HTML and ARIA attributes in the IAccessible2 attributes string
// "display", "tag", and "xml-roles" have somewhat unusual names for
// historical reasons. Aside from that, virtually every ARIA attribute
// is exposed in a really straightforward way, i.e. "aria-foo" is exposed
// as "foo".
AddAttributeToList(ax::mojom::StringAttribute::kDisplay, "display",
attributes);
AddAttributeToList(ax::mojom::StringAttribute::kHtmlTag, "tag", attributes);
AddAttributeToList(ax::mojom::StringAttribute::kRole, "xml-roles",
attributes);
AddAttributeToList(ax::mojom::StringAttribute::kPlaceholder, "placeholder",
attributes);
AddAttributeToList(ax::mojom::StringAttribute::kAutoComplete, "autocomplete",
attributes);
if (!HasStringAttribute(ax::mojom::StringAttribute::kAutoComplete) &&
HasState(ax::mojom::State::kAutofillAvailable)) {
AddAttributeToList("autocomplete", "list", attributes);
}
if (HasState(ax::mojom::State::kHasActions)) {
AddAttributeToList("has-actions", "true", attributes);
}
std::u16string role_description =
GetRoleDescriptionFromImageAnnotationStatusOrFromAttribute();
if (!role_description.empty() ||
HasStringAttribute(ax::mojom::StringAttribute::kRoleDescription)) {
AddAttributeToList("roledescription", base::UTF16ToUTF8(role_description),
attributes);
}
// Expose description-from and description.
int desc_from;
if (GetIntAttribute(ax::mojom::IntAttribute::kDescriptionFrom, &desc_from)) {
std::string from;
switch (static_cast<ax::mojom::DescriptionFrom>(desc_from)) {
case ax::mojom::DescriptionFrom::kAriaDescription:
// Descriptions are exposed via each platform's usual description field.
// Also, only aria-description is exposed via the "description" object
// attribute, in order to match Firefox.
AddAttributeToList(ax::mojom::StringAttribute::kDescription,
"description", attributes);
from = "aria-description";
break;
case ax::mojom::DescriptionFrom::kButtonLabel:
from = "button-label";
break;
case ax::mojom::DescriptionFrom::kProhibitedNameRepair:
from = "prohibited-name-repair";
break;
case ax::mojom::DescriptionFrom::kRelatedElement:
// aria-describedby=tooltip is mapped to "tooltip".
from = IsDescribedByTooltip() ? "tooltip" : "aria-describedby";
break;
case ax::mojom::DescriptionFrom::kRubyAnnotation:
from = "ruby-annotation";
break;
case ax::mojom::DescriptionFrom::kSummary:
from = "summary";
break;
case ax::mojom::DescriptionFrom::kSvgDescElement:
from = "svg-desc-element";
break;
case ax::mojom::DescriptionFrom::kTableCaption:
from = "table-caption";
break;
case ax::mojom::DescriptionFrom::kTitle:
case ax::mojom::DescriptionFrom::kPopoverTarget:
case ax::mojom::DescriptionFrom::kInterestFor:
// The following types of markup are mapped to "tooltip":
// * The title attribute.
// * A popover=something related via the `popovertarget` attribute.
// * A tooltip related via aria-describedby (see kRelatedElement above).
// * An interestfor pointing to plain content.
from = "tooltip";
break;
case ax::mojom::DescriptionFrom::kNone:
case ax::mojom::DescriptionFrom::kAttributeExplicitlyEmpty:
break;
}
if (!from.empty()) {
AddAttributeToList("description-from", from, attributes);
}
}
AddAttributeToList(ax::mojom::StringAttribute::kAriaBrailleLabel,
"braillelabel", attributes);
AddAttributeToList(ax::mojom::StringAttribute::kAriaBrailleRoleDescription,
"brailleroledescription", attributes);
AddAttributeToList(ax::mojom::StringAttribute::kKeyShortcuts, "keyshortcuts",
attributes);
AddAttributeToList(ax::mojom::IntAttribute::kHierarchicalLevel, "level",
attributes);
AddAttributeToList(ax::mojom::IntAttribute::kSetSize, "setsize", attributes);
AddAttributeToList(ax::mojom::IntAttribute::kPosInSet, "posinset",
attributes);
if (IsPlatformCheckable())
AddAttributeToList("checkable", "true", attributes);
if (IsInvisibleOrIgnored()) // Note: NVDA prefers this over INVISIBLE state.
AddAttributeToList("hidden", "true", attributes);
// Expose live region attributes.
AddAttributeToList(ax::mojom::StringAttribute::kLiveStatus, "live",
attributes);
AddAttributeToList(ax::mojom::StringAttribute::kLiveRelevant, "relevant",
attributes);
AddAttributeToList(ax::mojom::BoolAttribute::kLiveAtomic, "atomic",
attributes);
// Busy is usually associated with live regions but can occur anywhere:
AddAttributeToList(ax::mojom::BoolAttribute::kBusy, "busy", attributes);
// Expose container live region attributes.
AddAttributeToList(ax::mojom::StringAttribute::kContainerLiveStatus,
"container-live", attributes);
AddAttributeToList(ax::mojom::StringAttribute::kContainerLiveRelevant,
"container-relevant", attributes);
AddAttributeToList(ax::mojom::BoolAttribute::kContainerLiveAtomic,
"container-atomic", attributes);
AddAttributeToList(ax::mojom::BoolAttribute::kContainerLiveBusy,
"container-busy", attributes);
// Expose name-from.
ax::mojom::NameFrom name_from = GetNameFrom();
std::string from;
bool is_explicit_name = true;
switch (static_cast<ax::mojom::NameFrom>(name_from)) {
case ax::mojom::NameFrom::kAttribute:
from = "attribute";
DCHECK(!GetName().empty());
break;
case ax::mojom::NameFrom::kCaption:
from = "caption";
DCHECK(!GetName().empty());
break;
case ax::mojom::NameFrom::kContents:
is_explicit_name = false;
from = "contents";
DCHECK(!GetName().empty());
break;
case ax::mojom::NameFrom::kCssAltText:
from = "CSS alt text";
DCHECK(!GetName().empty());
break;
case ax::mojom::NameFrom::kPlaceholder:
from = "placeholder";
DCHECK(!GetName().empty());
break;
case ax::mojom::NameFrom::kProhibited:
case ax::mojom::NameFrom::kProhibitedAndRedundant:
is_explicit_name = false;
from = "prohibited";
DCHECK(GetName().empty());
break;
case ax::mojom::NameFrom::kRelatedElement:
from = "related-element";
DCHECK(!GetName().empty());
break;
case ax::mojom::NameFrom::kPopoverTarget:
case ax::mojom::NameFrom::kInterestFor:
case ax::mojom::NameFrom::kTitle:
from = "tooltip";
DCHECK(!GetName().empty());
break;
case ax::mojom::NameFrom::kValue:
from = "value";
DCHECK(!GetName().empty());
break;
case ax::mojom::NameFrom::kAttributeExplicitlyEmpty:
break;
case ax::mojom::NameFrom::kNone:
is_explicit_name = false;
break; // Not exposed.
}
if (!from.empty()) {
AddAttributeToList("name-from", from, attributes);
}
// Expose the non-standard explicit-name IA2 attribute.
if (is_explicit_name) {
AddAttributeToList("explicit-name", "true", attributes);
}
// Expose the aria-haspopup attribute.
int32_t has_popup;
if (GetIntAttribute(ax::mojom::IntAttribute::kHasPopup, &has_popup)) {
switch (static_cast<ax::mojom::HasPopup>(has_popup)) {
case ax::mojom::HasPopup::kFalse:
break;
case ax::mojom::HasPopup::kTrue:
AddAttributeToList("haspopup", "true", attributes);
break;
case ax::mojom::HasPopup::kMenu:
AddAttributeToList("haspopup", "menu", attributes);
break;
case ax::mojom::HasPopup::kListbox:
AddAttributeToList("haspopup", "listbox", attributes);
break;
case ax::mojom::HasPopup::kTree:
AddAttributeToList("haspopup", "tree", attributes);
break;
case ax::mojom::HasPopup::kGrid:
AddAttributeToList("haspopup", "grid", attributes);
break;
case ax::mojom::HasPopup::kDialog:
AddAttributeToList("haspopup", "dialog", attributes);
break;
}
} else if (HasState(ax::mojom::State::kAutofillAvailable)) {
AddAttributeToList("haspopup", "menu", attributes);
}
if (HasState(ax::mojom::State::kHasInterestFor)) {
AddAttributeToList("has-interest-for", "true", attributes);
}
// Expose the aria-ispopup attribute.
int32_t is_popup;
if (GetIntAttribute(ax::mojom::IntAttribute::kIsPopup, &is_popup)) {
switch (static_cast<ax::mojom::IsPopup>(is_popup)) {
case ax::mojom::IsPopup::kNone:
break;
case ax::mojom::IsPopup::kManual:
AddAttributeToList("ispopup", "manual", attributes);
break;
case ax::mojom::IsPopup::kAuto:
AddAttributeToList("ispopup", "auto", attributes);
break;
case ax::mojom::IsPopup::kHint:
AddAttributeToList("ispopup", "hint", attributes);
break;
}
}
// Expose the aria-current attribute.
int32_t aria_current_state;
if (GetIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState,
&aria_current_state)) {
switch (static_cast<ax::mojom::AriaCurrentState>(aria_current_state)) {
case ax::mojom::AriaCurrentState::kNone:
break;
case ax::mojom::AriaCurrentState::kFalse:
AddAttributeToList("current", "false", attributes);
break;
case ax::mojom::AriaCurrentState::kTrue:
AddAttributeToList("current", "true", attributes);
break;
case ax::mojom::AriaCurrentState::kPage:
AddAttributeToList("current", "page", attributes);
break;
case ax::mojom::AriaCurrentState::kStep:
AddAttributeToList("current", "step", attributes);
break;
case ax::mojom::AriaCurrentState::kLocation:
AddAttributeToList("current", "location", attributes);
break;
case ax::mojom::AriaCurrentState::kDate:
AddAttributeToList("current", "date", attributes);
break;
case ax::mojom::AriaCurrentState::kTime:
AddAttributeToList("current", "time", attributes);
break;
}
}
AXPlatformNodeDelegate* const delegate = GetDelegate();
// Expose table cell index.
if (IsCellOrTableHeader(GetRole())) {
std::optional<int> index = delegate->GetTableCellIndex();
if (index) {
std::string str_index(base::NumberToString(*index));
AddAttributeToList("table-cell-index", str_index, attributes);
}
}
if (GetRole() == ax::mojom::Role::kLayoutTable)
AddAttributeToList("layout-guess", "true", attributes);
// Expose aria-colcount and aria-rowcount in a table, grid or treegrid if they
// are different from its physical dimensions.
if (IsTableLike(GetRole()) &&
(delegate->GetTableAriaRowCount() != delegate->GetTableRowCount() ||
delegate->GetTableAriaColCount() != delegate->GetTableColCount())) {
AddAttributeToList(ax::mojom::IntAttribute::kAriaColumnCount, "colcount",
attributes);
AddAttributeToList(ax::mojom::IntAttribute::kAriaRowCount, "rowcount",
attributes);
}
if (IsCellOrTableHeader(GetRole()) || IsTableRow(GetRole())) {
// Expose aria-colindex and aria-rowindex in a cell or row only if they are
// different from the table's physical coordinates.
// Note: aria-col/rowindex is 1 based where as table's physical coordinates
// are 0 based, so we subtract aria-col/rowindex by 1 to compare with
// table's physical coordinates.
std::optional<int> aria_rowindex = delegate->GetTableCellAriaRowIndex();
std::optional<int> physical_rowindex = delegate->GetTableCellRowIndex();
std::optional<int> aria_colindex = delegate->GetTableCellAriaColIndex();
std::optional<int> physical_colindex = delegate->GetTableCellColIndex();
if (aria_rowindex && physical_rowindex &&
aria_rowindex.value() - 1 != physical_rowindex.value()) {
std::string str_value = base::NumberToString(*aria_rowindex);
AddAttributeToList("rowindex", str_value, attributes);
}
if (!IsTableRow(GetRole()) && aria_colindex && physical_colindex &&
aria_colindex.value() - 1 != physical_colindex.value()) {
AddAttributeToList(ax::mojom::IntAttribute::kAriaCellColumnIndex,
"colindex", attributes);
}
}
// Expose row or column header sort direction.
int32_t sort_direction;
if (IsTableHeader(GetRole()) &&
GetIntAttribute(ax::mojom::IntAttribute::kSortDirection,
&sort_direction)) {
switch (static_cast<ax::mojom::SortDirection>(sort_direction)) {
case ax::mojom::SortDirection::kNone:
break;
case ax::mojom::SortDirection::kUnsorted:
AddAttributeToList("sort", "none", attributes);
break;
case ax::mojom::SortDirection::kAscending:
AddAttributeToList("sort", "ascending", attributes);
break;
case ax::mojom::SortDirection::kDescending:
AddAttributeToList("sort", "descending", attributes);
break;
case ax::mojom::SortDirection::kOther:
AddAttributeToList("sort", "other", attributes);
break;
}
}
if (IsCellOrTableHeader(GetRole())) {
// These are the older, backwards compatible names that work with JAWS/NVDA:
AddAttributeToList(ax::mojom::StringAttribute::kAriaCellColumnIndexText,
"coltext", attributes);
AddAttributeToList(ax::mojom::StringAttribute::kAriaCellRowIndexText,
"rowtext", attributes);
// These newer names are consistent with the ARIA attribute names:
AddAttributeToList(ax::mojom::StringAttribute::kAriaCellColumnIndexText,
"colindextext", attributes);
AddAttributeToList(ax::mojom::StringAttribute::kAriaCellRowIndexText,
"rowindextext", attributes);
AddAttributeToList(ax::mojom::IntAttribute::kAriaCellColumnSpan, "colspan",
attributes);
AddAttributeToList(ax::mojom::IntAttribute::kAriaCellRowSpan, "rowspan",
attributes);
}
// Expose the value of a progress bar, slider, scroll bar or <select> element.
if (GetData().IsRangeValueSupported() ||
GetRole() == ax::mojom::Role::kComboBoxMenuButton) {
std::string value = base::UTF16ToUTF8(GetValueForControl());
if (!value.empty())
AddAttributeToList("valuetext", value, attributes);
}
// Expose dropeffect attribute.
// aria-dropeffect is deprecated in WAI-ARIA 1.1.
if (delegate->HasIntAttribute(
ax::mojom::IntAttribute::kDropeffectDeprecated)) {
NOTREACHED();
}
// Expose class attribute.
std::string class_attr;
if (delegate->GetStringAttribute(ax::mojom::StringAttribute::kClassName,
&class_attr)) {
AddAttributeToList("class", class_attr, attributes);
}
// Expose machine-readable datetime attribute on <time>, <ins> and <del>.
AddAttributeToList(ax::mojom::StringAttribute::kDateTime, "datetime",
attributes);
std::string id;
if (delegate->GetStringAttribute(ax::mojom::StringAttribute::kHtmlId, &id)) {
AddAttributeToList("id", id, attributes);
}
std::string input_name;
if (delegate->GetStringAttribute(ax::mojom::StringAttribute::kHtmlInputName,
&input_name)) {
AddAttributeToList("html-input-name", input_name, attributes);
}
std::string src;
if (IsImage(GetRole()) &&
GetStringAttribute(ax::mojom::StringAttribute::kUrl, &src)) {
AddAttributeToList("src", src, attributes);
}
if (delegate->HasIntAttribute(ax::mojom::IntAttribute::kTextAlign)) {
auto text_align = static_cast<ax::mojom::TextAlign>(
delegate->GetIntAttribute(ax::mojom::IntAttribute::kTextAlign));
switch (text_align) {
case ax::mojom::TextAlign::kNone:
break;
case ax::mojom::TextAlign::kLeft:
AddAttributeToList("text-align", "left", attributes);
break;
case ax::mojom::TextAlign::kRight:
AddAttributeToList("text-align", "right", attributes);
break;
case ax::mojom::TextAlign::kCenter:
AddAttributeToList("text-align", "center", attributes);
break;
case ax::mojom::TextAlign::kJustify:
AddAttributeToList("text-align", "justify", attributes);
break;
}
}
float text_indent;
if (GetFloatAttribute(ax::mojom::FloatAttribute::kTextIndent, &text_indent) !=
0.0f) {
// Round value to two decimal places.
std::stringstream value;
value << std::fixed << std::setprecision(2) << text_indent << "mm";
AddAttributeToList("text-indent", value.str(), attributes);
}
// Text fields need to report the attribute "text-model:a1" to instruct
// screen readers to use IAccessible2 APIs to handle text editing in this
// object (as opposed to treating it like a native Windows text box).
// The text-model:a1 attribute is documented here:
// http://www.linuxfoundation.org/collaborate/workgroups/accessibility/ia2/ia2_implementation_guide
if (IsTextField())
AddAttributeToList("text-model", "a1", attributes);
// Expose input-text type attribute.
if (IsAtomicTextField() || IsDateOrTimeInput(GetRole())) {
AddAttributeToList(ax::mojom::StringAttribute::kInputType,
"text-input-type", attributes);
}
// Expose details-from.
int details_from;
if (GetIntAttribute(ax::mojom::IntAttribute::kDetailsFrom, &details_from)) {
switch (static_cast<ax::mojom::DetailsFrom>(details_from)) {
case ax::mojom::DetailsFrom::kAriaDetails:
AddAttributeToList("details-from", "aria-details", attributes);
break;
case ax::mojom::DetailsFrom::kCssAnchor:
AddAttributeToList("details-from", "css-anchor", attributes);
break;
case ax::mojom::DetailsFrom::kPopoverTarget:
AddAttributeToList("details-from", "popover-target", attributes);
break;
case ax::mojom::DetailsFrom::kInterestFor:
AddAttributeToList("details-from", "interest-for", attributes);
break;
case ax::mojom::DetailsFrom::kCommandfor:
AddAttributeToList("details-from", "command-for", attributes);
break;
case ax::mojom::DetailsFrom::kCssScrollMarkerPseudoElement:
AddAttributeToList("details-from", "css-scroll-marker-pseudo-element",
attributes);
break;
}
}
std::string details_roles = ComputeDetailsRoles();
if (!details_roles.empty())
AddAttributeToList("details-roles", details_roles, attributes);
if (IsLink(GetRole())) {
AddAttributeToList(ax::mojom::StringAttribute::kLinkTarget, "link-target",
attributes);
}
// MathML content.
AddAttributeToList(ax::mojom::StringAttribute::kMathContent, "math",
attributes);
// The maxlength of an input.
// TODO(https://github.com/w3c/aria/issues/1119): consider aria-maxlength.
if (int max_length = GetIntAttribute(ax::mojom::IntAttribute::kMaxLength)) {
AddAttributeToList("maxlength", base::NumberToString(max_length),
attributes);
}
}
void AXPlatformNodeBase::AddAttributeToList(
const ax::mojom::StringAttribute attribute,
const char* name,
PlatformAttributeList* attributes) {
DCHECK(attributes);
std::string value;
if (GetStringAttribute(attribute, &value)) {
AddAttributeToList(name, value, attributes);
}
}
void AXPlatformNodeBase::AddAttributeToList(
const ax::mojom::BoolAttribute attribute,
const char* name,
PlatformAttributeList* attributes) {
DCHECK(attributes);
bool value;
if (GetBoolAttribute(attribute, &value)) {
AddAttributeToList(name, base::ToString(value), attributes);
}
}
void AXPlatformNodeBase::AddAttributeToList(
const ax::mojom::IntAttribute attribute,
const char* name,
PlatformAttributeList* attributes) {
DCHECK(attributes);
auto maybe_value = ComputeAttribute(GetDelegate(), attribute);
if (maybe_value.has_value()) {
std::string str_value = base::NumberToString(maybe_value.value());
AddAttributeToList(name, str_value, attributes);
}
}
void AXPlatformNodeBase::AddAttributeToList(const char* name,
const std::string& value,
PlatformAttributeList* attributes) {
AddAttributeToList(name, value.c_str(), attributes);
}
AXLegacyHypertext::AXLegacyHypertext() = default;
AXLegacyHypertext::~AXLegacyHypertext() = default;
AXLegacyHypertext::AXLegacyHypertext(const AXLegacyHypertext& other) = default;
AXLegacyHypertext& AXLegacyHypertext::operator=(
const AXLegacyHypertext& other) = default;
AXLegacyHypertext::AXLegacyHypertext(AXLegacyHypertext&& other) noexcept
: needs_update(std::exchange(other.needs_update, true)),
hyperlink_offset_to_index(std::move(other.hyperlink_offset_to_index)),
hyperlinks(std::move(other.hyperlinks)),
hypertext(std::move(other.hypertext)) {}
AXLegacyHypertext& AXLegacyHypertext::operator=(AXLegacyHypertext&& other) {
needs_update = std::exchange(other.needs_update, true);
hyperlink_offset_to_index = std::move(other.hyperlink_offset_to_index);
hyperlinks = std::move(other.hyperlinks);
hypertext = std::move(other.hypertext);
return *this;
}
void AXLegacyHypertext::Clear() {
needs_update = true;
hyperlink_offset_to_index.clear();
hyperlinks.clear();
hypertext.clear();
}
// TODO(nektar): To be able to use AXNode in Views, move this logic to AXNode.
void AXPlatformNodeBase::UpdateComputedHypertext() const {
hypertext_.Clear();
if (GetData().IsIgnored() || IsLeaf()) {
hypertext_.hypertext = GetTextContentUTF16();
hypertext_.needs_update = false;
return;
}
// Construct the hypertext for this node, which contains the concatenation
// of all of the static text and whitespace from this node's children, and an
// embedded object character for all the other children. Build up a map from
// the character index of each embedded object character to the id of the
// child object it points to.
AXLegacyHypertext::OffsetToIndex::container_type indices;
std::u16string hypertext;
for (AXPlatformNodeChildIterator child_iter = AXPlatformNodeChildrenBegin(),
child_end = AXPlatformNodeChildrenEnd();
child_iter != child_end; ++child_iter) {
// Similar to Firefox, we don't expose text nodes in IAccessible2 and ATK
// hypertext with the embedded object character. We copy all of their text
// instead.
if (child_iter->IsText()) {
hypertext_.hypertext += child_iter->GetTextContentUTF16();
} else {
int32_t char_offset = static_cast<int32_t>(hypertext_.hypertext.size());
int32_t child_unique_id = child_iter->GetUniqueId();
int32_t index = static_cast<int32_t>(hypertext_.hyperlinks.size());
indices.emplace_back(char_offset, index);
hypertext_.hyperlinks.push_back(child_unique_id);
hypertext_.hypertext += kEmbeddedCharacter;
}
}
hypertext_.hyperlink_offset_to_index.replace(std::move(indices));
hypertext_.needs_update = false;
}
void AXPlatformNodeBase::AddAttributeToList(const char* name,
const char* value,
PlatformAttributeList* attributes) {
}
std::optional<int> AXPlatformNodeBase::GetPosInSet() const {
return GetDelegate()->GetPosInSet();
}
std::optional<int> AXPlatformNodeBase::GetSetSize() const {
return GetDelegate()->GetSetSize();
}
bool AXPlatformNodeBase::ScrollToNode(ScrollType scroll_type) {
// ax::mojom::Action::kScrollToMakeVisible wants a target rect in *local*
// coords.
gfx::Rect r = gfx::ToEnclosingRect(GetData().relative_bounds.bounds);
r -= r.OffsetFromOrigin();
switch (scroll_type) {
case ScrollType::TopLeft:
r = gfx::Rect(r.x(), r.y(), 0, 0);
break;
case ScrollType::BottomRight:
r = gfx::Rect(r.right(), r.bottom(), 0, 0);
break;
case ScrollType::TopEdge:
r = gfx::Rect(r.x(), r.y(), r.width(), 0);
break;
case ScrollType::BottomEdge:
r = gfx::Rect(r.x(), r.bottom(), r.width(), 0);
break;
case ScrollType::LeftEdge:
r = gfx::Rect(r.x(), r.y(), 0, r.height());
break;
case ScrollType::RightEdge:
r = gfx::Rect(r.right(), r.y(), 0, r.height());
break;
case ScrollType::Anywhere:
break;
}
AXActionData action_data;
action_data.target_node_id = GetData().id;
action_data.action = ax::mojom::Action::kScrollToMakeVisible;
action_data.horizontal_scroll_alignment =
ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
action_data.vertical_scroll_alignment =
ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
action_data.scroll_behavior =
ax::mojom::ScrollBehavior::kDoNotScrollIfVisible;
action_data.target_rect = r;
GetDelegate()->AccessibilityPerformAction(action_data);
return true;
}
// static
void AXPlatformNodeBase::SanitizeStringAttribute(const std::string& input,
std::string* output) {
DCHECK(output);
// According to the IA2 spec and AT-SPI2, these characters need to be escaped
// with a backslash: backslash, colon, comma, equals and semicolon. Note
// that backslash must be replaced first.
base::ReplaceChars(input, "\\", "\\\\", output);
base::ReplaceChars(*output, ":", "\\:", output);
base::ReplaceChars(*output, ",", "\\,", output);
base::ReplaceChars(*output, "=", "\\=", output);
base::ReplaceChars(*output, ";", "\\;", output);
base::ReplaceChars(*output, "\r", " ", output);
base::ReplaceChars(*output, "\n", " ", output);
}
int32_t AXPlatformNodeBase::GetHyperlinkIndexFromChild(
AXPlatformNodeBase* child) {
if (hypertext_.hyperlinks.empty())
return -1;
auto iterator =
std::ranges::find(hypertext_.hyperlinks, child->GetUniqueId());
if (iterator == hypertext_.hyperlinks.end())
return -1;
return static_cast<int32_t>(iterator - hypertext_.hyperlinks.begin());
}
int32_t AXPlatformNodeBase::GetHypertextOffsetFromHyperlinkIndex(
int32_t hyperlink_index) {
for (auto& offset_index : hypertext_.hyperlink_offset_to_index) {
if (offset_index.second == hyperlink_index)
return offset_index.first;
}
return -1;
}
int32_t AXPlatformNodeBase::GetHypertextOffsetFromChild(
AXPlatformNodeBase* child) {
// TODO(dougt) DCHECK(child.owner()->PlatformGetParent() == owner());
if (IsLeaf())
return -1;
// Handle the case when we are dealing with a text-only child.
// Text-only children should not be present at tree roots and so no
// cross-tree traversal is necessary.
if (child->IsText()) {
int32_t hypertext_offset = 0;
for (auto child_iter = AXPlatformNodeChildrenBegin(),
child_end = AXPlatformNodeChildrenEnd();
child_iter != child_end && child_iter.get() != child; ++child_iter) {
if (child_iter->IsText()) {
hypertext_offset +=
static_cast<int32_t>(child_iter->GetHypertext().size());
} else {
++hypertext_offset;
}
}
return hypertext_offset;
}
int32_t hyperlink_index = GetHyperlinkIndexFromChild(child);
if (hyperlink_index < 0)
return -1;
return GetHypertextOffsetFromHyperlinkIndex(hyperlink_index);
}
int AXPlatformNodeBase::HypertextOffsetFromChildIndex(int child_index) const {
DCHECK_GE(child_index, 0);
DCHECK_LE(child_index, static_cast<int>(GetChildCount()));
// Use both a child index and an iterator to avoid an O(n^2) complexity which
// would be the case if we were to call GetChildAtIndex on each child.
int hypertext_offset = 0;
int endpoint_child_index = 0;
for (AXPlatformNodeChildIterator child_iter = AXPlatformNodeChildrenBegin(),
child_end = AXPlatformNodeChildrenEnd();
child_iter != child_end; ++child_iter) {
if (endpoint_child_index >= child_index) {
break;
}
int child_text_len = 1;
if (child_iter->IsText())
child_text_len =
base::checked_cast<int>(child_iter->GetHypertext().size());
endpoint_child_index++;
hypertext_offset += child_text_len;
}
return hypertext_offset;
}
int32_t AXPlatformNodeBase::GetHypertextOffsetFromDescendant(
AXPlatformNodeBase* descendant) {
auto* parent_object = static_cast<AXPlatformNodeBase*>(
FromNativeViewAccessible(descendant->GetDelegate()->GetParent()));
while (parent_object && parent_object != this) {
descendant = parent_object;
parent_object = static_cast<AXPlatformNodeBase*>(
FromNativeViewAccessible(descendant->GetParent()));
}
if (!parent_object)
return -1;
return parent_object->GetHypertextOffsetFromChild(descendant);
}
int AXPlatformNodeBase::GetHypertextOffsetFromEndpoint(
AXPlatformNodeBase* endpoint_object,
int endpoint_offset) {
DCHECK_GE(endpoint_offset, 0);
// There are three cases:
// 1. The selection endpoint is this object itself: endpoint_offset should be
// returned, possibly adjusted from a child offset to a hypertext offset.
// 2. The selection endpoint is an ancestor of this object. If endpoint_offset
// points out after this object, then this object text length is returned,
// otherwise 0.
// 3. The selection endpoint is a descendant of this object. The offset of the
// character in this object's hypertext corresponding to the subtree in which
// the endpoint is located should be returned.
// 4. The selection endpoint is in a completely different part of the tree.
// Either 0 or hypertext length should be returned depending on the direction
// that one needs to travel to find the endpoint.
//
// TODO(nektar): Replace all this logic with the use of AXNodePosition.
// Case 1. Is the endpoint object equal to this object
if (endpoint_object == this) {
if (endpoint_object->IsLeaf())
return endpoint_offset;
return HypertextOffsetFromChildIndex(endpoint_offset);
}
// Case 2. Is the endpoint an ancestor of this object.
if (IsDescendantOf(endpoint_object)) {
DCHECK_LE(endpoint_offset,
static_cast<int>(endpoint_object->GetChildCount()));
AXPlatformNodeBase* closest_ancestor = this;
while (closest_ancestor) {
AXPlatformNodeBase* parent = static_cast<AXPlatformNodeBase*>(
FromNativeViewAccessible(closest_ancestor->GetParent()));
if (parent == endpoint_object)
break;
closest_ancestor = parent;
}
// If the endpoint is after this node, then return the node's
// hypertext length, otherwise 0 as the endpoint points before the node.
std::optional<size_t> index_in_parent =
closest_ancestor->GetIndexInParent();
DCHECK(index_in_parent)
<< "No index in parent for ancestor: " << *closest_ancestor;
if (index_in_parent &&
endpoint_offset > static_cast<int>(*index_in_parent)) {
return static_cast<int>(GetHypertext().size());
}
return 0;
}
AXPlatformNodeBase* common_parent = this;
std::optional<size_t> index_in_common_parent = GetIndexInParent();
while (common_parent && !endpoint_object->IsDescendantOf(common_parent)) {
index_in_common_parent = common_parent->GetIndexInParent();
common_parent = static_cast<AXPlatformNodeBase*>(
FromNativeViewAccessible(common_parent->GetParent()));
}
if (!common_parent)
return -1;
DCHECK(!(common_parent->IsText()));
// Case 2. Is the selection endpoint inside a descendant of this object?
//
// We already checked in case 1 if our endpoint object is equal to this
// object. We can safely assume that it is a descendant or in a completely
// different part of the tree.
if (common_parent == this) {
int32_t hypertext_offset =
GetHypertextOffsetFromDescendant(endpoint_object);
auto* parent = static_cast<AXPlatformNodeBase*>(
FromNativeViewAccessible(endpoint_object->GetParent()));
if (parent == this && endpoint_object->IsText()) {
// Due to a historical design decision, the hypertext of the immediate
// parents of text objects includes all their text. We therefore need to
// adjust the hypertext offset in the parent by adding any text offset.
hypertext_offset += endpoint_offset;
}
return hypertext_offset;
}
// Case 3. Selection endpoint is in a completely different part of the tree:
// - Return 0 if it's in an earlier part of the tree.
// - Return GetHypertext.size() if it's in a later part of the tree.
// We can safely assume that the endpoint is in another part of the tree or
// at common parent, and that this object is a descendant of common parent.
std::optional<size_t> endpoint_index_in_common_parent;
for (auto child_iter = common_parent->AXPlatformNodeChildrenBegin(),
child_end = common_parent->AXPlatformNodeChildrenEnd();
child_iter != child_end; ++child_iter) {
if (endpoint_object->IsDescendantOf(child_iter.get())) {
endpoint_index_in_common_parent = child_iter->GetIndexInParent();
break;
}
}
if (endpoint_index_in_common_parent < index_in_common_parent) {
// In earlier point in tree than endpoint_object.
return 0;
}
if (endpoint_index_in_common_parent > index_in_common_parent) {
// In later point in the tree than endpoint_object.
return static_cast<int>(GetHypertext().size());
}
// TODO(crbug.com/40897578): Make sure this doesn't fire then turn the last
// conditional into a CHECK_GT(endpoint_index_in_common_parent,
// index_in_common_parent); and remove this code path.
DUMP_WILL_BE_NOTREACHED()
<< "Was not in descendant, so the endpoint_index_in_common_parent should "
"be < or > than the index_in_common_parent:\n"
<< "\n* This: " << this << "\n* Endpoint object: " << endpoint_object
<< "\n* Endpoint offset: " << endpoint_offset
<< "\n* Common parent: " << common_parent
<< "\n* Index in common parent: " << index_in_common_parent.value_or(-99)
<< "\n* Endpoint in common parent: "
<< endpoint_index_in_common_parent.value_or(-99);
return -1;
}
AXPlatformNodeBase::AXPosition AXPlatformNodeBase::HypertextOffsetToEndpoint(
int hypertext_offset) const {
DCHECK_GE(hypertext_offset, 0);
// The offset can be equal to the length when it is past the end.
DCHECK_LE(hypertext_offset, static_cast<int>(GetHypertext().size()));
if (IsLeaf()) {
if (IsText()) {
return GetDelegate()->CreateTextPositionAt(hypertext_offset);
}
return GetDelegate()->CreatePositionAt(hypertext_offset);
}
int current_hypertext_offset = hypertext_offset;
for (auto child_iter = AXPlatformNodeChildrenBegin(),
child_end = AXPlatformNodeChildrenEnd();
child_iter != child_end && current_hypertext_offset >= 0; ++child_iter) {
int child_text_len = 1;
if (child_iter->IsText())
child_text_len =
base::checked_cast<int>(child_iter->GetHypertext().size());
if (current_hypertext_offset <= child_text_len) {
int endpoint_offset = current_hypertext_offset;
if (child_iter->IsText())
return child_iter->GetDelegate()->CreateTextPositionAt(endpoint_offset);
return child_iter->GetDelegate()->CreatePositionAt(endpoint_offset);
}
current_hypertext_offset -= child_text_len;
}
return AXNodePosition::CreateNullPosition();
}
int AXPlatformNodeBase::GetSelectionAnchor(const AXSelection* selection) {
DCHECK(selection);
AXNodeID anchor_id = selection->anchor_object_id;
AXPlatformNodeBase* anchor_object =
static_cast<AXPlatformNodeBase*>(GetDelegate()->GetFromNodeID(anchor_id));
if (!anchor_object)
return -1;
return GetHypertextOffsetFromEndpoint(anchor_object,
selection->anchor_offset);
}
int AXPlatformNodeBase::GetSelectionFocus(const AXSelection* selection) {
DCHECK(selection);
AXNodeID focus_id = selection->focus_object_id;
AXPlatformNodeBase* focus_object =
static_cast<AXPlatformNodeBase*>(GetDelegate()->GetFromNodeID(focus_id));
if (!focus_object)
return -1;
return GetHypertextOffsetFromEndpoint(focus_object, selection->focus_offset);
}
void AXPlatformNodeBase::GetSelectionOffsets(int* selection_start,
int* selection_end) {
GetSelectionOffsets(nullptr, selection_start, selection_end);
}
void AXPlatformNodeBase::GetSelectionOffsets(const AXSelection* selection,
int* selection_start,
int* selection_end) {
DCHECK(selection_start && selection_end);
if (IsAtomicTextField() &&
GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart,
selection_start) &&
GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, selection_end)) {
return;
}
// If the unignored selection has not been computed yet, compute it now.
AXSelection unignored_selection;
if (!selection) {
unignored_selection = GetDelegate()->GetUnignoredSelection();
selection = &unignored_selection;
}
DCHECK(selection);
GetSelectionOffsetsFromTree(selection, selection_start, selection_end);
}
int AXPlatformNodeBase::GetCaretOffset() {
if (IsAtomicTextField()) {
return GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd);
}
// If the unignored selection has not been computed yet, compute it now.
AXSelection unignored_selection = GetDelegate()->GetUnignoredSelection();
int selection_start, selection_end;
GetSelectionOffsetsFromTree(&unignored_selection, &selection_start,
&selection_end, /*caret_only*/ true);
return selection_end;
}
void AXPlatformNodeBase::GetSelectionOffsetsFromTree(
const AXSelection* selection,
int* selection_start,
int* selection_end,
bool caret_only) {
DCHECK(selection_start && selection_end);
*selection_start = GetSelectionAnchor(selection);
*selection_end = GetSelectionFocus(selection);
if (*selection_start < 0 || *selection_end < 0)
return;
// There are three cases when a selection would start and end on the same
// character:
// 1. Anchor and focus are both in a subtree that is to the right of this
// object.
// 2. Anchor and focus are both in a subtree that is to the left of this
// object.
// 3. Anchor and focus are in a subtree represented by a single embedded
// object character.
// Only case 3 refers to a valid selection because cases 1 and 2 fall
// outside this object in their entirety.
// Selections that span more than one character are by definition inside
// this object, so checking them is not necessary.
if (*selection_start == *selection_end && !HasVisibleCaretOrSelection()) {
*selection_start = -1;
*selection_end = -1;
return;
}
if (caret_only) {
// Just return the offsets, skipping the below computation that returns
// and end offset after an embedded object character when the selection
// ends wihin the descendant subtree.
return;
}
// The IA2 Spec says that if the largest of the two offsets falls on an
// embedded object character and if there is a selection in that embedded
// object, it should be incremented by one so that it points after the
// embedded object character.
// This is a signal to AT software that the embedded object is also part of
// the selection.
int* largest_offset =
(*selection_start <= *selection_end) ? selection_end : selection_start;
const std::map<int, int>& offset_to_child_index =
GetDelegate()->GetHypertextOffsetToHyperlinkChildIndex();
auto index_iter = offset_to_child_index.find(*largest_offset);
if (index_iter == offset_to_child_index.end())
return;
int child_index = index_iter->second;
DCHECK_GE(child_index, 0);
DCHECK_LT(static_cast<size_t>(child_index), GetChildCount());
AXPlatformNodeBase* hyperlink = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(ChildAtIndex(child_index)));
if (!hyperlink)
return;
int hyperlink_selection_start, hyperlink_selection_end;
hyperlink->GetSelectionOffsets(selection, &hyperlink_selection_start,
&hyperlink_selection_end);
if (hyperlink_selection_start >= 0 && hyperlink_selection_end >= 0 &&
hyperlink_selection_start != hyperlink_selection_end) {
++(*largest_offset);
}
}
bool AXPlatformNodeBase::IsSameHypertextCharacter(
const AXLegacyHypertext& old_hypertext,
size_t old_char_index,
size_t new_char_index) {
if (old_char_index >= old_hypertext.hypertext.size() ||
new_char_index >= hypertext_.hypertext.size()) {
return false;
}
// For anything other than the "embedded character", we just compare the
// characters directly.
char16_t old_ch = old_hypertext.hypertext[old_char_index];
char16_t new_ch = hypertext_.hypertext[new_char_index];
if (old_ch != new_ch)
return false;
if (new_ch != kEmbeddedCharacter)
return true;
// If it's an embedded character, they're only identical if the child id
// the hyperlink points to is the same.
const auto& old_offset_to_index = old_hypertext.hyperlink_offset_to_index;
const std::vector<int32_t>& old_hyperlinks = old_hypertext.hyperlinks;
int32_t old_hyperlinkscount = static_cast<int32_t>(old_hyperlinks.size());
auto iter = old_offset_to_index.find(static_cast<int32_t>(old_char_index));
int old_index = (iter != old_offset_to_index.end()) ? iter->second : -1;
int old_child_id = (old_index >= 0 && old_index < old_hyperlinkscount)
? old_hyperlinks[old_index]
: -1;
const auto& new_offset_to_index = hypertext_.hyperlink_offset_to_index;
const std::vector<int32_t>& new_hyperlinks = hypertext_.hyperlinks;
int32_t new_hyperlinkscount = static_cast<int32_t>(new_hyperlinks.size());
iter = new_offset_to_index.find(static_cast<int32_t>(new_char_index));
int new_index = (iter != new_offset_to_index.end()) ? iter->second : -1;
int new_child_id = (new_index >= 0 && new_index < new_hyperlinkscount)
? new_hyperlinks[new_index]
: -1;
return old_child_id == new_child_id;
}
// Return true if the index represents a text character.
bool AXPlatformNodeBase::IsText(const std::u16string& text,
size_t index,
bool is_indexed_from_end) {
size_t text_len = text.size();
if (index == text_len)
return false;
auto ch = text[is_indexed_from_end ? text_len - index - 1 : index];
return ch != kEmbeddedCharacter;
}
bool AXPlatformNodeBase::IsPlatformCheckable() const {
return GetData().HasCheckedState();
}
void AXPlatformNodeBase::ComputeHypertextRemovedAndInserted(
const AXLegacyHypertext& old_hypertext,
size_t* start,
size_t* old_len,
size_t* new_len) {
*start = 0;
*old_len = 0;
*new_len = 0;
// Do not compute for text objects, otherwise redundant text change
// announcements will occur in live regions, as the parent hypertext also
// changes.
if (IsText())
return;
const std::u16string& old_text = old_hypertext.hypertext;
const std::u16string& new_text = hypertext_.hypertext;
// TODO(accessibility) Plumb through which part of text changed so we don't
// have to guess what changed based on character differences. This can be
// wrong in some cases as follows:
// -- EDITABLE --
// If editable: when part of the text node changes, assume only that part
// changed, and not the entire thing. For example, if "car" changes to
// "cat", assume only 1 letter changed. This code compares common characters
// to guess what has changed.
// -- NOT EDITABLE --
// When part of the text changes, assume the entire node's text changed. For
// example, if "car" changes to "cat" then assume all 3 letters changed.
// Note, it is possible (though rare) that CharacterData methods are used to
// remove, insert, replace or append a substring.
bool allow_partial_text_node_changes = HasState(ax::mojom::State::kEditable);
size_t prefix_index = 0;
size_t common_prefix = 0;
while (prefix_index < old_text.size() && prefix_index < new_text.size() &&
IsSameHypertextCharacter(old_hypertext, prefix_index, prefix_index)) {
++prefix_index;
if (allow_partial_text_node_changes ||
(!IsText(old_text, prefix_index) && !IsText(new_text, prefix_index))) {
common_prefix = prefix_index;
}
}
size_t suffix_index = 0;
size_t common_suffix = 0;
while (common_prefix + suffix_index < old_text.size() &&
common_prefix + suffix_index < new_text.size() &&
IsSameHypertextCharacter(old_hypertext,
old_text.size() - suffix_index - 1,
new_text.size() - suffix_index - 1)) {
++suffix_index;
if (allow_partial_text_node_changes ||
(!IsText(old_text, suffix_index, true) &&
!IsText(new_text, suffix_index, true))) {
common_suffix = suffix_index;
}
}
*start = common_prefix;
*old_len = old_text.size() - common_prefix - common_suffix;
*new_len = new_text.size() - common_prefix - common_suffix;
}
int AXPlatformNodeBase::FindTextBoundary(
ax::mojom::TextBoundary boundary,
int offset,
ax::mojom::MoveDirection direction,
ax::mojom::TextAffinity affinity) const {
DCHECK_NE(boundary, ax::mojom::TextBoundary::kNone);
const AXPosition position =
GetDelegate()->CreateTextPositionAt(offset, affinity);
// On Windows and Linux ATK, searching for a text boundary should always stop
// at the boundary of the current object.
AXMovementOptions options{AXBoundaryBehavior::kStopAtAnchorBoundary,
AXBoundaryDetection::kDontCheckInitialPosition};
// On Windows and Linux ATK, it is standard text navigation behavior to stop
// if we are searching in the backwards direction and the current position is
// already at the required text boundary.
if (direction == ax::mojom::MoveDirection::kBackward) {
options.boundary_detection = AXBoundaryDetection::kCheckInitialPosition;
}
const AXPosition boundary_position =
position->CreatePositionAtTextBoundary(boundary, direction, options);
if (boundary_position->IsNullPosition())
return -1;
DCHECK_EQ(boundary_position->GetAnchor(), position->GetAnchor());
DCHECK_GE(boundary_position->text_offset(), 0);
return boundary_position->text_offset();
}
AXPlatformNodeBase* AXPlatformNodeBase::NearestLeafToPoint(
gfx::Point point) const {
// First, scope the search to the node that contains point.
AXPlatformNodeBase* nearest_node =
static_cast<AXPlatformNodeBase*>(AXPlatformNode::FromNativeViewAccessible(
GetDelegate()->HitTestSync(point.x(), point.y())));
if (!nearest_node)
return nullptr;
AXPlatformNodeBase* parent = nearest_node;
// GetFirstChild does not consider if the parent is a leaf.
AXPlatformNodeBase* current_descendant =
parent->GetChildCount() ? parent->GetFirstChild() : nullptr;
AXPlatformNodeBase* nearest_descendant = nullptr;
float shortest_distance;
while (parent && current_descendant) {
// Manhattan Distance is used to provide faster distance estimates.
float current_distance = current_descendant->GetDelegate()
->GetClippedScreenBoundsRect()
.ManhattanDistanceToPoint(point);
if (!nearest_descendant || current_distance < shortest_distance) {
shortest_distance = current_distance;
nearest_descendant = current_descendant;
}
// Traverse
AXPlatformNodeBase* next_sibling = current_descendant->GetNextSibling();
if (next_sibling) {
current_descendant = next_sibling;
} else {
// We have gone through all siblings, update nearest and descend if
// possible.
if (nearest_descendant) {
nearest_node = nearest_descendant;
// If the nearest node is a leaf that does not have a child tree, break.
if (!nearest_node->GetChildCount())
break;
parent = nearest_node;
current_descendant = parent->GetFirstChild();
// Reset nearest_descendant to force the nearest node to be a descendant
// of "parent".
nearest_descendant = nullptr;
}
}
}
return nearest_node;
}
int AXPlatformNodeBase::NearestTextIndexToPoint(gfx::Point point) {
// For text objects, find the text position nearest to the point.The nearest
// index of a non-text object is implicitly 0. Text fields such as textarea
// have an embedded div inside them that holds all the text,
// GetRangeBoundsRect will correctly handle these nodes
int nearest_index = 0;
const AXCoordinateSystem coordinate_system = AXCoordinateSystem::kScreenDIPs;
const AXClippingBehavior clipping_behavior = AXClippingBehavior::kUnclipped;
// Manhattan Distance is used to provide faster distance estimates.
// get the distance from the point to the bounds of each character.
float shortest_distance = GetDelegate()
->GetInnerTextRangeBoundsRect(
0, 1, coordinate_system, clipping_behavior)
.ManhattanDistanceToPoint(point);
for (int i = 1, text_length = GetTextContentLengthUTF16(); i < text_length;
++i) {
float current_distance =
GetDelegate()
->GetInnerTextRangeBoundsRect(i, i + 1, coordinate_system,
clipping_behavior)
.ManhattanDistanceToPoint(point);
if (current_distance < shortest_distance) {
shortest_distance = current_distance;
nearest_index = i;
}
}
return nearest_index;
}
TextAttributeList AXPlatformNodeBase::ComputeTextAttributes() const {
TextAttributeList attributes;
// From the IA2 Spec:
// Occasionally, word processors will automatically generate characters which
// appear on a line along with editable text. The characters are not
// themselves editable, but are part of the document. The most common examples
// of automatically inserted characters are in bulleted and numbered lists.
if (HasBoolAttribute(ax::mojom::BoolAttribute::kNotUserSelectableStyle)) {
// From IA2 text attribute guide:
// this attribute's value is “true” for list bullet/numbering prefix text or
// layout-inserted text such as via the CSS pseudo styles :before or :after.
attributes.emplace_back("auto-generated", "true");
}
AXPlatformNodeDelegate* const delegate = GetDelegate();
int color;
if ((color = delegate->GetBackgroundColor())) {
unsigned int red = SkColorGetR(color);
unsigned int green = SkColorGetG(color);
unsigned int blue = SkColorGetB(color);
std::string color_value = "rgb(" + base::NumberToString(red) + ',' +
base::NumberToString(green) + ',' +
base::NumberToString(blue) + ')';
SanitizeTextAttributeValue(color_value, &color_value);
attributes.emplace_back(std::make_pair("background-color", color_value));
}
if ((color = delegate->GetColor())) {
unsigned int red = SkColorGetR(color);
unsigned int green = SkColorGetG(color);
unsigned int blue = SkColorGetB(color);
std::string color_value = "rgb(" + base::NumberToString(red) + ',' +
base::NumberToString(green) + ',' +
base::NumberToString(blue) + ')';
SanitizeTextAttributeValue(color_value, &color_value);
attributes.emplace_back(std::make_pair("color", color_value));
}
// First try to get the inherited font family name from the delegate. If we
// cannot find any name, fall back to looking the hierarchy of this node's
// AXNodeData instead.
std::string font_family(delegate->GetInheritedFontFamilyName());
if (font_family.empty()) {
font_family =
GetInheritedStringAttribute(ax::mojom::StringAttribute::kFontFamily);
}
// Attribute has no default value.
if (!font_family.empty()) {
SanitizeTextAttributeValue(font_family, &font_family);
attributes.emplace_back(std::make_pair("font-family", font_family));
}
std::optional<float> font_size_in_points = GetFontSizeInPoints();
// Attribute has no default value.
if (font_size_in_points) {
attributes.emplace_back(std::make_pair(
"font-size", base::NumberToString(*font_size_in_points) + "pt"));
}
// TODO(nektar): Add Blink support for the following attributes:
// text-line-through-mode, text-line-through-width, text-outline:false,
// text-position:baseline, text-shadow:none, text-underline-mode:continuous.
int32_t text_style = GetIntAttribute(ax::mojom::IntAttribute::kTextStyle);
if (text_style) {
if (HasTextStyle(ax::mojom::TextStyle::kBold))
attributes.emplace_back(std::make_pair("font-weight", "bold"));
if (HasTextStyle(ax::mojom::TextStyle::kItalic))
attributes.emplace_back(std::make_pair("font-style", "italic"));
if (HasTextStyle(ax::mojom::TextStyle::kLineThrough)) {
// TODO(nektar): Figure out a more specific value.
attributes.emplace_back(
std::make_pair("text-line-through-style", "solid"));
}
if (HasTextStyle(ax::mojom::TextStyle::kUnderline)) {
// TODO(nektar): Figure out a more specific value.
attributes.emplace_back(std::make_pair("text-underline-style", "solid"));
}
}
std::string language = delegate->GetLanguage();
if (!language.empty()) {
SanitizeTextAttributeValue(language, &language);
attributes.emplace_back(std::make_pair("language", language));
}
auto text_direction = static_cast<ax::mojom::WritingDirection>(
GetIntAttribute(ax::mojom::IntAttribute::kTextDirection));
switch (text_direction) {
case ax::mojom::WritingDirection::kNone:
break;
case ax::mojom::WritingDirection::kLtr:
attributes.emplace_back(std::make_pair("writing-mode", "lr"));
break;
case ax::mojom::WritingDirection::kRtl:
attributes.emplace_back(std::make_pair("writing-mode", "rl"));
break;
case ax::mojom::WritingDirection::kTtb:
attributes.emplace_back(std::make_pair("writing-mode", "tb"));
break;
case ax::mojom::WritingDirection::kBtt:
// Not listed in the IA2 Spec.
attributes.emplace_back(std::make_pair("writing-mode", "bt"));
break;
}
auto text_position = static_cast<ax::mojom::TextPosition>(
GetIntAttribute(ax::mojom::IntAttribute::kTextPosition));
switch (text_position) {
case ax::mojom::TextPosition::kNone:
break;
case ax::mojom::TextPosition::kSubscript:
attributes.emplace_back(std::make_pair("text-position", "sub"));
break;
case ax::mojom::TextPosition::kSuperscript:
attributes.emplace_back(std::make_pair("text-position", "super"));
break;
}
return attributes;
}
int AXPlatformNodeBase::GetSelectionCount() const {
int max_items = GetMaxSelectableItems();
if (!max_items)
return 0;
return GetSelectedItems(max_items);
}
AXPlatformNodeBase* AXPlatformNodeBase::GetSelectedItem(
int selected_index) const {
DCHECK_GE(selected_index, 0);
int max_items = GetMaxSelectableItems();
if (max_items == 0)
return nullptr;
if (selected_index >= max_items)
return nullptr;
std::vector<AXPlatformNodeBase*> selected_children;
int requested_count = selected_index + 1;
int returned_count = GetSelectedItems(requested_count, &selected_children);
if (returned_count <= selected_index)
return nullptr;
DCHECK(!selected_children.empty());
DCHECK_LT(selected_index, static_cast<int>(selected_children.size()));
return selected_children[selected_index];
}
int AXPlatformNodeBase::GetSelectedItems(
int max_items,
std::vector<AXPlatformNodeBase*>* out_selected_items) const {
int selected_count = 0;
for (auto child_iter = AXPlatformNodeChildrenBegin(),
child_end = AXPlatformNodeChildrenEnd();
child_iter != child_end && selected_count < max_items; ++child_iter) {
if (!IsItemLike(child_iter->GetRole())) {
selected_count += child_iter->GetSelectedItems(max_items - selected_count,
out_selected_items);
} else if (child_iter->GetBoolAttribute(
ax::mojom::BoolAttribute::kSelected)) {
selected_count++;
if (out_selected_items)
out_selected_items->emplace_back(child_iter.get());
}
}
return selected_count;
}
void AXPlatformNodeBase::SanitizeTextAttributeValue(const std::string& input,
std::string* output) const {
DCHECK(output);
}
bool AXPlatformNodeBase::IsDescribedByTooltip() const {
const std::vector<int32_t>& description_ids =
GetIntListAttribute(ax::mojom::IntListAttribute::kDescribedbyIds);
std::string description_from;
for (int id : description_ids) {
AXPlatformNodeBase* description_object =
static_cast<AXPlatformNodeBase*>(GetDelegate()->GetFromNodeID(id));
if (description_object &&
description_object->GetRole() == ax::mojom::Role::kTooltip) {
return true;
}
}
return false;
}
std::string AXPlatformNodeBase::ComputeDetailsRoles() const {
const std::vector<int32_t>& details_ids =
GetIntListAttribute(ax::mojom::IntListAttribute::kDetailsIds);
if (details_ids.empty())
return std::string();
std::set<std::string> details_roles_set;
AXPlatformNodeDelegate* const delegate = GetDelegate();
for (int id : details_ids) {
AXPlatformNodeBase* detail_object =
static_cast<AXPlatformNodeBase*>(delegate->GetFromNodeID(id));
if (!detail_object)
continue;
switch (detail_object->GetRole()) {
case ax::mojom::Role::kComment:
details_roles_set.insert("comment");
break;
case ax::mojom::Role::kDefinition:
details_roles_set.insert("definition");
break;
case ax::mojom::Role::kDocEndnote:
details_roles_set.insert("doc-endnote");
break;
case ax::mojom::Role::kDocFootnote:
details_roles_set.insert("doc-footnote");
break;
case ax::mojom::Role::kGroup:
case ax::mojom::Role::kRegion: {
if (DescendantHasComment(detail_object)) {
details_roles_set.insert("comment");
break;
}
[[fallthrough]];
}
default:
// If a popover of any kind, use "popover" -- technically this is not a
// role, and therefore, details-roles is more of a hints field. Use * to
// indicate some other role.
if (detail_object->GetDelegate()->node()->HasIntAttribute(
ax::mojom::IntAttribute::kIsPopup)) {
details_roles_set.insert("popover");
} else {
details_roles_set.insert("*");
}
break;
}
}
// Create space delimited list of types. The set will not be large, as there
// are not very many possible types.
std::vector<std::string> details_roles_vector(details_roles_set.begin(),
details_roles_set.end());
return base::JoinString(details_roles_vector, " ");
}
// static
bool AXPlatformNodeBase::DescendantHasComment(const AXPlatformNodeBase* node) {
// These should still report comment if there are comments inside them.
constexpr size_t kMaxChildrenToCheck = 8;
constexpr size_t kMaxDepthToCheck = 4;
if (FindDescendantRoleWithMaxDepth(node, ax::mojom::Role::kComment,
kMaxDepthToCheck, kMaxChildrenToCheck)) {
return true;
}
return false;
}
int AXPlatformNodeBase::GetMaxSelectableItems() const {
if (IsLeaf())
return 0;
if (!IsContainerWithSelectableChildren(GetRole()))
return 0;
int max_items = 1;
if (HasState(ax::mojom::State::kMultiselectable))
max_items = std::numeric_limits<int>::max();
return max_items;
}
} // namespace ui