// Copyright 2016 The Chromium Authors. All rights reserved. // 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_position.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/ax_tree_manager.h" #include "ui/accessibility/ax_tree_manager_map.h" namespace ui { AXEmbeddedObjectBehavior g_ax_embedded_object_behavior = #if defined(OS_WIN) AXEmbeddedObjectBehavior::kExposeCharacter; #else AXEmbeddedObjectBehavior::kSuppressCharacter; #endif // defined(OS_WIN) // static AXNodePosition::AXPositionInstance AXNodePosition::CreatePosition( const AXNode& node, int child_index_or_text_offset, ax::mojom::TextAffinity affinity) { if (!node.tree()) return CreateNullPosition(); AXTreeID tree_id = node.tree()->GetAXTreeID(); if (node.IsText()) { return CreateTextPosition(tree_id, node.id(), child_index_or_text_offset, affinity); } return CreateTreePosition(tree_id, node.id(), child_index_or_text_offset); } AXNodePosition::AXNodePosition() = default; AXNodePosition::~AXNodePosition() = default; AXNodePosition::AXNodePosition(const AXNodePosition& other) : AXPosition(other) {} AXNodePosition::AXPositionInstance AXNodePosition::Clone() const { return AXPositionInstance(new AXNodePosition(*this)); } void AXNodePosition::AnchorChild(int child_index, AXTreeID* tree_id, AXNode::AXID* child_id) const { DCHECK(tree_id); DCHECK(child_id); if (!GetAnchor() || child_index < 0 || child_index >= AnchorChildCount()) { *tree_id = AXTreeIDUnknown(); *child_id = AXNode::kInvalidAXID; return; } AXNode* child = nullptr; const AXTreeManager* child_tree_manager = AXTreeManagerMap::GetInstance().GetManagerForChildTree(*GetAnchor()); if (child_tree_manager) { // The child node exists in a separate tree from its parent. child = child_tree_manager->GetRootAsAXNode(); *tree_id = child_tree_manager->GetTreeID(); } else { child = GetAnchor()->children()[size_t(child_index)]; *tree_id = this->tree_id(); } DCHECK(child); *child_id = child->id(); } int AXNodePosition::AnchorChildCount() const { if (!GetAnchor()) return 0; const AXTreeManager* child_tree_manager = AXTreeManagerMap::GetInstance().GetManagerForChildTree(*GetAnchor()); if (child_tree_manager) return 1; return int(GetAnchor()->children().size()); } int AXNodePosition::AnchorUnignoredChildCount() const { if (!GetAnchor()) return 0; return static_cast(GetAnchor()->GetUnignoredChildCount()); } int AXNodePosition::AnchorIndexInParent() const { return GetAnchor() ? int(GetAnchor()->index_in_parent()) : INVALID_INDEX; } int AXNodePosition::AnchorSiblingCount() const { AXNode* parent = GetAnchor()->GetUnignoredParent(); if (parent) return static_cast(parent->GetUnignoredChildCount()); return 0; } base::stack AXNodePosition::GetAncestorAnchors() const { base::stack anchors; AXNode* current_anchor = GetAnchor(); AXNode::AXID current_anchor_id = GetAnchor()->id(); AXTreeID current_tree_id = tree_id(); AXNode::AXID parent_anchor_id = AXNode::kInvalidAXID; AXTreeID parent_tree_id = AXTreeIDUnknown(); while (current_anchor) { anchors.push(current_anchor); current_anchor = GetParent( current_anchor /*child*/, current_tree_id /*child_tree_id*/, &parent_tree_id /*parent_tree_id*/, &parent_anchor_id /*parent_id*/); current_anchor_id = parent_anchor_id; current_tree_id = parent_tree_id; } return anchors; } AXNode* AXNodePosition::GetLowestUnignoredAncestor() const { if (!GetAnchor()) return nullptr; return GetAnchor()->GetUnignoredParent(); } void AXNodePosition::AnchorParent(AXTreeID* tree_id, AXNode::AXID* parent_id) const { DCHECK(tree_id); DCHECK(parent_id); *tree_id = AXTreeIDUnknown(); *parent_id = AXNode::kInvalidAXID; if (!GetAnchor()) return; AXNode* parent = GetParent(GetAnchor() /*child*/, this->tree_id() /*child_tree_id*/, tree_id /*parent_tree_id*/, parent_id /*parent_id*/); if (!parent) { *tree_id = AXTreeIDUnknown(); *parent_id = AXNode::kInvalidAXID; } } AXNode* AXNodePosition::GetNodeInTree(AXTreeID tree_id, AXNode::AXID node_id) const { if (node_id == AXNode::kInvalidAXID) return nullptr; AXTreeManager* manager = AXTreeManagerMap::GetInstance().GetManager(tree_id); if (manager) return manager->GetNodeFromTree(tree_id, node_id); return nullptr; } AXNode::AXID AXNodePosition::GetAnchorID(AXNode* node) const { return node->id(); } AXTreeID AXNodePosition::GetTreeID(AXNode* node) const { return node->tree()->GetAXTreeID(); } base::string16 AXNodePosition::GetText() const { if (IsNullPosition()) return base::string16(); // Special case, if a node has only ignored descendants, i.e., it appears to // be empty to assistive software, on some platforms we need to still treat it // as a character and a word boundary. We achieve this by adding an embedded // object character in the text representation used by this class, but we // don't expose that character to assistive software that tries to retrieve // the node's inner text. if (IsEmptyObjectReplacedByCharacter()) return base::string16(1, kEmbeddedCharacter); const AXNode* anchor = GetAnchor(); DCHECK(anchor); switch (g_ax_embedded_object_behavior) { case AXEmbeddedObjectBehavior::kSuppressCharacter: return base::UTF8ToUTF16(anchor->GetInnerText()); case AXEmbeddedObjectBehavior::kExposeCharacter: return base::UTF8ToUTF16(anchor->GetHypertext()); } } bool AXNodePosition::IsInLineBreak() const { if (IsNullPosition()) return false; DCHECK(GetAnchor()); return GetAnchor()->IsLineBreak(); } bool AXNodePosition::IsInTextObject() const { if (IsNullPosition()) return false; DCHECK(GetAnchor()); return GetAnchor()->IsText(); } bool AXNodePosition::IsInWhiteSpace() const { if (IsNullPosition()) return false; DCHECK(GetAnchor()); return GetAnchor()->IsLineBreak() || base::ContainsOnlyChars(GetText(), base::kWhitespaceUTF16); } // This override is an optimized version AXPosition::MaxTextOffset. Instead of // concatenating the strings in GetText() to then get their text length, we sum // the lengths of the individual strings. This is faster than concatenating the // strings first and then taking their length, especially when the process // is recursive. int AXNodePosition::MaxTextOffset() const { if (IsNullPosition()) return INVALID_OFFSET; if (IsEmptyObjectReplacedByCharacter()) return 1; const AXNode* anchor = GetAnchor(); DCHECK(anchor); // TODO(nektar): Replace with PlatformChildCount when AXNodePosition and // BrowserAccessibilityPosition will be merged. if (!AnchorChildCount() || anchor->IsText()) return base::UTF8ToUTF16(anchor->GetInnerText()).length(); int text_length = 0; // This is an optimization over retrieving the text of the whole subtree and // then finding its length. It saves time by adding lengths instead of // concatenating strings. for (int i = 0; i < AnchorChildCount(); ++i) text_length += CreateChildPositionAt(i)->MaxTextOffset(); return text_length; } bool AXNodePosition::IsEmbeddedObjectInParent() const { switch (g_ax_embedded_object_behavior) { case AXEmbeddedObjectBehavior::kSuppressCharacter: return false; case AXEmbeddedObjectBehavior::kExposeCharacter: // We don't need to expose an "embedded object character" for textual // nodes and nodes that are invisible to platform APIs. Textual nodes are // represented by their actual text. return !IsNullPosition() && !GetAnchor()->IsText() && !GetAnchor()->IsDescendantOfPlainTextField() && GetAnchor()->IsChildOfLeaf(); } } bool AXNodePosition::IsInLineBreakingObject() const { if (IsNullPosition()) return false; DCHECK(GetAnchor()); return GetAnchor()->data().GetBoolAttribute( ax::mojom::BoolAttribute::kIsLineBreakingObject) && !GetAnchor()->IsInListMarker(); } ax::mojom::Role AXNodePosition::GetAnchorRole() const { if (IsNullPosition()) return ax::mojom::Role::kNone; DCHECK(GetAnchor()); return GetRole(GetAnchor()); } ax::mojom::Role AXNodePosition::GetRole(AXNode* node) const { return node->data().role; } AXNodeTextStyles AXNodePosition::GetTextStyles() const { // Check either the current anchor or its parent for text styles. AXNodeTextStyles current_anchor_text_styles = !IsNullPosition() ? GetAnchor()->data().GetTextStyles() : AXNodeTextStyles(); if (current_anchor_text_styles.IsUnset()) { AXPositionInstance parent = CreateParentPosition(); if (!parent->IsNullPosition()) return parent->GetAnchor()->data().GetTextStyles(); } return current_anchor_text_styles; } std::vector AXNodePosition::GetWordStartOffsets() const { if (IsNullPosition()) return std::vector(); DCHECK(GetAnchor()); // Embedded object replacement characters are not represented in |kWordStarts| // attribute. if (IsEmptyObjectReplacedByCharacter()) return {0}; return GetAnchor()->data().GetIntListAttribute( ax::mojom::IntListAttribute::kWordStarts); } std::vector AXNodePosition::GetWordEndOffsets() const { if (IsNullPosition()) return std::vector(); DCHECK(GetAnchor()); // Embedded object replacement characters are not represented in |kWordEnds| // attribute. Since the whole text exposed inside of an embedded object is of // length 1 (the embedded object replacement character), the word end offset // is positioned at 1. Because we want to treat the embedded object // replacement characters as ordinary characters, it wouldn't be consistent to // assume they have no length and return 0 instead of 1. if (IsEmptyObjectReplacedByCharacter()) return {1}; return GetAnchor()->data().GetIntListAttribute( ax::mojom::IntListAttribute::kWordEnds); } AXNode::AXID AXNodePosition::GetNextOnLineID(AXNode::AXID node_id) const { if (IsNullPosition()) return AXNode::kInvalidAXID; AXNode* node = GetNodeInTree(tree_id(), node_id); int next_on_line_id; if (!node || !node->data().GetIntAttribute( ax::mojom::IntAttribute::kNextOnLineId, &next_on_line_id)) { return AXNode::kInvalidAXID; } return static_cast(next_on_line_id); } AXNode::AXID AXNodePosition::GetPreviousOnLineID(AXNode::AXID node_id) const { if (IsNullPosition()) return AXNode::kInvalidAXID; AXNode* node = GetNodeInTree(tree_id(), node_id); int previous_on_line_id; if (!node || !node->data().GetIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, &previous_on_line_id)) { return AXNode::kInvalidAXID; } return static_cast(previous_on_line_id); } AXNode* AXNodePosition::GetParent(AXNode* child, AXTreeID child_tree_id, AXTreeID* parent_tree_id, AXNode::AXID* parent_id) { DCHECK(parent_tree_id); DCHECK(parent_id); *parent_tree_id = AXTreeIDUnknown(); *parent_id = AXNode::kInvalidAXID; if (!child) return nullptr; AXNode* parent = child->parent(); *parent_tree_id = child_tree_id; if (!parent) { AXTreeManager* manager = AXTreeManagerMap::GetInstance().GetManager(child_tree_id); if (manager) { parent = manager->GetParentNodeFromParentTreeAsAXNode(); *parent_tree_id = manager->GetParentTreeID(); } } if (!parent) { *parent_tree_id = AXTreeIDUnknown(); return parent; } *parent_id = parent->id(); return parent; } } // namespace ui