#include "ui/accessibility/ax_node.h"
#include <algorithm>
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_computed_node_data.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_hypertext.h"
#include "ui/accessibility/ax_language_detection.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_selection.h"
#include "ui/accessibility/ax_table_info.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_manager.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/transform.h"
namespace ui {
constexpr char AXNode::kEmbeddedObjectCharacterUTF8[];
constexpr char16_t AXNode::kEmbeddedObjectCharacterUTF16[];
constexpr int AXNode::kEmbeddedObjectCharacterLengthUTF8;
constexpr int AXNode::kEmbeddedObjectCharacterLengthUTF16;
AXNode::AXNode(AXTree* tree,
AXNode* parent,
AXNodeID id,
size_t index_in_parent,
size_t unignored_index_in_parent)
: tree_(tree),
index_in_parent_(index_in_parent),
unignored_index_in_parent_(unignored_index_in_parent),
parent_(parent) {
DCHECK(tree_);
data_.id = id;
}
AXNode::~AXNode() = default;
AXNodeData&& AXNode::TakeData() {
return std::move(data_);
}
const std::vector<raw_ptr<AXNode, VectorExperimental>>& AXNode::GetAllChildren()
const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return children_;
}
size_t AXNode::GetChildCount() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return children_.size();
}
#if AX_FAIL_FAST_BUILD()
size_t AXNode::GetSubtreeCount() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
size_t count = 1;
for (AXNode* child : children_) {
count += child->GetSubtreeCount();
}
return count;
}
#endif
size_t AXNode::GetChildCountCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
if (child_tree_manager)
return 1u;
return GetChildCount();
}
size_t AXNode::GetUnignoredChildCount() const {
DCHECK(!IsIgnored()) << "Called unignored method on ignored node: " << *this;
DCHECK(!tree_->GetTreeUpdateInProgressState());
return unignored_child_count_;
}
size_t AXNode::GetUnignoredChildCountCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
if (child_tree_manager) {
DCHECK_EQ(unignored_child_count_, 0u)
<< "A node cannot be hosting both a child tree and other nodes as "
"children.";
return 1u;
}
return unignored_child_count_;
}
AXNode* AXNode::GetChildAtIndex(size_t index) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
if (index >= GetChildCount())
return nullptr;
return children_[index];
}
AXNode* AXNode::GetChildAtIndexCrossingTreeBoundary(size_t index) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
if (child_tree_manager) {
DCHECK_EQ(index, 0u)
<< "A node cannot be hosting both a child tree and other nodes as "
"children.";
return child_tree_manager->GetRoot();
}
return GetChildAtIndex(index);
}
AXNode* AXNode::GetUnignoredChildAtIndex(size_t index) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
for (auto it = UnignoredChildrenBegin(), end = UnignoredChildrenEnd();
it != end; ++it) {
if (index == 0)
return it.get();
--index;
}
return nullptr;
}
AXNode* AXNode::GetUnignoredChildAtIndexCrossingTreeBoundary(
size_t index) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
if (child_tree_manager) {
DCHECK_EQ(index, 0u)
<< "A node cannot be hosting both a child tree and other nodes as "
"children.";
return child_tree_manager->GetRoot();
}
return GetUnignoredChildAtIndex(index);
}
AXNode* AXNode::GetParent() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return parent_;
}
AXNode* AXNode::GetParentCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
if (parent_)
return parent_;
const AXTreeManager* manager = GetManager();
if (manager)
return manager->GetParentNodeFromParentTree();
return nullptr;
}
AXNode* AXNode::GetUnignoredParent() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
AXNode* unignored_parent = GetParent();
while (unignored_parent && unignored_parent->IsIgnored())
unignored_parent = unignored_parent->GetParent();
return unignored_parent;
}
AXNode* AXNode::GetUnignoredParentCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
AXNode* unignored_parent = GetUnignoredParent();
if (!unignored_parent) {
const AXTreeManager* manager = GetManager();
if (manager)
unignored_parent = manager->GetParentNodeFromParentTree();
}
return unignored_parent;
}
base::queue<AXNode*> AXNode::GetAncestorsCrossingTreeBoundaryAsQueue() const {
base::queue<AXNode*> ancestors;
AXNode* ancestor = const_cast<AXNode*>(this);
while (ancestor) {
ancestors.push(ancestor);
ancestor = ancestor->GetParentCrossingTreeBoundary();
}
return ancestors;
}
base::stack<AXNode*> AXNode::GetAncestorsCrossingTreeBoundaryAsStack() const {
base::stack<AXNode*> ancestors;
AXNode* ancestor = const_cast<AXNode*>(this);
while (ancestor) {
ancestors.push(ancestor);
ancestor = ancestor->GetParentCrossingTreeBoundary();
}
return ancestors;
}
size_t AXNode::GetIndexInParent() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return index_in_parent_;
}
size_t AXNode::GetUnignoredIndexInParent() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return unignored_index_in_parent_;
}
AXNode* AXNode::GetFirstChild() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return GetChildAtIndex(0);
}
AXNode* AXNode::GetFirstChildCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return GetChildAtIndexCrossingTreeBoundary(0);
}
AXNode* AXNode::GetFirstUnignoredChild() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return ComputeFirstUnignoredChildRecursive();
}
AXNode* AXNode::GetFirstUnignoredChildCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
if (child_tree_manager)
return child_tree_manager->GetRoot();
return ComputeFirstUnignoredChildRecursive();
}
AXNode* AXNode::GetLastChild() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
size_t n = GetChildCount();
if (n == 0)
return nullptr;
return GetChildAtIndex(n - 1);
}
AXNode* AXNode::GetLastChildCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
size_t n = GetChildCountCrossingTreeBoundary();
if (n == 0)
return nullptr;
return GetChildAtIndexCrossingTreeBoundary(n - 1);
}
AXNode* AXNode::GetLastUnignoredChild() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return ComputeLastUnignoredChildRecursive();
}
AXNode* AXNode::GetLastUnignoredChildCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
if (child_tree_manager)
return child_tree_manager->GetRoot();
return ComputeLastUnignoredChildRecursive();
}
AXNode* AXNode::GetDeepestFirstDescendant() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
if (!GetChildCount())
return nullptr;
AXNode* deepest_descendant = GetFirstChild();
DCHECK(deepest_descendant);
while (deepest_descendant->GetChildCount()) {
deepest_descendant = deepest_descendant->GetFirstChild();
DCHECK(deepest_descendant);
}
return deepest_descendant;
}
AXNode* AXNode::GetDeepestFirstDescendantCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
if (!GetChildCountCrossingTreeBoundary())
return nullptr;
AXNode* deepest_descendant = GetFirstChildCrossingTreeBoundary();
DCHECK(deepest_descendant);
while (deepest_descendant->GetChildCountCrossingTreeBoundary()) {
deepest_descendant =
deepest_descendant->GetFirstChildCrossingTreeBoundary();
DCHECK(deepest_descendant);
}
return deepest_descendant;
}
AXNode* AXNode::GetDeepestFirstUnignoredDescendant() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
DCHECK(!IsIgnored()) << "Called unignored method on ignored node: " << *this;
if (!GetUnignoredChildCount())
return nullptr;
AXNode* deepest_descendant = GetFirstUnignoredChild();
DCHECK(deepest_descendant);
while (deepest_descendant->GetUnignoredChildCount()) {
deepest_descendant = deepest_descendant->GetFirstUnignoredChild();
DCHECK(deepest_descendant);
}
return deepest_descendant;
}
AXNode* AXNode::GetDeepestFirstUnignoredDescendantCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
DCHECK(!IsIgnored()) << "Called unignored method on ignored node: " << *this;
if (!GetUnignoredChildCountCrossingTreeBoundary())
return nullptr;
AXNode* deepest_descendant = GetFirstUnignoredChildCrossingTreeBoundary();
DCHECK(deepest_descendant);
while (deepest_descendant->GetUnignoredChildCountCrossingTreeBoundary()) {
deepest_descendant =
deepest_descendant->GetFirstUnignoredChildCrossingTreeBoundary();
DCHECK(deepest_descendant);
}
return deepest_descendant;
}
AXNode* AXNode::GetDeepestLastDescendant() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
if (!GetChildCount())
return nullptr;
AXNode* deepest_descendant = GetLastChild();
DCHECK(deepest_descendant);
while (deepest_descendant->GetChildCount()) {
deepest_descendant = deepest_descendant->GetLastChild();
DCHECK(deepest_descendant);
}
return deepest_descendant;
}
AXNode* AXNode::GetDeepestLastDescendantCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
DCHECK(!IsIgnored()) << "Called unignored method on ignored node: " << *this;
if (!GetChildCountCrossingTreeBoundary())
return nullptr;
AXNode* deepest_descendant = GetLastChildCrossingTreeBoundary();
DCHECK(deepest_descendant);
while (deepest_descendant->GetChildCountCrossingTreeBoundary()) {
deepest_descendant = deepest_descendant->GetLastChildCrossingTreeBoundary();
DCHECK(deepest_descendant);
}
return deepest_descendant;
}
AXNode* AXNode::GetDeepestLastUnignoredDescendant() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
DCHECK(!IsIgnored()) << "Called unignored method on ignored node: " << *this;
if (!GetUnignoredChildCount())
return nullptr;
AXNode* deepest_descendant = GetLastUnignoredChild();
DCHECK(deepest_descendant);
while (deepest_descendant->GetUnignoredChildCount()) {
deepest_descendant = deepest_descendant->GetLastUnignoredChild();
DCHECK(deepest_descendant);
}
return deepest_descendant;
}
AXNode* AXNode::GetDeepestLastUnignoredDescendantCrossingTreeBoundary() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
DCHECK(!IsIgnored()) << "Called unignored method on ignored node: " << *this;
if (!GetUnignoredChildCountCrossingTreeBoundary())
return nullptr;
AXNode* deepest_descendant = GetLastUnignoredChildCrossingTreeBoundary();
DCHECK(deepest_descendant);
while (deepest_descendant->GetUnignoredChildCountCrossingTreeBoundary()) {
deepest_descendant =
deepest_descendant->GetLastUnignoredChildCrossingTreeBoundary();
DCHECK(deepest_descendant);
}
return deepest_descendant;
}
AXNode* AXNode::GetNextSibling() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
AXNode* parent = GetParent();
if (!parent)
return nullptr;
DCHECK(parent || !GetIndexInParent())
<< "Root nodes lack a parent. Their index_in_parent should be 0.";
size_t nextIndex = GetIndexInParent() + 1;
if (nextIndex >= parent->GetChildCount())
return nullptr;
return parent->GetChildAtIndex(nextIndex);
}
AXNode* AXNode::GetNextUnignoredSibling() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXNode* current = this;
bool considerChildren = false;
while (current) {
AXNode* candidate;
if (considerChildren && (candidate = current->GetFirstChild())) {
if (!candidate->IsIgnored())
return candidate;
current = candidate;
} else if ((candidate = current->GetNextSibling())) {
if (!candidate->IsIgnored())
return candidate;
current = candidate;
considerChildren = true;
} else {
current = current->GetParent();
if (!current || !current->IsIgnored())
return nullptr;
considerChildren = false;
}
}
return nullptr;
}
AXNode* AXNode::GetPreviousSibling() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
DCHECK(GetParent() || !GetIndexInParent())
<< "Root nodes lack a parent. Their index_in_parent should be 0.";
size_t index = GetIndexInParent();
if (index == 0)
return nullptr;
return GetParent()->GetChildAtIndex(index - 1);
}
AXNode* AXNode::GetPreviousUnignoredSibling() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXNode* current = this;
bool considerChildren = false;
while (current) {
AXNode* candidate;
if (considerChildren && (candidate = current->GetLastChild())) {
if (!candidate->IsIgnored())
return candidate;
current = candidate;
} else if ((candidate = current->GetPreviousSibling())) {
if (!candidate->IsIgnored())
return candidate;
current = candidate;
considerChildren = true;
} else {
current = current->GetParent();
if (!current || !current->IsIgnored())
return nullptr;
considerChildren = false;
}
}
return nullptr;
}
AXNode* AXNode::GetNextUnignoredInTreeOrder() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
if (GetUnignoredChildCount())
return GetFirstUnignoredChild();
const AXNode* node = this;
while (node) {
AXNode* sibling = node->GetNextUnignoredSibling();
if (sibling)
return sibling;
node = node->GetUnignoredParent();
}
return nullptr;
}
AXNode* AXNode::GetPreviousUnignoredInTreeOrder() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
AXNode* sibling = GetPreviousUnignoredSibling();
if (!sibling)
return GetUnignoredParent();
if (sibling->GetUnignoredChildCount())
return sibling->GetDeepestLastUnignoredDescendant();
return sibling;
}
AXNode::AllChildCrossingTreeBoundaryIterator
AXNode::AllChildrenCrossingTreeBoundaryBegin() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return AllChildCrossingTreeBoundaryIterator(
this, GetFirstChildCrossingTreeBoundary());
}
AXNode::AllChildCrossingTreeBoundaryIterator
AXNode::AllChildrenCrossingTreeBoundaryEnd() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return AllChildCrossingTreeBoundaryIterator(this, nullptr);
}
AXNode::UnignoredChildIterator AXNode::UnignoredChildrenBegin() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return UnignoredChildIterator(this, GetFirstUnignoredChild());
}
AXNode::UnignoredChildIterator AXNode::UnignoredChildrenEnd() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return UnignoredChildIterator(this, nullptr);
}
AXNode::UnignoredChildCrossingTreeBoundaryIterator
AXNode::UnignoredChildrenCrossingTreeBoundaryBegin() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return UnignoredChildCrossingTreeBoundaryIterator(
this, GetFirstUnignoredChildCrossingTreeBoundary());
}
AXNode::UnignoredChildCrossingTreeBoundaryIterator
AXNode::UnignoredChildrenCrossingTreeBoundaryEnd() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return UnignoredChildCrossingTreeBoundaryIterator(this, nullptr);
}
bool AXNode::CanFireEvents() const {
return !IsChildOfLeaf();
}
AXNode* AXNode::GetLowestCommonAncestor(const AXNode& other) {
if (this == &other)
return this;
AXNode* common_ancestor = nullptr;
base::stack<AXNode*> our_ancestors =
GetAncestorsCrossingTreeBoundaryAsStack();
base::stack<AXNode*> other_ancestors =
other.GetAncestorsCrossingTreeBoundaryAsStack();
while (!our_ancestors.empty() && !other_ancestors.empty() &&
our_ancestors.top() == other_ancestors.top()) {
common_ancestor = our_ancestors.top();
our_ancestors.pop();
other_ancestors.pop();
}
return common_ancestor;
}
std::optional<int> AXNode::CompareTo(const AXNode& other) const {
if (this == &other)
return 0;
AXNode* common_ancestor = nullptr;
base::stack<AXNode*> our_ancestors =
GetAncestorsCrossingTreeBoundaryAsStack();
base::stack<AXNode*> other_ancestors =
other.GetAncestorsCrossingTreeBoundaryAsStack();
while (!our_ancestors.empty() && !other_ancestors.empty() &&
our_ancestors.top() == other_ancestors.top()) {
common_ancestor = our_ancestors.top();
our_ancestors.pop();
other_ancestors.pop();
}
if (!common_ancestor)
return std::nullopt;
if (common_ancestor == this)
return -1;
if (common_ancestor == &other)
return 1;
if (our_ancestors.empty() || other_ancestors.empty()) {
NOTREACHED() << "The common ancestor should be followed by two uncommon "
"children in the two corresponding lists of ancestors.";
}
size_t this_uncommon_ancestor_index = our_ancestors.top()->GetIndexInParent();
size_t other_uncommon_ancestor_index =
other_ancestors.top()->GetIndexInParent();
DCHECK_NE(this_uncommon_ancestor_index, other_uncommon_ancestor_index)
<< "Deepest uncommon ancestors should truly be uncommon, i.e. not be the "
"same node.";
return this_uncommon_ancestor_index - other_uncommon_ancestor_index;
}
bool AXNode::IsText() const {
if (GetRole() == ax::mojom::Role::kListMarker)
return !IsIgnored() && !GetUnignoredChildCount();
return ui::IsText(GetRole());
}
bool AXNode::IsLineBreak() const {
return GetRole() == ax::mojom::Role::kLineBreak ||
(GetRole() == ax::mojom::Role::kInlineTextBox &&
GetBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject));
}
void AXNode::SetData(const AXNodeData& src) {
data_ = src;
}
void AXNode::SetLocation(AXNodeID offset_container_id,
const gfx::RectF& location,
gfx::Transform* transform) {
data_.relative_bounds.offset_container_id = offset_container_id;
data_.relative_bounds.bounds = location;
if (transform) {
data_.relative_bounds.transform =
std::make_unique<gfx::Transform>(*transform);
} else {
data_.relative_bounds.transform.reset();
}
}
void AXNode::SetScrollInfo(const int& scroll_x, const int& scroll_y) {
data_.AddIntAttribute(ax::mojom::IntAttribute::kScrollX, scroll_x);
data_.AddIntAttribute(ax::mojom::IntAttribute::kScrollY, scroll_y);
}
void AXNode::GetScrollInfo(int* scroll_x, int* scroll_y) const {
*scroll_x = GetIntAttribute(ax::mojom::IntAttribute::kScrollX);
*scroll_y = GetIntAttribute(ax::mojom::IntAttribute::kScrollY);
}
void AXNode::SetIndexInParent(size_t index_in_parent) {
index_in_parent_ = index_in_parent;
}
void AXNode::UpdateUnignoredCachedValues() {
computed_node_data_.reset();
if (!IsIgnored())
UpdateUnignoredCachedValuesRecursive(0);
}
void AXNode::SwapChildren(
std::vector<raw_ptr<AXNode, VectorExperimental>>* children) {
children->swap(children_);
}
bool AXNode::IsDescendantOf(const AXNode* ancestor) const {
if (!ancestor)
return false;
if (this == ancestor)
return true;
if (const AXNode* parent = GetParent())
return parent->IsDescendantOf(ancestor);
return false;
}
bool AXNode::IsDescendantOfCrossingTreeBoundary(const AXNode* ancestor) const {
if (!ancestor)
return false;
if (this == ancestor)
return true;
if (const AXNode* parent = GetParentCrossingTreeBoundary())
return parent->IsDescendantOfCrossingTreeBoundary(ancestor);
return false;
}
SkColor AXNode::ComputeColor() const {
return ComputeColorAttribute(ax::mojom::IntAttribute::kColor);
}
SkColor AXNode::ComputeBackgroundColor() const {
return ComputeColorAttribute(ax::mojom::IntAttribute::kBackgroundColor);
}
SkColor AXNode::ComputeColorAttribute(ax::mojom::IntAttribute attr) const {
SkColor color = GetIntAttribute(attr);
AXNode* ancestor = GetParent();
while (ancestor && SkColorGetA(color) != SK_AlphaOPAQUE) {
SkColor background_color = ancestor->GetIntAttribute(attr);
color = color_utils::GetResultingPaintColor(color, background_color);
ancestor = ancestor->GetParent();
}
return color;
}
AXTreeManager* AXNode::GetManager() const {
return AXTreeManager::FromID(tree_->GetAXTreeID());
}
bool AXNode::HasVisibleCaretOrSelection() const {
const AXSelection selection = GetSelection();
const AXNode* focus = tree()->GetFromId(selection.focus_object_id);
if (!focus || !focus->IsDescendantOf(this))
return false;
const AXNode* text_field = GetTextFieldAncestor();
if (text_field)
return true;
return !selection.IsCollapsed();
}
AXSelection AXNode::GetSelection() const {
DCHECK(tree()) << "Cannot retrieve the current selection if the node is not "
"attached to an accessibility tree.\n"
<< *this;
return tree()->GetSelection();
}
AXSelection AXNode::GetUnignoredSelection() const {
DCHECK(tree()) << "Cannot retrieve the current selection if the node is not "
"attached to an accessibility tree.\n"
<< *this;
AXSelection selection = tree()->GetUnignoredSelection();
const AXNode* anchor = tree()->GetFromId(selection.anchor_object_id);
if (anchor && !anchor->IsLeaf()) {
DCHECK_GE(selection.anchor_offset, 0);
if (static_cast<size_t>(selection.anchor_offset) <
anchor->GetChildCount()) {
const AXNode* anchor_child =
anchor->GetChildAtIndex(selection.anchor_offset);
DCHECK(anchor_child);
selection.anchor_offset =
static_cast<int>(anchor_child->GetUnignoredIndexInParent());
} else {
selection.anchor_offset =
static_cast<int>(anchor->GetUnignoredChildCount());
}
}
const AXNode* focus = tree()->GetFromId(selection.focus_object_id);
if (focus && !focus->IsLeaf()) {
DCHECK_GE(selection.focus_offset, 0);
if (static_cast<size_t>(selection.focus_offset) < focus->GetChildCount()) {
const AXNode* focus_child =
focus->GetChildAtIndex(selection.focus_offset);
DCHECK(focus_child);
selection.focus_offset =
static_cast<int>(focus_child->GetUnignoredIndexInParent());
} else {
selection.focus_offset =
static_cast<int>(focus->GetUnignoredChildCount());
}
}
return selection;
}
bool AXNode::HasIntAttribute(ax::mojom::IntAttribute attribute) const {
if (data().HasIntAttribute(attribute)) {
return true;
}
return CanComputeIntAttribute(attribute);
}
bool AXNode::CanComputeIntAttribute(ax::mojom::IntAttribute attribute) const {
if (attribute != ax::mojom::IntAttribute::kNextOnLineId &&
attribute != ax::mojom::IntAttribute::kPreviousOnLineId) {
return false;
}
if (!::features::IsAccessibilityPruneRedundantInlineConnectivityEnabled()) {
return false;
}
if (data().role != ax::mojom::Role::kInlineTextBox) {
return false;
}
if (!GetParent()) {
return false;
}
if (this == GetParent()->GetFirstChild() &&
attribute == ax::mojom::IntAttribute::kPreviousOnLineId) {
return GetParent()->data().HasIntAttribute(attribute);
}
if (this == GetParent()->GetLastChild() &&
attribute == ax::mojom::IntAttribute::kNextOnLineId) {
return GetParent()->data().HasIntAttribute(attribute);
}
return false;
}
int AXNode::GetIntAttribute(ax::mojom::IntAttribute attribute) const {
int value = data().GetIntAttribute(attribute);
if (value != kDefaultIntValue || data().HasIntAttribute(attribute)) {
return value;
}
if (CanComputeIntAttribute(attribute)) {
return GetParent()->data().GetIntAttribute(attribute);
}
return kDefaultIntValue;
}
bool AXNode::HasStringAttribute(ax::mojom::StringAttribute attribute) const {
if (data().HasStringAttribute(attribute)) {
return true;
}
return CanComputeStringAttribute(attribute);
}
bool AXNode::CanComputeStringAttribute(
ax::mojom::StringAttribute attribute) const {
switch (attribute) {
case ax::mojom::StringAttribute::kValue:
return data().IsNonAtomicTextField();
case ax::mojom::StringAttribute::kName:
return data().role == ax::mojom::Role::kInlineTextBox &&
data().GetNameFrom() == ax::mojom::NameFrom::kContents &&
GetParent() &&
GetParent()->data().GetNameFrom() ==
ax::mojom::NameFrom::kContents &&
GetParent()->data().HasStringAttribute(
ax::mojom::StringAttribute::kName);
default:
return false;
}
}
#if BUILDFLAG(ARKWEB_ACCESSIBILITY)
bool AXNode::GetStringAttribute(ax::mojom::StringAttribute attribute,
std::string* value) const {
if (GetComputedNodeData().HasOrCanComputeAttribute(attribute)) {
*value = GetComputedNodeData().GetOrComputeAttributeUTF8(attribute);
return true;
}
return false;
}
#endif
const std::string& AXNode::GetStringAttribute(
ax::mojom::StringAttribute attribute) const {
if (data().HasStringAttribute(attribute)) {
return data().GetStringAttribute(attribute);
}
if (CanComputeStringAttribute(attribute)) {
return GetComputedNodeData().ComputeAttributeUTF8(attribute);
}
return base::EmptyString();
}
std::u16string AXNode::GetString16Attribute(
ax::mojom::StringAttribute attribute) const {
if (data().HasStringAttribute(attribute)) {
const std::string& value_utf8 = data().GetStringAttribute(attribute);
return base::UTF8ToUTF16(value_utf8);
}
if (CanComputeStringAttribute(attribute)) {
return GetComputedNodeData().ComputeAttributeUTF16(attribute);
}
return std::u16string();
}
bool AXNode::HasInheritedStringAttribute(
ax::mojom::StringAttribute attribute) const {
for (const AXNode* current_node = this; current_node;
current_node = current_node->GetParent()) {
if (current_node->HasStringAttribute(attribute))
return true;
}
return false;
}
const std::string& AXNode::GetInheritedStringAttribute(
ax::mojom::StringAttribute attribute) const {
for (const AXNode* current_node = this; current_node;
current_node = current_node->GetParent()) {
if (current_node->HasStringAttribute(attribute))
return current_node->GetStringAttribute(attribute);
}
return base::EmptyString();
}
std::u16string AXNode::GetInheritedString16Attribute(
ax::mojom::StringAttribute attribute) const {
return base::UTF8ToUTF16(GetInheritedStringAttribute(attribute));
}
bool AXNode::HasIntListAttribute(ax::mojom::IntListAttribute attribute) const {
if (data().HasIntListAttribute(attribute)) {
return true;
}
return CanComputeIntListAttribute(attribute);
}
bool AXNode::CanComputeIntListAttribute(
ax::mojom::IntListAttribute attribute) const {
switch (attribute) {
case ax::mojom::IntListAttribute::kLineStarts:
case ax::mojom::IntListAttribute::kLineEnds:
case ax::mojom::IntListAttribute::kSentenceStarts:
case ax::mojom::IntListAttribute::kSentenceEnds:
case ax::mojom::IntListAttribute::kWordStarts:
case ax::mojom::IntListAttribute::kWordEnds:
return true;
default:
return false;
}
}
const std::vector<int32_t>& AXNode::GetIntListAttribute(
ax::mojom::IntListAttribute attribute) const {
if (data().HasIntListAttribute(attribute)) {
return data().GetIntListAttribute(attribute);
}
if (CanComputeIntListAttribute(attribute)) {
return GetComputedNodeData().ComputeAttribute(attribute);
}
return data().GetIntListAttribute(ax::mojom::IntListAttribute::kNone);
}
AXLanguageInfo* AXNode::GetLanguageInfo() const {
return language_info_.get();
}
void AXNode::SetLanguageInfo(std::unique_ptr<AXLanguageInfo> lang_info) {
language_info_ = std::move(lang_info);
}
void AXNode::ClearLanguageInfo() {
language_info_.reset();
}
const AXComputedNodeData& AXNode::GetComputedNodeData() const {
if (!computed_node_data_)
computed_node_data_ = std::make_unique<AXComputedNodeData>(*this);
return *computed_node_data_;
}
void AXNode::ClearComputedNodeData() {
computed_node_data_.reset();
}
const std::string& AXNode::GetNameUTF8() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return this->GetStringAttribute(ax::mojom::StringAttribute::kName);
}
std::u16string AXNode::GetNameUTF16() const {
return base::UTF8ToUTF16(GetNameUTF8());
}
const std::u16string& AXNode::GetHypertext() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
hypertext_ = AXHypertext();
if (IsLeaf() || IsChildOfLeaf()) {
hypertext_.hypertext = GetTextContentUTF16();
} else {
static const base::NoDestructor<std::u16string> embedded_character_str(
AXNode::kEmbeddedObjectCharacterUTF16);
auto first = UnignoredChildrenCrossingTreeBoundaryBegin();
for (auto iter = first, end = UnignoredChildrenCrossingTreeBoundaryEnd();
iter != end; ++iter) {
if (iter->IsText()) {
hypertext_.hypertext += iter->GetTextContentUTF16();
} else {
int character_offset = static_cast<int>(hypertext_.hypertext.size());
auto inserted =
hypertext_.hypertext_offset_to_hyperlink_child_index.emplace(
character_offset, static_cast<int>(std::distance(first, iter)));
DCHECK(inserted.second) << "An embedded object at " << character_offset
<< " has already been encountered.";
hypertext_.hypertext += *embedded_character_str;
}
}
}
hypertext_.needs_update = false;
return hypertext_.hypertext;
}
const std::map<int, int>& AXNode::GetHypertextOffsetToHyperlinkChildIndex()
const {
GetHypertext();
return hypertext_.hypertext_offset_to_hyperlink_child_index;
}
const std::string& AXNode::GetTextContentUTF8() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return GetComputedNodeData().GetOrComputeTextContentUTF8();
}
const std::u16string& AXNode::GetTextContentUTF16() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return GetComputedNodeData().GetOrComputeTextContentUTF16();
}
int AXNode::GetTextContentLengthUTF8() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return GetComputedNodeData().GetOrComputeTextContentLengthUTF8();
}
int AXNode::GetTextContentLengthUTF16() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return GetComputedNodeData().GetOrComputeTextContentLengthUTF16();
}
gfx::RectF AXNode::GetTextContentRangeBoundsUTF16(int start_offset,
int end_offset) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
DCHECK_LE(start_offset, end_offset)
<< "Invalid `start_offset` and `end_offset`.\n"
<< start_offset << ' ' << end_offset << "\nin\n"
<< *this;
int text_content_length = GetTextContentLengthUTF16();
if (end_offset > text_content_length) {
return gfx::RectF();
}
const std::vector<int32_t>& character_offsets =
GetIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets);
int character_offsets_length =
base::checked_cast<int>(character_offsets.size());
if (character_offsets_length < GetTextContentLengthUTF16()) {
start_offset = std::min(start_offset, character_offsets_length);
end_offset = std::min(end_offset, character_offsets_length);
}
int start_pixel_offset =
start_offset > 0
? character_offsets[base::checked_cast<size_t>(start_offset - 1)]
: 0;
int end_pixel_offset =
end_offset > 0
? character_offsets[base::checked_cast<size_t>(end_offset - 1)]
: 0;
int max_pixel_offset = character_offsets_length > 0
? character_offsets[character_offsets_length - 1]
: 0;
const gfx::RectF& node_bounds = data().relative_bounds.bounds;
gfx::RectF out_bounds;
switch (static_cast<ax::mojom::WritingDirection>(
GetIntAttribute(ax::mojom::IntAttribute::kTextDirection))) {
case ax::mojom::WritingDirection::kNone:
case ax::mojom::WritingDirection::kLtr:
out_bounds = gfx::RectF(start_pixel_offset, 0,
end_pixel_offset - start_pixel_offset,
node_bounds.height());
break;
case ax::mojom::WritingDirection::kRtl: {
int left = max_pixel_offset - end_pixel_offset;
int right = max_pixel_offset - start_pixel_offset;
out_bounds = gfx::RectF(left, 0, right - left, node_bounds.height());
break;
}
case ax::mojom::WritingDirection::kTtb:
out_bounds = gfx::RectF(0, start_pixel_offset, node_bounds.width(),
end_pixel_offset - start_pixel_offset);
break;
case ax::mojom::WritingDirection::kBtt: {
int top = max_pixel_offset - end_pixel_offset;
int bottom = max_pixel_offset - start_pixel_offset;
out_bounds = gfx::RectF(0, top, node_bounds.width(), bottom - top);
break;
}
}
return out_bounds;
}
std::string AXNode::GetLanguage() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
for (const AXNode* cur = this; cur; cur = cur->GetParent()) {
const AXLanguageInfo* lang_info = cur->GetLanguageInfo();
if (lang_info && !lang_info->language.empty())
return lang_info->language;
if (cur->HasStringAttribute(ax::mojom::StringAttribute::kLanguage))
return cur->GetStringAttribute(ax::mojom::StringAttribute::kLanguage);
}
return std::string();
}
std::string AXNode::GetValueForControl() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
if (data().IsTextField()) {
return GetStringAttribute(ax::mojom::StringAttribute::kValue);
}
if (data().IsRangeValueSupported())
return GetTextForRangeValue();
if (GetRole() == ax::mojom::Role::kColorWell)
return GetValueForColorWell();
if (!IsControl(GetRole()))
return std::string();
return GetStringAttribute(ax::mojom::StringAttribute::kValue);
}
std::ostream& operator<<(std::ostream& stream, const AXNode& node) {
stream << node.data().ToString( false);
if (node.tree()->GetTreeUpdateInProgressState()) {
return stream;
}
if (node.GetUnignoredChildCountCrossingTreeBoundary()) {
stream << " unignored_child_ids=";
bool needs_comma = false;
for (auto it = node.UnignoredChildrenBegin(),
end = node.UnignoredChildrenEnd();
it != end; ++it) {
if (needs_comma) {
stream << ",";
} else {
needs_comma = true;
}
stream << it.get()->data().id;
}
}
if (node.IsLeaf()) {
stream << " is_leaf";
}
if (node.IsChildOfLeaf()) {
stream << " is_child_of_leaf";
}
return stream;
}
std::ostream& operator<<(std::ostream& stream, const AXNode* node) {
if (!node) {
return stream << "null";
}
return stream << *node;
}
bool AXNode::IsTable() const {
return IsTableLike(GetRole());
}
std::optional<int> AXNode::GetTableColCount() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
return static_cast<int>(table_info->col_count);
}
std::optional<int> AXNode::GetTableRowCount() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
return static_cast<int>(table_info->row_count);
}
std::optional<int> AXNode::GetTableAriaColCount() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
return std::make_optional(table_info->aria_col_count);
}
std::optional<int> AXNode::GetTableAriaRowCount() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
return std::make_optional(table_info->aria_row_count);
}
std::optional<int> AXNode::GetTableCellCount() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
return static_cast<int>(table_info->unique_cell_ids.size());
}
AXNode* AXNode::GetTableCellFromIndex(int index) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return nullptr;
if (index < 0 ||
static_cast<size_t>(index) >= table_info->unique_cell_ids.size()) {
return nullptr;
}
return tree_->GetFromId(
table_info->unique_cell_ids[static_cast<size_t>(index)]);
}
AXNode* AXNode::GetTableCaption() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return nullptr;
return tree_->GetFromId(table_info->caption_id);
}
AXNode* AXNode::GetTableCellFromCoords(int row_index, int col_index) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return nullptr;
if (row_index < 0 ||
static_cast<size_t>(row_index) >= table_info->row_count ||
col_index < 0 ||
static_cast<size_t>(col_index) >= table_info->col_count) {
return nullptr;
}
return tree_->GetFromId(table_info->cell_ids[static_cast<size_t>(row_index)]
[static_cast<size_t>(col_index)]);
}
AXNode* AXNode::GetTableCellFromAriaCoords(int aria_row_index,
int aria_col_index) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info) {
return nullptr;
}
if (aria_row_index < 1 || aria_row_index > table_info->aria_row_count ||
aria_col_index < 1 || aria_col_index > table_info->aria_col_count) {
return nullptr;
}
for (size_t row = 0; row < table_info->row_count; ++row) {
for (size_t col = 0; col < table_info->col_count; ++col) {
AXNode* node = tree_->GetFromId(table_info->cell_ids[row][col]);
CHECK(node);
std::optional<int> current_aria_row = node->GetTableCellAriaRowIndex();
std::optional<int> current_aria_col = node->GetTableCellAriaColIndex();
if (!current_aria_row || *current_aria_row < aria_row_index) {
break;
} else if (*current_aria_row > aria_row_index) {
return nullptr;
}
if (!current_aria_col || *current_aria_col < aria_col_index) {
continue;
} else if (*current_aria_col > aria_col_index) {
return nullptr;
}
DCHECK(*current_aria_row == aria_row_index &&
*current_aria_col == aria_col_index);
return node;
}
}
return nullptr;
}
std::vector<AXNodeID> AXNode::GetTableColHeaderNodeIds() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::vector<AXNodeID>();
std::vector<AXNodeID> col_header_ids;
for (std::vector<AXNodeID> col_headers_at_index : table_info->col_headers) {
col_header_ids.insert(col_header_ids.end(), col_headers_at_index.begin(),
col_headers_at_index.end());
}
return col_header_ids;
}
std::vector<AXNodeID> AXNode::GetTableColHeaderNodeIds(int col_index) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::vector<AXNodeID>();
if (col_index < 0 || static_cast<size_t>(col_index) >= table_info->col_count)
return std::vector<AXNodeID>();
return std::vector<AXNodeID>(
table_info->col_headers[static_cast<size_t>(col_index)]);
}
std::vector<AXNodeID> AXNode::GetTableRowHeaderNodeIds(int row_index) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::vector<AXNodeID>();
if (row_index < 0 || static_cast<size_t>(row_index) >= table_info->row_count)
return std::vector<AXNodeID>();
return std::vector<AXNodeID>(
table_info->row_headers[static_cast<size_t>(row_index)]);
}
std::vector<AXNodeID> AXNode::GetTableUniqueCellIds() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::vector<AXNodeID>();
return std::vector<AXNodeID>(table_info->unique_cell_ids);
}
const std::vector<raw_ptr<AXNode, VectorExperimental>>*
AXNode::GetExtraMacNodes() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
if (!IsTable() || IsInvisibleOrIgnored()) {
return nullptr;
}
const AXTableInfo* table_info = tree_->GetTableInfo(this);
if (!table_info)
return nullptr;
return &table_info->extra_mac_nodes;
}
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
AXNode* AXNode::GetExtraAnnouncementNode(
ax::mojom::AriaNotificationPriority priority_property) const {
if (!tree_->extra_announcement_nodes()) {
tree_->CreateExtraAnnouncementNodes();
}
switch (priority_property) {
case ax::mojom::AriaNotificationPriority::kHigh:
return &tree_->extra_announcement_nodes()->AssertiveNode();
case ax::mojom::AriaNotificationPriority::kNormal:
return &tree_->extra_announcement_nodes()->PoliteNode();
}
NOTREACHED();
}
#endif
bool AXNode::IsGenerated() const {
bool is_generated_node = id() < 0 && id() > kInitialEmptyDocumentRootNodeID;
#if DCHECK_IS_ON()
#if BUILDFLAG(IS_APPLE)
bool is_extra_mac_node_role =
GetRole() == ax::mojom::Role::kColumn ||
GetRole() == ax::mojom::Role::kTableHeaderContainer;
DCHECK_EQ(is_generated_node, is_extra_mac_node_role);
#elif BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
if (GetParent() && GetParent()->GetManager()) {
DCHECK_EQ(GetParent(), GetManager()->GetRoot());
}
#endif
#endif
return is_generated_node;
}
bool AXNode::IsTableRow() const {
return ui::IsTableRow(GetRole());
}
std::optional<int> AXNode::GetTableRowRowIndex() const {
if (!IsTableRow())
return std::nullopt;
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
const auto& iter = table_info->row_id_to_index.find(id());
if (iter == table_info->row_id_to_index.end())
return std::nullopt;
return static_cast<int>(iter->second);
}
std::vector<AXNodeID> AXNode::GetTableRowNodeIds() const {
std::vector<AXNodeID> row_node_ids;
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return row_node_ids;
for (AXNode* node : table_info->row_nodes)
row_node_ids.push_back(node->id());
return row_node_ids;
}
#if BUILDFLAG(IS_APPLE)
bool AXNode::IsTableColumn() const {
return ui::IsTableColumn(GetRole());
}
std::optional<int> AXNode::GetTableColColIndex() const {
if (!IsTableColumn())
return std::nullopt;
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
int index = 0;
for (const AXNode* node : table_info->extra_mac_nodes) {
if (node == this)
break;
index++;
}
return index;
}
#endif
bool AXNode::IsTableCellOrHeader() const {
return IsCellOrTableHeader(GetRole());
}
std::optional<int> AXNode::GetTableCellIndex() const {
if (!IsTableCellOrHeader())
return std::nullopt;
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
const auto& iter = table_info->cell_id_to_index.find(id());
if (iter != table_info->cell_id_to_index.end())
return static_cast<int>(iter->second);
return std::nullopt;
}
std::optional<int> AXNode::GetTableCellColIndex() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
std::optional<int> index = GetTableCellIndex();
if (!index)
return std::nullopt;
return static_cast<int>(table_info->cell_data_vector[*index].col_index);
}
std::optional<int> AXNode::GetTableCellRowIndex() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
if (IsTableRow()) {
if (const AXNode* first_cell = table_info->GetFirstCellInRow(this)) {
return first_cell->GetTableCellRowIndex();
}
return std::nullopt;
}
std::optional<int> index = GetTableCellIndex();
if (!index)
return std::nullopt;
return static_cast<int>(table_info->cell_data_vector[*index].row_index);
}
std::optional<int> AXNode::GetTableCellColSpan() const {
if (!IsTableCellOrHeader())
return std::nullopt;
int col_span = GetIntAttribute(ax::mojom::IntAttribute::kTableCellColumnSpan);
if (col_span ||
HasIntAttribute(ax::mojom::IntAttribute::kTableCellColumnSpan)) {
return col_span;
}
return 1;
}
std::optional<int> AXNode::GetTableCellRowSpan() const {
if (!IsTableCellOrHeader())
return std::nullopt;
int row_span = GetIntAttribute(ax::mojom::IntAttribute::kTableCellRowSpan);
if (row_span || HasIntAttribute(ax::mojom::IntAttribute::kTableCellRowSpan)) {
return row_span;
}
return 1;
}
std::optional<int> AXNode::GetTableCellAriaColIndex() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
std::optional<int> index = GetTableCellIndex();
if (!index)
return std::nullopt;
int aria_col_index =
static_cast<int>(table_info->cell_data_vector[*index].aria_col_index);
return (aria_col_index > 0) ? std::optional<int>(aria_col_index)
: std::nullopt;
}
std::optional<int> AXNode::GetTableCellAriaRowIndex() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::nullopt;
if (IsTableRow()) {
if (const AXNode* first_cell = table_info->GetFirstCellInRow(this)) {
return first_cell->GetTableCellAriaRowIndex();
}
return std::nullopt;
}
std::optional<int> index = GetTableCellIndex();
if (!index) {
return std::nullopt;
}
int aria_row_index =
static_cast<int>(table_info->cell_data_vector[*index].aria_row_index);
return (aria_row_index > 0) ? std::optional<int>(aria_row_index)
: std::nullopt;
}
std::vector<AXNodeID> AXNode::GetTableCellColHeaderNodeIds() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info || table_info->col_count <= 0)
return std::vector<AXNodeID>();
int col_index = GetTableCellColIndex().value_or(0);
return std::vector<AXNodeID>(table_info->col_headers[col_index]);
}
void AXNode::GetTableCellColHeaders(std::vector<AXNode*>* col_headers) const {
DCHECK(col_headers);
std::vector<AXNodeID> col_header_ids = GetTableCellColHeaderNodeIds();
IdVectorToNodeVector(col_header_ids, col_headers);
}
std::vector<AXNodeID> AXNode::GetTableCellRowHeaderNodeIds() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info || table_info->row_count <= 0)
return std::vector<AXNodeID>();
int row_index = GetTableCellRowIndex().value_or(0);
return std::vector<AXNodeID>(table_info->row_headers[row_index]);
}
void AXNode::GetTableCellRowHeaders(std::vector<AXNode*>* row_headers) const {
DCHECK(row_headers);
std::vector<AXNodeID> row_header_ids = GetTableCellRowHeaderNodeIds();
IdVectorToNodeVector(row_header_ids, row_headers);
}
bool AXNode::IsCellOrHeaderOfAriaGrid() const {
if (!IsTableCellOrHeader())
return false;
const AXNode* node = this;
while (node && !node->IsTable())
node = node->GetParent();
if (!node)
return false;
return node->GetRole() == ax::mojom::Role::kGrid ||
node->GetRole() == ax::mojom::Role::kTreeGrid;
}
AXTableInfo* AXNode::GetAncestorTableInfo() const {
const AXNode* node = this;
while (node && !node->IsTable()) {
node = node->GetUnignoredParent();
}
if (!node || node->IsInvisibleOrIgnored()) {
return nullptr;
}
return tree_->GetTableInfo(node);
}
void AXNode::IdVectorToNodeVector(const std::vector<AXNodeID>& ids,
std::vector<AXNode*>* nodes) const {
for (AXNodeID id : ids) {
AXNode* node = tree_->GetFromId(id);
if (node)
nodes->push_back(node);
}
}
std::optional<int> AXNode::GetHierarchicalLevel() const {
int hierarchical_level =
GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel);
if (hierarchical_level > 0)
return hierarchical_level;
return std::nullopt;
}
bool AXNode::IsOrderedSetItem() const {
if (IsRowInTreeGrid(GetOrderedSet()))
return true;
return ui::IsItemLike(GetRole());
}
bool AXNode::IsOrderedSet() const {
if (IsRowGroupInTreeGrid())
return true;
return ui::IsSetLike(GetRole());
}
std::optional<int> AXNode::GetPosInSet() const {
return tree_->GetPosInSet(*this);
}
std::optional<int> AXNode::GetSetSize() const {
return tree_->GetSetSize(*this);
}
bool AXNode::SetRoleMatchesItemRole(const AXNode* ordered_set) const {
ax::mojom::Role item_role = GetRole();
if (IsRowInTreeGrid(ordered_set) ||
item_role == ax::mojom::Role::kDisclosureTriangle ||
item_role == ax::mojom::Role::kDisclosureTriangleGrouped) {
return true;
}
switch (ordered_set->GetRole()) {
case ax::mojom::Role::kFeed:
return item_role == ax::mojom::Role::kArticle;
case ax::mojom::Role::kList:
return item_role == ax::mojom::Role::kListItem;
case ax::mojom::Role::kGroup:
return item_role == ax::mojom::Role::kComment ||
item_role == ax::mojom::Role::kListItem ||
item_role == ax::mojom::Role::kMenuItem ||
item_role == ax::mojom::Role::kMenuItemRadio ||
item_role == ax::mojom::Role::kMenuItemCheckBox ||
item_role == ax::mojom::Role::kListBoxOption ||
item_role == ax::mojom::Role::kTreeItem;
case ax::mojom::Role::kMenu:
return item_role == ax::mojom::Role::kMenuItem ||
item_role == ax::mojom::Role::kMenuItemRadio ||
item_role == ax::mojom::Role::kMenuItemCheckBox;
case ax::mojom::Role::kMenuBar:
return item_role == ax::mojom::Role::kMenuItem ||
item_role == ax::mojom::Role::kMenuItemRadio ||
item_role == ax::mojom::Role::kMenuItemCheckBox;
case ax::mojom::Role::kTabList:
return item_role == ax::mojom::Role::kTab;
case ax::mojom::Role::kTree:
case ax::mojom::Role::kTreeItem:
return item_role == ax::mojom::Role::kTreeItem;
case ax::mojom::Role::kListBox:
return item_role == ax::mojom::Role::kListBoxOption;
case ax::mojom::Role::kMenuListPopup:
return item_role == ax::mojom::Role::kMenuListOption ||
item_role == ax::mojom::Role::kMenuItem ||
item_role == ax::mojom::Role::kMenuItemRadio ||
item_role == ax::mojom::Role::kMenuItemCheckBox;
case ax::mojom::Role::kRadioGroup:
return item_role == ax::mojom::Role::kRadioButton;
case ax::mojom::Role::kDescriptionList:
return item_role == ax::mojom::Role::kTerm;
case ax::mojom::Role::kComboBoxSelect:
return item_role == ax::mojom::Role::kMenuListPopup;
default:
return false;
}
}
bool AXNode::IsIgnoredContainerForOrderedSet() const {
return IsIgnored() || IsEmbeddedGroup() ||
GetRole() == ax::mojom::Role::kCell ||
GetRole() == ax::mojom::Role::kDetails ||
GetRole() == ax::mojom::Role::kLabelText ||
GetRole() == ax::mojom::Role::kLayoutTableCell ||
GetRole() == ax::mojom::Role::kLayoutTableRow ||
GetRole() == ax::mojom::Role::kListItem ||
GetRole() == ax::mojom::Role::kGenericContainer ||
GetRole() == ax::mojom::Role::kGridCell ||
GetRole() == ax::mojom::Role::kRow ||
GetRole() == ax::mojom::Role::kScrollView ||
GetRole() == ax::mojom::Role::kUnknown;
}
bool AXNode::IsRowInTreeGrid(const AXNode* ordered_set) const {
if (GetRole() != ax::mojom::Role::kRow || !ordered_set || !IsFocusable())
return false;
if (ordered_set->GetRole() == ax::mojom::Role::kTreeGrid)
return true;
return ordered_set->IsRowGroupInTreeGrid();
}
bool AXNode::IsRowGroupInTreeGrid() const {
if (GetRole() != ax::mojom::Role::kRowGroup)
return false;
AXNode* ordered_set = GetOrderedSet();
return ordered_set && ordered_set->GetRole() == ax::mojom::Role::kTreeGrid;
}
int AXNode::UpdateUnignoredCachedValuesRecursive(int startIndex) {
int count = 0;
for (AXNode* child : children()) {
if (child->IsIgnored()) {
child->unignored_index_in_parent_ = 0;
count += child->UpdateUnignoredCachedValuesRecursive(startIndex + count);
} else {
child->unignored_index_in_parent_ = startIndex + count++;
}
}
unignored_child_count_ = count;
return count;
}
AXNode* AXNode::GetOrderedSet() const {
AXNode* result = GetParent();
while (result && result->IsIgnoredContainerForOrderedSet()) {
result = result->GetParent();
}
return result;
}
bool AXNode::IsReadOnlySupported() const {
if (IsCellOrHeaderOfAriaGrid())
return true;
return ui::IsReadOnlySupported(GetRole());
}
bool AXNode::IsReadOnlyOrDisabled() const {
switch (data().GetRestriction()) {
case ax::mojom::Restriction::kReadOnly:
case ax::mojom::Restriction::kDisabled:
return true;
case ax::mojom::Restriction::kNone: {
if (HasState(ax::mojom::State::kEditable) ||
HasState(ax::mojom::State::kRichlyEditable)) {
return false;
}
if (ShouldHaveReadonlyStateByDefault(GetRole()))
return true;
return !IsReadOnlySupported();
}
}
}
bool AXNode::IsView() const {
const AXTreeManager* manager = GetManager();
if (!manager) {
return false;
}
return manager->IsView();
}
AXNode* AXNode::ComputeLastUnignoredChildRecursive() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
if (children().empty())
return nullptr;
for (int i = static_cast<int>(children().size()) - 1; i >= 0; --i) {
AXNode* child = children_[i];
if (!child->IsIgnored())
return child;
AXNode* descendant = child->ComputeLastUnignoredChildRecursive();
if (descendant)
return descendant;
}
return nullptr;
}
AXNode* AXNode::ComputeFirstUnignoredChildRecursive() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
for (size_t i = 0; i < children().size(); i++) {
AXNode* child = children_[i];
if (!child->IsIgnored())
return child;
AXNode* descendant = child->ComputeFirstUnignoredChildRecursive();
if (descendant)
return descendant;
}
return nullptr;
}
std::string AXNode::GetTextForRangeValue() const {
DCHECK(data().IsRangeValueSupported());
std::string range_value =
GetStringAttribute(ax::mojom::StringAttribute::kValue);
if (range_value.empty()) {
float numeric_value =
GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange);
if (numeric_value != AXNode::kDefaultFloatValue ||
HasFloatAttribute(ax::mojom::FloatAttribute::kValueForRange)) {
return base::StringPrintf("%g", numeric_value);
}
}
return range_value;
}
std::string AXNode::GetValueForColorWell() const {
DCHECK_EQ(GetRole(), ax::mojom::Role::kColorWell);
unsigned int color = static_cast<unsigned int>(
GetIntAttribute(ax::mojom::IntAttribute::kColorValue));
unsigned int red = SkColorGetR(color);
unsigned int green = SkColorGetG(color);
unsigned int blue = SkColorGetB(color);
return base::StringPrintf("%d%% red %d%% green %d%% blue", red * 100 / 255,
green * 100 / 255, blue * 100 / 255);
}
bool AXNode::IsIgnored() const {
if(GetRole() == ax::mojom::Role::kRowGroup) {
return true;
}
return AXTree::ComputeNodeIsIgnored(&tree_->data(), data());
}
bool AXNode::IsIgnoredForTextNavigation() const {
if (GetRole() == ax::mojom::Role::kSplitter)
return true;
if (GetRole() == ax::mojom::Role::kGenericContainer &&
!GetUnignoredChildCount() && !HasState(ax::mojom::State::kEditable)) {
return true;
}
return false;
}
bool AXNode::IsInvisibleOrIgnored() const {
return id() != tree_->data().focus_id && (IsIgnored() || data_.IsInvisible());
}
bool AXNode::IsChildOfLeaf() const {
for (const AXNode* ancestor = GetUnignoredParent(); ancestor;
ancestor = ancestor->GetUnignoredParent()) {
if (ancestor->IsLeaf()) {
return true;
}
}
return false;
}
bool AXNode::IsEmptyLeaf() const {
if (!IsLeaf())
return false;
if (GetUnignoredChildCountCrossingTreeBoundary())
return !GetTextContentLengthUTF8();
return IsIgnored() || !GetTextContentLengthUTF8();
}
bool AXNode::IsLeaf() const {
if (!GetChildCountCrossingTreeBoundary())
return true;
if (IsIgnored())
return false;
int child_count = GetUnignoredChildCountCrossingTreeBoundary();
if (!child_count)
return true;
#if BUILDFLAG(IS_WIN)
if (IsCollapsedMenuListSelect())
return true;
#endif
if (data().IsAtomicTextField() || IsText())
return true;
if (data().IsNonAtomicTextField())
return false;
switch (GetRole()) {
case ax::mojom::Role::kButton:
return false;
case ax::mojom::Role::kImage: {
const std::string role =
GetStringAttribute(ax::mojom::StringAttribute::kRole);
return role == "img" || role == "image";
}
case ax::mojom::Role::kDocCover:
case ax::mojom::Role::kGraphicsSymbol:
case ax::mojom::Role::kMeter:
case ax::mojom::Role::kScrollBar:
case ax::mojom::Role::kSpinButton:
case ax::mojom::Role::kSlider:
case ax::mojom::Role::kSplitter:
case ax::mojom::Role::kProgressIndicator:
return true;
case ax::mojom::Role::kCheckBox:
case ax::mojom::Role::kListBoxOption:
case ax::mojom::Role::kMath:
case ax::mojom::Role::kMenuListOption:
case ax::mojom::Role::kMenuItem:
case ax::mojom::Role::kMenuItemCheckBox:
case ax::mojom::Role::kMenuItemRadio:
case ax::mojom::Role::kPopUpButton:
case ax::mojom::Role::kToggleButton:
case ax::mojom::Role::kRadioButton:
case ax::mojom::Role::kSwitch:
case ax::mojom::Role::kTab: {
if (child_count > 2 || HasState(ax::mojom::State::kEditable))
return false;
const AXNode* child1 = GetFirstUnignoredChildCrossingTreeBoundary();
if (!child1 || !child1->IsText())
return false;
const AXNode* child2 = child1->GetNextSibling();
return !child2 || child2->IsText();
}
default:
return false;
}
}
bool AXNode::IsFocusable() const {
return HasState(ax::mojom::State::kFocusable) ||
IsLikelyARIAActiveDescendant();
}
bool AXNode::IsLikelyARIAActiveDescendant() const {
if (!ui::IsLikelyActiveDescendantRole(GetRole()))
return false;
if (!HasStringAttribute(ax::mojom::StringAttribute::kRole) &&
!ui::IsCellOrTableHeader(GetRole())) {
return false;
}
if (IsInvisibleOrIgnored() ||
GetIntAttribute(ax::mojom::IntAttribute::kRestriction) ==
static_cast<int>(ax::mojom::Restriction::kDisabled)) {
return false;
}
if (!HasStringAttribute(ax::mojom::StringAttribute::kHtmlId)) {
return false;
}
for (AXNode* ancestor_node = GetUnignoredParent(); ancestor_node;
ancestor_node = ancestor_node->GetUnignoredParent()) {
if (ancestor_node->HasIntAttribute(
ax::mojom::IntAttribute::kActivedescendantId)) {
return true;
}
if (ui::IsComboBoxContainer(ancestor_node->GetRole())) {
std::set<AXNodeID> nodes_that_control_this_list =
tree()->GetReverseRelations(ax::mojom::IntListAttribute::kControlsIds,
ancestor_node->id());
for (AXNodeID id : nodes_that_control_this_list) {
if (AXNode* node = tree()->GetFromId(id)) {
if (ui::IsTextField(node->GetRole()) ||
ui::IsComboBox(node->GetRole())) {
return node->HasIntAttribute(
ax::mojom::IntAttribute::kActivedescendantId);
}
}
}
}
}
return false;
}
bool AXNode::IsInListMarker() const {
if (GetRole() == ax::mojom::Role::kListMarker)
return true;
if (!IsText())
return false;
AXNode* parent_node = GetUnignoredParent();
if (!parent_node)
return false;
if (parent_node->GetRole() == ax::mojom::Role::kListMarker)
return true;
AXNode* grandparent_node = parent_node->GetUnignoredParent();
return grandparent_node &&
grandparent_node->GetRole() == ax::mojom::Role::kListMarker;
}
bool AXNode::IsCollapsedMenuListSelect() const {
return HasState(ax::mojom::State::kCollapsed) &&
GetRole() == ax::mojom::Role::kComboBoxSelect;
}
bool AXNode::IsRootWebAreaForPresentationalIframe() const {
if (!ui::IsPlatformDocument(GetRole()))
return false;
const AXNode* parent = GetUnignoredParentCrossingTreeBoundary();
if (!parent)
return false;
return parent->GetRole() == ax::mojom::Role::kIframePresentational;
}
AXNode* AXNode::GetCollapsedMenuListSelectAncestor() const {
AXNode* node = GetOrderedSet();
if (!node)
return nullptr;
if (node->GetRole() != ax::mojom::Role::kComboBoxSelect) {
node = node->GetParent();
if (!node)
return nullptr;
}
return node->IsCollapsedMenuListSelect() ? node : nullptr;
}
bool AXNode::IsEmbeddedGroup() const {
if (GetRole() != ax::mojom::Role::kGroup || !GetUnignoredParent()) {
return false;
}
return ui::IsSetLike(GetUnignoredParent()->GetRole());
}
AXNode* AXNode::GetLowestPlatformAncestor() const {
AXNode* current_node = const_cast<AXNode*>(this);
AXNode* lowest_unignored_node = current_node;
for (; lowest_unignored_node && lowest_unignored_node->IsIgnored();
lowest_unignored_node = lowest_unignored_node->GetParent()) {
}
AXNode* highest_leaf_node = lowest_unignored_node;
for (AXNode* ancestor_node = lowest_unignored_node; ancestor_node;
ancestor_node = ancestor_node->GetUnignoredParent()) {
if (ancestor_node->IsLeaf())
highest_leaf_node = ancestor_node;
}
if (highest_leaf_node)
return highest_leaf_node;
if (lowest_unignored_node)
return lowest_unignored_node;
return current_node;
}
AXNode* AXNode::GetTextFieldAncestor() const {
for (AXNode* ancestor = const_cast<AXNode*>(this); ancestor;
ancestor = ancestor->GetUnignoredParent()) {
if (ancestor->data().IsTextField())
return ancestor;
}
return nullptr;
}
AXNode* AXNode::GetTextFieldInnerEditorElement() const {
if (!data().IsAtomicTextField() || !GetUnignoredChildCount())
return nullptr;
AXNode* text_container = GetDeepestFirstUnignoredDescendant();
DCHECK(text_container) << "Unable to retrieve deepest unignored child on\n"
<< *this;
if (text_container->GetRole() == ax::mojom::Role::kInlineTextBox)
text_container = text_container->GetUnignoredParent();
if (text_container->GetRole() == ax::mojom::Role::kStaticText ||
text_container->GetRole() == ax::mojom::Role::kLineBreak) {
text_container = text_container->GetUnignoredParent();
}
DCHECK(text_container) << "Unexpected unignored parent while computing text "
"field inner editor element on\n"
<< *this;
if (text_container->GetRole() == ax::mojom::Role::kGenericContainer)
return text_container;
return nullptr;
}
AXNode* AXNode::GetSelectionContainer() const {
if (!IsSelectSupported(GetRole()))
return nullptr;
if (IsInvisibleOrIgnored() ||
GetIntAttribute(ax::mojom::IntAttribute::kRestriction) ==
static_cast<int>(ax::mojom::Restriction::kDisabled)) {
return nullptr;
}
for (AXNode* ancestor = const_cast<AXNode*>(this); ancestor;
ancestor = ancestor->GetUnignoredParent()) {
if (ui::IsContainerWithSelectableChildren(ancestor->GetRole()))
return ancestor;
}
return nullptr;
}
AXNode* AXNode::GetTableAncestor() const {
for (AXNode* ancestor = const_cast<AXNode*>(this); ancestor;
ancestor = ancestor->GetUnignoredParent()) {
if (ancestor->IsTable())
return ancestor;
}
return nullptr;
}
bool AXNode::IsDescendantOfAtomicTextField() const {
AXNode* text_field_node = GetTextFieldAncestor();
return text_field_node && text_field_node->data().IsAtomicTextField();
}
}