// Copyright 2013 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/ax_node.h" #include #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/ax_computed_node_data.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 { // Definition of static class members. 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) { data_.id = id; } AXNode::~AXNode() = default; AXNodeData&& AXNode::TakeData() { return std::move(data_); } const std::vector& AXNode::GetAllChildren() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); return children_; } size_t AXNode::GetChildCount() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); return children_.size(); } 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 { // TODO(nektar): Should DCHECK that this node is not ignored. DCHECK(!tree_->GetTreeUpdateInProgressState()); return unignored_child_count_; } size_t AXNode::GetUnignoredChildCountCrossingTreeBoundary() const { // TODO(nektar): Should DCHECK that this node is not ignored. 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; // A child tree is never ignored. } 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 { // TODO(nektar): Should DCHECK that this node is not ignored. DCHECK(!tree_->GetTreeUpdateInProgressState()); for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) { if (index == 0) return it.get(); --index; } return nullptr; } AXNode* AXNode::GetUnignoredChildAtIndexCrossingTreeBoundary( size_t index) const { // TODO(nektar): Should DCHECK that this node is not ignored. 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."; // A child tree is never ignored. 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->GetParentNodeFromParentTreeAsAXNode(); 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->GetParentNodeFromParentTreeAsAXNode(); } return unignored_parent; } base::stack AXNode::GetAncestorsCrossingTreeBoundary() const { base::stack ancestors; AXNode* ancestor = const_cast(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::GetDeepestFirstChild() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetChildCount()) return nullptr; AXNode* deepest_child = GetFirstChild(); DCHECK(deepest_child); while (deepest_child->GetChildCount()) deepest_child = deepest_child->GetFirstChild(); return deepest_child; } AXNode* AXNode::GetDeepestFirstChildCrossingTreeBoundary() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetChildCountCrossingTreeBoundary()) return nullptr; AXNode* deepest_child = GetFirstChildCrossingTreeBoundary(); DCHECK(deepest_child); while (deepest_child->GetChildCountCrossingTreeBoundary()) deepest_child = deepest_child->GetFirstChildCrossingTreeBoundary(); return deepest_child; } AXNode* AXNode::GetDeepestFirstUnignoredChild() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetUnignoredChildCount()) return nullptr; AXNode* deepest_child = GetFirstUnignoredChild(); DCHECK(deepest_child); while (deepest_child->GetUnignoredChildCount()) deepest_child = deepest_child->GetFirstUnignoredChild(); return deepest_child; } AXNode* AXNode::GetDeepestFirstUnignoredChildCrossingTreeBoundary() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetUnignoredChildCountCrossingTreeBoundary()) return nullptr; AXNode* deepest_child = GetFirstUnignoredChildCrossingTreeBoundary(); DCHECK(deepest_child); while (deepest_child->GetUnignoredChildCountCrossingTreeBoundary()) deepest_child = deepest_child->GetFirstUnignoredChildCrossingTreeBoundary(); return deepest_child; } AXNode* AXNode::GetDeepestLastChild() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetChildCount()) return nullptr; AXNode* deepest_child = GetLastChild(); DCHECK(deepest_child); while (deepest_child->GetChildCount()) deepest_child = deepest_child->GetLastChild(); return deepest_child; } AXNode* AXNode::GetDeepestLastChildCrossingTreeBoundary() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetChildCountCrossingTreeBoundary()) return nullptr; AXNode* deepest_child = GetLastChildCrossingTreeBoundary(); DCHECK(deepest_child); while (deepest_child->GetChildCountCrossingTreeBoundary()) deepest_child = deepest_child->GetLastChildCrossingTreeBoundary(); return deepest_child; } AXNode* AXNode::GetDeepestLastUnignoredChild() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetUnignoredChildCount()) return nullptr; AXNode* deepest_child = GetLastUnignoredChild(); DCHECK(deepest_child); while (deepest_child->GetUnignoredChildCount()) deepest_child = deepest_child->GetLastUnignoredChild(); return deepest_child; } AXNode* AXNode::GetDeepestLastUnignoredChildCrossingTreeBoundary() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetUnignoredChildCountCrossingTreeBoundary()) return nullptr; AXNode* deepest_child = GetLastUnignoredChildCrossingTreeBoundary(); DCHECK(deepest_child); while (deepest_child->GetUnignoredChildCountCrossingTreeBoundary()) deepest_child = deepest_child->GetLastUnignoredChildCrossingTreeBoundary(); return deepest_child; } 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); } // Search for the next sibling of this node, skipping over any ignored nodes // encountered. // // In our search: // If we find an ignored sibling, we consider its children as our siblings. // If we run out of siblings, we consider an ignored parent's siblings as our // own siblings. // // Note: this behaviour of 'skipping over' an ignored node makes this subtly // different to finding the next (direct) sibling which is unignored. // // Consider a tree, where (i) marks a node as ignored: // // 1 // ├── 2 // ├── 3(i) // │ └── 5 // └── 4 // // The next sibling of node 2 is node 3, which is ignored. // The next unignored sibling of node 2 could be either: // 1) node 4 - next unignored sibling in the literal tree, or // 2) node 5 - next unignored sibling in the logical document. // // There is no next sibling of node 5. // The next unignored sibling of node 5 could be either: // 1) null - no next sibling in the literal tree, or // 2) node 4 - next unignored sibling in the logical document. // // In both cases, this method implements approach (2). // // TODO(chrishall): Can we remove this non-reflexive case by forbidding // GetNextUnignoredSibling calls on an ignored started node? // Note: this means that Next/Previous-UnignoredSibling are not reflexive if // either of the nodes in question are ignored. From above we get an example: // NextUnignoredSibling(3) is 4, but // PreviousUnignoredSibling(4) is 5. // // The view of unignored siblings for node 3 includes both node 2 and node 4: // 2 <-- [3(i)] --> 4 // // Whereas nodes 2, 5, and 4 do not consider node 3 to be an unignored sibling: // null <-- [2] --> 5 // 2 <-- [5] --> 4 // 5 <-- [4] --> null AXNode* AXNode::GetNextUnignoredSibling() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXNode* current = this; // If there are children of the |current| node still to consider. bool considerChildren = false; while (current) { // A |candidate| sibling to consider. // If it is unignored then we have found our result. // Otherwise promote it to |current| and consider its children. 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; // Look through the ignored candidate node to consider their children as // though they were siblings. considerChildren = true; } else { // Continue our search through a parent iff they are ignored. // // If |current| has an ignored parent, then we consider the parent's // siblings as though they were siblings of |current|. // // Given a tree: // 1 // ├── 2(?) // │ └── [4] // └── 3 // // Node 4's view of siblings: // literal tree: null <-- [4] --> null // // If node 2 is not ignored, then node 4's view doesn't change, and we // have no more nodes to consider: // unignored tree: null <-- [4] --> null // // If instead node 2 is ignored, then node 4's view of siblings grows to // include node 3, and we have more nodes to consider: // unignored tree: null <-- [4] --> 3 current = current->GetParent(); if (!current || !current->IsIgnored()) return nullptr; // We have already considered all relevant descendants of |current|. 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); } // Search for the previous sibling of this node, skipping over any ignored nodes // encountered. // // In our search for a sibling: // If we find an ignored sibling, we may consider its children as siblings. // If we run out of siblings, we may consider an ignored parent's siblings as // our own. // // See the documentation for |GetNextUnignoredSibling| for more details. AXNode* AXNode::GetPreviousUnignoredSibling() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXNode* current = this; // If there are children of the |current| node still to consider. bool considerChildren = false; while (current) { // A |candidate| sibling to consider. // If it is unignored then we have found our result. // Otherwise promote it to |current| and consider its children. 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; // Look through the ignored candidate node to consider their children as // though they were siblings. considerChildren = true; } else { // Continue our search through a parent iff they are ignored. // // If |current| has an ignored parent, then we consider the parent's // siblings as though they were siblings of |current|. // // Given a tree: // 1 // ├── 2 // └── 3(?) // └── [4] // // Node 4's view of siblings: // literal tree: null <-- [4] --> null // // If node 3 is not ignored, then node 4's view doesn't change, and we // have no more nodes to consider: // unignored tree: null <-- [4] --> null // // If instead node 3 is ignored, then node 4's view of siblings grows to // include node 2, and we have more nodes to consider: // unignored tree: 2 <-- [4] --> null current = current->GetParent(); if (!current || !current->IsIgnored()) return nullptr; // We have already considered all relevant descendants of |current|. 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->GetDeepestLastUnignoredChild(); return sibling; } AXNode::AllChildIterator AXNode::AllChildrenBegin() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); return AllChildIterator(this, GetFirstChild()); } AXNode::AllChildIterator AXNode::AllChildrenEnd() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); return AllChildIterator(this, nullptr); } 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 { // TODO(nektar): Cache the `IsChildOfLeaf` state in `AXComputedNodeData`. return !IsChildOfLeaf(); } AXNode* AXNode::GetLowestCommonAncestor(const AXNode& other) { if (this == &other) return this; AXNode* common_ancestor = nullptr; base::stack our_ancestors = GetAncestorsCrossingTreeBoundary(); base::stack other_ancestors = other.GetAncestorsCrossingTreeBoundary(); 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; } absl::optional AXNode::CompareTo(const AXNode& other) const { if (this == &other) return 0; AXNode* common_ancestor = nullptr; base::stack our_ancestors = GetAncestorsCrossingTreeBoundary(); base::stack other_ancestors = other.GetAncestorsCrossingTreeBoundary(); 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 absl::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."; return absl::nullopt; } 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 { // Regular list markers only expose their alternative text, but do not expose // their descendants; and the descendants should be ignored. This is because // the alternative text depends on the counter style and can be different from // the actual (visual) marker text, and hence, inconsistent with the // descendants. We treat a list marker as non-text only if it still has // non-ignored descendants, which happens only when: // - The list marker itself is ignored but the descendants are not // - Or the list marker contains images if (GetRole() == ax::mojom::Role::kListMarker) return !GetUnignoredChildCount(); return ui::IsText(GetRole()); } bool AXNode::IsLineBreak() const { // The last condition captures inline text nodes whose only content is an '\n' // character. 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(*transform); } else { data_.relative_bounds.transform.reset(); } } 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* 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(); // If the color has some transparency, keep blending with background // colors until we get an opaque color or reach the root. 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; // A selection or the caret will be visible in a focused text field (including // a content editable). const AXNode* text_field = GetTextFieldAncestor(); if (text_field) return true; // The selection will be visible in non-editable content only if it is not // collapsed. 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(); // "selection.anchor_offset" and "selection.focus_ofset" might need to be // adjusted if the anchor or the focus nodes include ignored children. // // TODO(nektar): Move this logic into its own "AXSelection" class and cache // the result for faster reuse. const AXNode* anchor = tree()->GetFromId(selection.anchor_object_id); if (anchor && !anchor->IsLeaf()) { DCHECK_GE(selection.anchor_offset, 0); if (static_cast(selection.anchor_offset) < anchor->GetChildCount()) { const AXNode* anchor_child = anchor->GetChildAtIndex(selection.anchor_offset); DCHECK(anchor_child); selection.anchor_offset = static_cast(anchor_child->GetUnignoredIndexInParent()); } else { selection.anchor_offset = static_cast(anchor->GetUnignoredChildCount()); } } const AXNode* focus = tree()->GetFromId(selection.focus_object_id); if (focus && !focus->IsLeaf()) { DCHECK_GE(selection.focus_offset, 0); if (static_cast(selection.focus_offset) < focus->GetChildCount()) { const AXNode* focus_child = focus->GetChildAtIndex(selection.focus_offset); DCHECK(focus_child); selection.focus_offset = static_cast(focus_child->GetUnignoredIndexInParent()); } else { selection.focus_offset = static_cast(focus->GetUnignoredChildCount()); } } return selection; } bool AXNode::HasStringAttribute(ax::mojom::StringAttribute attribute) const { return GetComputedNodeData().HasOrCanComputeAttribute(attribute); } const std::string& AXNode::GetStringAttribute( ax::mojom::StringAttribute attribute) const { return GetComputedNodeData().GetOrComputeAttributeUTF8(attribute); } bool AXNode::GetStringAttribute(ax::mojom::StringAttribute attribute, std::string* value) const { if (GetComputedNodeData().HasOrCanComputeAttribute(attribute)) { *value = GetComputedNodeData().GetOrComputeAttributeUTF8(attribute); return true; } return false; } std::u16string AXNode::GetString16Attribute( ax::mojom::StringAttribute attribute) const { return GetComputedNodeData().GetOrComputeAttributeUTF16(attribute); } bool AXNode::GetString16Attribute(ax::mojom::StringAttribute attribute, std::u16string* value) const { if (GetComputedNodeData().HasOrCanComputeAttribute(attribute)) { *value = GetComputedNodeData().GetOrComputeAttributeUTF16(attribute); return true; } return false; } 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 { return GetComputedNodeData().HasOrCanComputeAttribute(attribute); } const std::vector& AXNode::GetIntListAttribute( ax::mojom::IntListAttribute attribute) const { return GetComputedNodeData().GetOrComputeAttribute(attribute); } bool AXNode::GetIntListAttribute(ax::mojom::IntListAttribute attribute, std::vector* value) const { if (GetComputedNodeData().HasOrCanComputeAttribute(attribute)) { *value = GetComputedNodeData().GetOrComputeAttribute(attribute); return true; } return false; } AXLanguageInfo* AXNode::GetLanguageInfo() const { return language_info_.get(); } void AXNode::SetLanguageInfo(std::unique_ptr 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(*this); return *computed_node_data_; } void AXNode::ClearComputedNodeData() { computed_node_data_.reset(); } const std::string& AXNode::GetNameUTF8() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXNode* node = this; if (GetRole() == ax::mojom::Role::kPortal && GetNameFrom() == ax::mojom::NameFrom::kNone) { const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this); if (child_tree_manager) node = child_tree_manager->GetRoot(); } return node->GetStringAttribute(ax::mojom::StringAttribute::kName); } std::u16string AXNode::GetNameUTF16() const { // Storing a copy of the name in UTF16 would probably not be helpful because // it could potentially double the memory usage of AXTree. return base::UTF8ToUTF16(GetNameUTF8()); } const std::u16string& AXNode::GetHypertext() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); // TODO(nektar): Introduce proper caching of hypertext via // `AXHypertext::needs_update`. hypertext_ = AXHypertext(); // Hypertext is not exposed for descendants of leaf nodes. For such nodes, // their text content is equivalent to their hypertext. Otherwise, we would // never be able to compute equivalent ancestor positions in atomic text // fields given an AXPosition on an inline text box descendant, because there // is often an ignored generic container between the text descendants and the // text field node. // // For example, look at the following accessibility tree and the text // positions indicated using "<>" symbols in the text content of every node, // and then imagine what would happen if the generic container was represented // by an "embedded object replacement character" in the text of its text field // parent. // ++kTextField "Hell" IsLeaf=true // ++++kGenericContainer "Hell" ignored IsChildOfLeaf=true // ++++++kStaticText "Hell" IsChildOfLeaf=true // ++++++++kInlineTextBox "Hell" IsChildOfLeaf=true if (IsLeaf() || IsChildOfLeaf()) { hypertext_.hypertext = GetTextContentUTF16(); } else { // Construct the hypertext for this node, which contains the concatenation // of the text content of this node's textual children, and an "object // replacement character" for all the other children. // // Note that the word "hypertext" comes from the IAccessible2 Standard and // has nothing to do with HTML. static const base::NoDestructor embedded_character_str( AXNode::kEmbeddedObjectCharacterUTF16); auto first = UnignoredChildrenCrossingTreeBoundaryBegin(); for (auto iter = first; iter != UnignoredChildrenCrossingTreeBoundaryEnd(); ++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 (iter->IsText()) { hypertext_.hypertext += iter->GetTextContentUTF16(); } else { int character_offset = static_cast(hypertext_.hypertext.size()); auto inserted = hypertext_.hypertext_offset_to_hyperlink_child_index.emplace( character_offset, static_cast(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; } void AXNode::SetNeedsToUpdateHypertext() { old_hypertext_ = hypertext_; hypertext_.needs_update = true; // TODO(nektar): Introduce proper caching of hypertext via // `AXHypertext::needs_update`. GetHypertext(); // Forces `hypertext_` to immediately update. } const std::map& AXNode::GetHypertextOffsetToHyperlinkChildIndex() const { // TODO(nektar): Introduce proper caching of hypertext via // `AXHypertext::needs_update`. GetHypertext(); // Update `hypertext_` if not up-to-date. return hypertext_.hypertext_offset_to_hyperlink_child_index; } const AXHypertext& AXNode::GetOldHypertext() const { // TODO(nektar): Introduce proper caching of hypertext via // `AXHypertext::needs_update`. GetHypertext(); // Update `hypertext_` if not up-to-date. return old_hypertext_; } 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::GetTextContentRangeBoundsUTF8(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; // Since we DCHECK that `start_offset` <= `end_offset`, there is no need to // check whether `start_offset` is also in range. if (end_offset > GetTextContentLengthUTF8()) return gfx::RectF(); // TODO(nektar): Update this to use // "base/strings/utf_offset_string_conversions.h" which provides caching of // offsets. std::u16string out_trancated_string_utf16; if (!base::UTF8ToUTF16(GetTextContentUTF8().data(), base::checked_cast(start_offset), &out_trancated_string_utf16)) { return gfx::RectF(); } start_offset = base::checked_cast(out_trancated_string_utf16.length()); if (!base::UTF8ToUTF16(GetTextContentUTF8().data(), base::checked_cast(end_offset), &out_trancated_string_utf16)) { return gfx::RectF(); } end_offset = base::checked_cast(out_trancated_string_utf16.length()); return GetTextContentRangeBoundsUTF16(start_offset, end_offset); } 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; // Since we DCHECK that `start_offset` <= `end_offset`, there is no need to // check whether `start_offset` is also in range. if (end_offset > GetTextContentLengthUTF16()) return gfx::RectF(); const std::vector& character_offsets = GetIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets); int character_offsets_length = base::checked_cast(character_offsets.size()); // Charactger offsets are always based on the UTF-16 representation of the // text. if (character_offsets_length < GetTextContentLengthUTF16()) { // Blink might not return pixel offsets for all characters. Clamp the // character range to be within the number of provided pixels. Note that the // first character always starts at pixel 0, so an offset for that character // is not provided. // // TODO(accessibility): We need to fix this bug in Blink. start_offset = std::min(start_offset, character_offsets_length); end_offset = std::min(end_offset, character_offsets_length); } // TODO(nektar): Remove all this code and fix up the character offsets vector // itself. int start_pixel_offset = start_offset > 0 ? character_offsets[base::checked_cast(start_offset - 1)] : 0; int end_pixel_offset = end_offset > 0 ? character_offsets[base::checked_cast(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( 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()); // Walk up tree considering both detected and author declared languages. for (const AXNode* cur = this; cur; cur = cur->GetParent()) { // If language detection has assigned a language then we prefer that. const AXLanguageInfo* lang_info = cur->GetLanguageInfo(); if (lang_info && !lang_info->language.empty()) return lang_info->language; // If the page author has declared a language attribute we fallback to that. 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()) { // Returns the value of a text field. If necessary, computes the value from // the field's internal representation in the accessibility tree, in order // to minimize cross-process communication between the renderer and the // browser processes. 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) { return stream << node.data().ToString(); } bool AXNode::IsTable() const { return IsTableLike(GetRole()); } absl::optional AXNode::GetTableColCount() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; return static_cast(table_info->col_count); } absl::optional AXNode::GetTableRowCount() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; return static_cast(table_info->row_count); } absl::optional AXNode::GetTableAriaColCount() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; return absl::make_optional(table_info->aria_col_count); } absl::optional AXNode::GetTableAriaRowCount() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; return absl::make_optional(table_info->aria_row_count); } absl::optional AXNode::GetTableCellCount() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; return static_cast(table_info->unique_cell_ids.size()); } absl::optional AXNode::GetTableHasColumnOrRowHeaderNode() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; return !table_info->all_headers.empty(); } AXNode* AXNode::GetTableCellFromIndex(int index) const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return nullptr; // There is a table but there is no cell with the given index. if (index < 0 || static_cast(index) >= table_info->unique_cell_ids.size()) { return nullptr; } return tree_->GetFromId( table_info->unique_cell_ids[static_cast(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; // There is a table but the given coordinates are outside the table. if (row_index < 0 || static_cast(row_index) >= table_info->row_count || col_index < 0 || static_cast(col_index) >= table_info->col_count) { return nullptr; } return tree_->GetFromId(table_info->cell_ids[static_cast(row_index)] [static_cast(col_index)]); } std::vector AXNode::GetTableColHeaderNodeIds() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return std::vector(); std::vector col_header_ids; // Flatten and add column header ids of each column to |col_header_ids|. for (std::vector 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 AXNode::GetTableColHeaderNodeIds(int col_index) const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return std::vector(); if (col_index < 0 || static_cast(col_index) >= table_info->col_count) return std::vector(); return std::vector( table_info->col_headers[static_cast(col_index)]); } std::vector AXNode::GetTableRowHeaderNodeIds(int row_index) const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return std::vector(); if (row_index < 0 || static_cast(row_index) >= table_info->row_count) return std::vector(); return std::vector( table_info->row_headers[static_cast(row_index)]); } std::vector AXNode::GetTableUniqueCellIds() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return std::vector(); return std::vector(table_info->unique_cell_ids); } const std::vector* AXNode::GetExtraMacNodes() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); // Should only be available on the table node itself, not any of its children. const AXTableInfo* table_info = tree_->GetTableInfo(this); if (!table_info) return nullptr; return &table_info->extra_mac_nodes; } bool AXNode::IsGenerated() const { bool is_generated_node = id() < 0; #if DCHECK_IS_ON() // Currently, the only generated nodes are columns and table header // containers, and when those roles occur, they are always extra mac nodes. // This could change in the future. 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); #endif return is_generated_node; } // // Table row-like nodes. // bool AXNode::IsTableRow() const { return ui::IsTableRow(GetRole()); } absl::optional AXNode::GetTableRowRowIndex() const { if (!IsTableRow()) return absl::nullopt; const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; const auto& iter = table_info->row_id_to_index.find(id()); if (iter == table_info->row_id_to_index.end()) return absl::nullopt; return static_cast(iter->second); } std::vector AXNode::GetTableRowNodeIds() const { std::vector 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) // // Table column-like nodes. These nodes are only present on macOS. // bool AXNode::IsTableColumn() const { return ui::IsTableColumn(GetRole()); } absl::optional AXNode::GetTableColColIndex() const { if (!IsTableColumn()) return absl::nullopt; const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; int index = 0; for (const AXNode* node : table_info->extra_mac_nodes) { if (node == this) break; index++; } return index; } #endif // BUILDFLAG(IS_APPLE) // // Table cell-like nodes. // bool AXNode::IsTableCellOrHeader() const { return IsCellOrTableHeader(GetRole()); } absl::optional AXNode::GetTableCellIndex() const { if (!IsTableCellOrHeader()) return absl::nullopt; const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; const auto& iter = table_info->cell_id_to_index.find(id()); if (iter != table_info->cell_id_to_index.end()) return static_cast(iter->second); return absl::nullopt; } absl::optional AXNode::GetTableCellColIndex() const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; absl::optional index = GetTableCellIndex(); if (!index) return absl::nullopt; return static_cast(table_info->cell_data_vector[*index].col_index); } absl::optional AXNode::GetTableCellRowIndex() const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; absl::optional index = GetTableCellIndex(); if (!index) return absl::nullopt; return static_cast(table_info->cell_data_vector[*index].row_index); } absl::optional AXNode::GetTableCellColSpan() const { // If it's not a table cell, don't return a col span. if (!IsTableCellOrHeader()) return absl::nullopt; // Otherwise, try to return a colspan, with 1 as the default if it's not // specified. int col_span; if (GetIntAttribute(ax::mojom::IntAttribute::kTableCellColumnSpan, &col_span)) return col_span; return 1; } absl::optional AXNode::GetTableCellRowSpan() const { // If it's not a table cell, don't return a row span. if (!IsTableCellOrHeader()) return absl::nullopt; // Otherwise, try to return a row span, with 1 as the default if it's not // specified. int row_span; if (GetIntAttribute(ax::mojom::IntAttribute::kTableCellRowSpan, &row_span)) return row_span; return 1; } absl::optional AXNode::GetTableCellAriaColIndex() const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; absl::optional index = GetTableCellIndex(); if (!index) return absl::nullopt; int aria_col_index = static_cast(table_info->cell_data_vector[*index].aria_col_index); // |aria-colindex| attribute is one-based, value less than 1 is invalid. // https://www.w3.org/TR/wai-aria-1.2/#aria-colindex return (aria_col_index > 0) ? absl::optional(aria_col_index) : absl::nullopt; } absl::optional AXNode::GetTableCellAriaRowIndex() const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) return absl::nullopt; absl::optional index = GetTableCellIndex(); if (!index) return absl::nullopt; int aria_row_index = static_cast(table_info->cell_data_vector[*index].aria_row_index); // |aria-rowindex| attribute is one-based, value less than 1 is invalid. // https://www.w3.org/TR/wai-aria-1.2/#aria-rowindex return (aria_row_index > 0) ? absl::optional(aria_row_index) : absl::nullopt; } std::vector AXNode::GetTableCellColHeaderNodeIds() const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info || table_info->col_count <= 0) return std::vector(); // If this node is not a cell, then return the headers for the first column. int col_index = GetTableCellColIndex().value_or(0); return std::vector(table_info->col_headers[col_index]); } void AXNode::GetTableCellColHeaders(std::vector* col_headers) const { DCHECK(col_headers); std::vector col_header_ids = GetTableCellColHeaderNodeIds(); IdVectorToNodeVector(col_header_ids, col_headers); } std::vector AXNode::GetTableCellRowHeaderNodeIds() const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info || table_info->row_count <= 0) return std::vector(); // If this node is not a cell, then return the headers for the first row. int row_index = GetTableCellRowIndex().value_or(0); return std::vector(table_info->row_headers[row_index]); } void AXNode::GetTableCellRowHeaders(std::vector* row_headers) const { DCHECK(row_headers); std::vector 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->GetParent(); if (node) return tree_->GetTableInfo(node); return nullptr; } void AXNode::IdVectorToNodeVector(const std::vector& ids, std::vector* nodes) const { for (AXNodeID id : ids) { AXNode* node = tree_->GetFromId(id); if (node) nodes->push_back(node); } } absl::optional AXNode::GetHierarchicalLevel() const { int hierarchical_level = GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel); // According to the WAI_ARIA spec, a defined hierarchical level value is // greater than 0. // https://www.w3.org/TR/wai-aria-1.1/#aria-level if (hierarchical_level > 0) return hierarchical_level; return absl::nullopt; } bool AXNode::IsOrderedSetItem() const { // Tree grid rows should be treated as ordered set items. Since we don't have // a separate row role for tree grid rows, we can't just add the Role::kRow to // IsItemLike. We need to validate that the row is indeed part of a tree grid. if (IsRowInTreeGrid(GetOrderedSet())) return true; return ui::IsItemLike(GetRole()); } bool AXNode::IsOrderedSet() const { // Tree grid rows should be considered like ordered set items and a tree grid // like an ordered set. Continuing that logic, in order to compute the right // PosInSet and SetSize, row groups inside of a tree grid should also be // ordered sets. if (IsRowGroupInTreeGrid()) return true; return ui::IsSetLike(GetRole()); } // Uses AXTree's cache to calculate node's PosInSet. absl::optional AXNode::GetPosInSet() { return tree_->GetPosInSet(*this); } // Uses AXTree's cache to calculate node's SetSize. absl::optional AXNode::GetSetSize() { return tree_->GetSetSize(*this); } // Returns true if the role of ordered set matches the role of item. // Returns false otherwise. bool AXNode::SetRoleMatchesItemRole(const AXNode* ordered_set) const { ax::mojom::Role item_role = GetRole(); // Tree grid rows should be treated as ordered set items. if (IsRowInTreeGrid(ordered_set)) return true; // Switch on role of ordered set 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::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: 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: // Only the term for each description list entry should receive posinset // and setsize. return item_role == ax::mojom::Role::kDescriptionListTerm || item_role == ax::mojom::Role::kTerm; case ax::mojom::Role::kComboBoxSelect: // kComboBoxSelect wraps a kMenuListPopUp. return item_role == ax::mojom::Role::kMenuListPopup; default: return false; } } bool AXNode::IsIgnoredContainerForOrderedSet() const { return IsIgnored() || IsEmbeddedGroup() || GetRole() == ax::mojom::Role::kLabelText || GetRole() == ax::mojom::Role::kListItem || GetRole() == ax::mojom::Role::kGenericContainer || GetRole() == ax::mojom::Role::kScrollView || GetRole() == ax::mojom::Role::kUnknown; } bool AXNode::IsRowInTreeGrid(const AXNode* ordered_set) const { // Tree grid rows have the requirement of being focusable, so we use it to // avoid iterating over rows that clearly aren't part of a tree grid. if (GetRole() != ax::mojom::Role::kRow || !HasState(ax::mojom::State::kFocusable) || !ordered_set) { return false; } if (ordered_set->GetRole() == ax::mojom::Role::kTreeGrid) return true; return ordered_set->IsRowGroupInTreeGrid(); } bool AXNode::IsRowGroupInTreeGrid() const { // To the best of our understanding, row groups can't be nested. // // According to https://www.w3.org/TR/wai-aria-1.1/#rowgroup, a row group is a // "structural equivalent to the thead, tfoot, and tbody elements in an HTML // table". It is specified in the spec of the thead, tfoot and tbody elements // that they need to be children of a table element, meaning that there can // only be one level of such elements. We assume the same for row groups. 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; } // Finds ordered set that contains node. // Is not required for set's role to match node's role. AXNode* AXNode::GetOrderedSet() const { AXNode* result = GetParent(); // Continue walking up while parent is invalid, ignored, a generic container, // unknown, or embedded group. while (result && result->IsIgnoredContainerForOrderedSet()) { result = result->GetParent(); } return result; } bool AXNode::IsReadOnlySupported() const { // Grid cells and headers can't be derived solely from the role (need to check // the ancestor chain) so check this first. 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; // When readonly is not supported, we assume that the node is always // read-only and mark it as such since this is the default behavior. return !IsReadOnlySupported(); } } } AXNode* AXNode::ComputeLastUnignoredChildRecursive() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (children().empty()) return nullptr; for (int i = static_cast(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); float numeric_value; if (range_value.empty() && GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange, &numeric_value)) { // This method of number to string conversion creates a localized string // and avoids padding with extra zeros after the decimal point. // For example, 3.5 is converted to "3.5" rather than "3.50000". return base::StringPrintf("%g", numeric_value); } return range_value; } std::string AXNode::GetValueForColorWell() const { DCHECK_EQ(GetRole(), ax::mojom::Role::kColorWell); // static cast because SkColor is a 4-byte unsigned int unsigned int color = static_cast( 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 the focus has moved, then it could make a previously ignored node // unignored or vice versa. We never ignore focused nodes otherwise users of // assistive software might be unable to interact with the webpage. return AXTree::ComputeNodeIsIgnored(&tree_->data(), data()); } bool AXNode::IsIgnoredForTextNavigation() const { // Splitters do not contribute anything to the tree's text representation, so // stopping on a splitter would erroniously appear to a screen reader user // that the cursor has stopped on the next unignored object. if (GetRole() == ax::mojom::Role::kSplitter) return true; // A generic container without any unignored children that is not editable // should not be used for text-based navigation. Such nodes don't make sense // for screen readers to land on, since no role / text will be announced and // no action is possible. 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 { // TODO(nektar): Cache this state in `AXComputedNodeData`. 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(); // Text exposed by ignored leaf (text) nodes is not exposed to the platforms' // accessibility layer, hence such leaf nodes are in effect empty. return IsIgnored() || !GetTextContentLengthUTF8(); } bool AXNode::IsLeaf() const { // A node is a leaf if it has no descendants, i.e. if it is at the bottom of // the tree, regardless whether it is ignored or not. if (!GetChildCountCrossingTreeBoundary()) return true; // Ignored nodes with any kind of descendants, (ignored or unignored), cannot // be leaves because: A) If some of their descendants are unignored then those // descendants need to be exposed to the platform layer, and B) If all of // their descendants are ignored they cannot be at the bottom of the platform // tree since that tree does not expose any ignored objects. if (IsIgnored()) return false; // An unignored node is a leaf if all of its descendants are ignored. int child_count = GetUnignoredChildCountCrossingTreeBoundary(); if (!child_count) return true; #if BUILDFLAG(IS_WIN) // On Windows, we want to hide the subtree of a collapsed , wrap the wrapper as well. // There are several incarnations of this structure. // 1. An empty atomic text field: // -- Generic container <-- there can be any number of these in a chain. // However, some empty text fields have the below structure, with empty // text boxes. // 2. A single line, an atomic text field with some text in it: // -- Generic container <-- there can be any number of these in a chain. // ---- Static text // ------ Inline text box children (zero or more) // ---- Line Break (optional, a placeholder break element if the text data // ends with '\n' or '\r') // 3. A multiline textarea with some text in it: // Similar to #2, but can repeat the static text, line break children // multiple times. AXNode* text_container = GetDeepestFirstUnignoredChild(); DCHECK(text_container) << "Unable to retrieve deepest unignored child on\n" << *this; // Non-empty text fields expose a set of static text objects with one or more // inline text boxes each. On some platforms, such as Android, we don't enable // inline text boxes, and only the static text objects are exposed. if (text_container->GetRole() == ax::mojom::Role::kInlineTextBox) text_container = text_container->GetUnignoredParent(); // Get the parent of the static text or the line break, if any; a line break // is possible when the field contains a line break as its first character. 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 { for (AXNode* ancestor = const_cast(this); ancestor; ancestor = ancestor->GetUnignoredParent()) { if (IsContainerWithSelectableChildren(ancestor->GetRole())) return ancestor; } return nullptr; } AXNode* AXNode::GetTableAncestor() const { for (AXNode* ancestor = const_cast(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(); } } // namespace ui