| // 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<AXNodePosition, AXNode>(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<int>(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<int>(parent->GetUnignoredChildCount()); |
| |
| return 0; |
| } |
| |
| base::stack<AXNode*> AXNodePosition::GetAncestorAnchors() const { |
| base::stack<AXNode*> 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 expose an "object replacement character" for all nodes except |
| // textual nodes as well as nodes that are invisible to platform APIs, AKA |
| // nodes that are descendants of platform leaves. In the former case, |
| // textual nodes are represented by their actual text in the text of their |
| // parent nodes, in order to maintain compatibility with how Firefox |
| // exposes text in IAccessibleText. For the latter case, an example of a |
| // platform leaf is a plain text field because all of the accessibility |
| // subtree inside the text field is not visible to platform APIs. |
| // |
| // Please note that for navigational purposes, we need to expose an |
| // "object replacement character" in empty controls, such as in an empty |
| // text field. The presence or the absence of accessible content inside a |
| // control might alter whether an "object replacement character" would be |
| // exposed in that control, in contrast to ordinary text such as in the |
| // case of a non-empty simple text field which should only have textual |
| // nodes inside it. This is because empty controls need to act as a word |
| // and character boundary. See |
| // "AXPosition::IsEmptyObjectReplacedByCharacter()" for more information. |
| return !IsNullPosition() && !GetAnchor()->IsText() && |
| !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<int32_t> AXNodePosition::GetWordStartOffsets() const { |
| if (IsNullPosition()) |
| return std::vector<int32_t>(); |
| 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<int32_t> AXNodePosition::GetWordEndOffsets() const { |
| if (IsNullPosition()) |
| return std::vector<int32_t>(); |
| 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<AXNode::AXID>(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<AXNode::AXID>(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 |