1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ui/accessibility/ax_node_position.h"
6
7 #include "base/strings/string_util.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "build/build_config.h"
10 #include "ui/accessibility/ax_enums.mojom.h"
11 #include "ui/accessibility/ax_node_data.h"
12 #include "ui/accessibility/ax_tree_manager.h"
13 #include "ui/accessibility/ax_tree_manager_map.h"
14
15 namespace ui {
16
17 AXEmbeddedObjectBehavior g_ax_embedded_object_behavior =
18 #if defined(OS_WIN)
19 AXEmbeddedObjectBehavior::kExposeCharacter;
20 #else
21 AXEmbeddedObjectBehavior::kSuppressCharacter;
22 #endif // defined(OS_WIN)
23
24 // static
CreatePosition(const AXNode & node,int child_index_or_text_offset,ax::mojom::TextAffinity affinity)25 AXNodePosition::AXPositionInstance AXNodePosition::CreatePosition(
26 const AXNode& node,
27 int child_index_or_text_offset,
28 ax::mojom::TextAffinity affinity) {
29 if (!node.tree())
30 return CreateNullPosition();
31
32 AXTreeID tree_id = node.tree()->GetAXTreeID();
33 if (node.IsText()) {
34 return CreateTextPosition(tree_id, node.id(), child_index_or_text_offset,
35 affinity);
36 }
37
38 return CreateTreePosition(tree_id, node.id(), child_index_or_text_offset);
39 }
40
41 AXNodePosition::AXNodePosition() = default;
42
43 AXNodePosition::~AXNodePosition() = default;
44
AXNodePosition(const AXNodePosition & other)45 AXNodePosition::AXNodePosition(const AXNodePosition& other)
46 : AXPosition<AXNodePosition, AXNode>(other) {}
47
Clone() const48 AXNodePosition::AXPositionInstance AXNodePosition::Clone() const {
49 return AXPositionInstance(new AXNodePosition(*this));
50 }
51
AnchorChild(int child_index,AXTreeID * tree_id,AXNode::AXID * child_id) const52 void AXNodePosition::AnchorChild(int child_index,
53 AXTreeID* tree_id,
54 AXNode::AXID* child_id) const {
55 DCHECK(tree_id);
56 DCHECK(child_id);
57
58 if (!GetAnchor() || child_index < 0 || child_index >= AnchorChildCount()) {
59 *tree_id = AXTreeIDUnknown();
60 *child_id = AXNode::kInvalidAXID;
61 return;
62 }
63
64 AXNode* child = nullptr;
65 const AXTreeManager* child_tree_manager =
66 AXTreeManagerMap::GetInstance().GetManagerForChildTree(*GetAnchor());
67 if (child_tree_manager) {
68 // The child node exists in a separate tree from its parent.
69 child = child_tree_manager->GetRootAsAXNode();
70 *tree_id = child_tree_manager->GetTreeID();
71 } else {
72 child = GetAnchor()->children()[size_t{child_index}];
73 *tree_id = this->tree_id();
74 }
75
76 DCHECK(child);
77 *child_id = child->id();
78 }
79
AnchorChildCount() const80 int AXNodePosition::AnchorChildCount() const {
81 if (!GetAnchor())
82 return 0;
83
84 const AXTreeManager* child_tree_manager =
85 AXTreeManagerMap::GetInstance().GetManagerForChildTree(*GetAnchor());
86 if (child_tree_manager)
87 return 1;
88
89 return int{GetAnchor()->children().size()};
90 }
91
AnchorUnignoredChildCount() const92 int AXNodePosition::AnchorUnignoredChildCount() const {
93 if (!GetAnchor())
94 return 0;
95
96 return static_cast<int>(GetAnchor()->GetUnignoredChildCount());
97 }
98
AnchorIndexInParent() const99 int AXNodePosition::AnchorIndexInParent() const {
100 return GetAnchor() ? int{GetAnchor()->index_in_parent()} : INVALID_INDEX;
101 }
102
AnchorSiblingCount() const103 int AXNodePosition::AnchorSiblingCount() const {
104 AXNode* parent = GetAnchor()->GetUnignoredParent();
105 if (parent)
106 return static_cast<int>(parent->GetUnignoredChildCount());
107
108 return 0;
109 }
110
GetAncestorAnchors() const111 base::stack<AXNode*> AXNodePosition::GetAncestorAnchors() const {
112 base::stack<AXNode*> anchors;
113 AXNode* current_anchor = GetAnchor();
114
115 AXNode::AXID current_anchor_id = GetAnchor()->id();
116 AXTreeID current_tree_id = tree_id();
117
118 AXNode::AXID parent_anchor_id = AXNode::kInvalidAXID;
119 AXTreeID parent_tree_id = AXTreeIDUnknown();
120
121 while (current_anchor) {
122 anchors.push(current_anchor);
123 current_anchor = GetParent(
124 current_anchor /*child*/, current_tree_id /*child_tree_id*/,
125 &parent_tree_id /*parent_tree_id*/, &parent_anchor_id /*parent_id*/);
126
127 current_anchor_id = parent_anchor_id;
128 current_tree_id = parent_tree_id;
129 }
130 return anchors;
131 }
132
GetLowestUnignoredAncestor() const133 AXNode* AXNodePosition::GetLowestUnignoredAncestor() const {
134 if (!GetAnchor())
135 return nullptr;
136
137 return GetAnchor()->GetUnignoredParent();
138 }
139
AnchorParent(AXTreeID * tree_id,AXNode::AXID * parent_id) const140 void AXNodePosition::AnchorParent(AXTreeID* tree_id,
141 AXNode::AXID* parent_id) const {
142 DCHECK(tree_id);
143 DCHECK(parent_id);
144
145 *tree_id = AXTreeIDUnknown();
146 *parent_id = AXNode::kInvalidAXID;
147
148 if (!GetAnchor())
149 return;
150
151 AXNode* parent =
152 GetParent(GetAnchor() /*child*/, this->tree_id() /*child_tree_id*/,
153 tree_id /*parent_tree_id*/, parent_id /*parent_id*/);
154
155 if (!parent) {
156 *tree_id = AXTreeIDUnknown();
157 *parent_id = AXNode::kInvalidAXID;
158 }
159 }
160
GetNodeInTree(AXTreeID tree_id,AXNode::AXID node_id) const161 AXNode* AXNodePosition::GetNodeInTree(AXTreeID tree_id,
162 AXNode::AXID node_id) const {
163 if (node_id == AXNode::kInvalidAXID)
164 return nullptr;
165
166 AXTreeManager* manager = AXTreeManagerMap::GetInstance().GetManager(tree_id);
167 if (manager)
168 return manager->GetNodeFromTree(tree_id, node_id);
169
170 return nullptr;
171 }
172
GetAnchorID(AXNode * node) const173 AXNode::AXID AXNodePosition::GetAnchorID(AXNode* node) const {
174 return node->id();
175 }
176
GetTreeID(AXNode * node) const177 AXTreeID AXNodePosition::GetTreeID(AXNode* node) const {
178 return node->tree()->GetAXTreeID();
179 }
180
GetText() const181 base::string16 AXNodePosition::GetText() const {
182 if (IsNullPosition())
183 return base::string16();
184
185 // Special case, if a node has only ignored descendants, i.e., it appears to
186 // be empty to assistive software, on some platforms we need to still treat it
187 // as a character and a word boundary. We achieve this by adding an embedded
188 // object character in the text representation used by this class, but we
189 // don't expose that character to assistive software that tries to retrieve
190 // the node's inner text.
191 if (IsEmptyObjectReplacedByCharacter())
192 return base::string16(1, kEmbeddedCharacter);
193
194 const AXNode* anchor = GetAnchor();
195 DCHECK(anchor);
196 switch (g_ax_embedded_object_behavior) {
197 case AXEmbeddedObjectBehavior::kSuppressCharacter:
198 return base::UTF8ToUTF16(anchor->GetInnerText());
199 case AXEmbeddedObjectBehavior::kExposeCharacter:
200 return base::UTF8ToUTF16(anchor->GetHypertext());
201 }
202 }
203
IsInLineBreak() const204 bool AXNodePosition::IsInLineBreak() const {
205 if (IsNullPosition())
206 return false;
207 DCHECK(GetAnchor());
208 return GetAnchor()->IsLineBreak();
209 }
210
IsInTextObject() const211 bool AXNodePosition::IsInTextObject() const {
212 if (IsNullPosition())
213 return false;
214 DCHECK(GetAnchor());
215 return GetAnchor()->IsText();
216 }
217
IsInWhiteSpace() const218 bool AXNodePosition::IsInWhiteSpace() const {
219 if (IsNullPosition())
220 return false;
221 DCHECK(GetAnchor());
222 return GetAnchor()->IsLineBreak() ||
223 base::ContainsOnlyChars(GetText(), base::kWhitespaceUTF16);
224 }
225
226 // This override is an optimized version AXPosition::MaxTextOffset. Instead of
227 // concatenating the strings in GetText() to then get their text length, we sum
228 // the lengths of the individual strings. This is faster than concatenating the
229 // strings first and then taking their length, especially when the process
230 // is recursive.
MaxTextOffset() const231 int AXNodePosition::MaxTextOffset() const {
232 if (IsNullPosition())
233 return INVALID_OFFSET;
234
235 if (IsEmptyObjectReplacedByCharacter())
236 return 1;
237
238 const AXNode* anchor = GetAnchor();
239 DCHECK(anchor);
240 // TODO(nektar): Replace with PlatformChildCount when AXNodePosition and
241 // BrowserAccessibilityPosition will be merged.
242 if (!AnchorChildCount() || anchor->IsText())
243 return base::UTF8ToUTF16(anchor->GetInnerText()).length();
244
245 int text_length = 0;
246 // This is an optimization over retrieving the text of the whole subtree and
247 // then finding its length. It saves time by adding lengths instead of
248 // concatenating strings.
249 for (int i = 0; i < AnchorChildCount(); ++i)
250 text_length += CreateChildPositionAt(i)->MaxTextOffset();
251
252 return text_length;
253 }
254
IsEmbeddedObjectInParent() const255 bool AXNodePosition::IsEmbeddedObjectInParent() const {
256 switch (g_ax_embedded_object_behavior) {
257 case AXEmbeddedObjectBehavior::kSuppressCharacter:
258 return false;
259 case AXEmbeddedObjectBehavior::kExposeCharacter:
260 // We don't need to expose an "embedded object character" for textual
261 // nodes and nodes that are invisible to platform APIs. Textual nodes are
262 // represented by their actual text.
263 return !IsNullPosition() && !GetAnchor()->IsText() &&
264 !GetAnchor()->IsDescendantOfPlainTextField() &&
265 GetAnchor()->IsChildOfLeaf();
266 }
267 }
268
IsInLineBreakingObject() const269 bool AXNodePosition::IsInLineBreakingObject() const {
270 if (IsNullPosition())
271 return false;
272 DCHECK(GetAnchor());
273 return GetAnchor()->data().GetBoolAttribute(
274 ax::mojom::BoolAttribute::kIsLineBreakingObject) &&
275 !GetAnchor()->IsInListMarker();
276 }
277
GetAnchorRole() const278 ax::mojom::Role AXNodePosition::GetAnchorRole() const {
279 if (IsNullPosition())
280 return ax::mojom::Role::kNone;
281 DCHECK(GetAnchor());
282 return GetRole(GetAnchor());
283 }
284
GetRole(AXNode * node) const285 ax::mojom::Role AXNodePosition::GetRole(AXNode* node) const {
286 return node->data().role;
287 }
288
GetTextStyles() const289 AXNodeTextStyles AXNodePosition::GetTextStyles() const {
290 // Check either the current anchor or its parent for text styles.
291 AXNodeTextStyles current_anchor_text_styles =
292 !IsNullPosition() ? GetAnchor()->data().GetTextStyles()
293 : AXNodeTextStyles();
294 if (current_anchor_text_styles.IsUnset()) {
295 AXPositionInstance parent = CreateParentPosition();
296 if (!parent->IsNullPosition())
297 return parent->GetAnchor()->data().GetTextStyles();
298 }
299 return current_anchor_text_styles;
300 }
301
GetWordStartOffsets() const302 std::vector<int32_t> AXNodePosition::GetWordStartOffsets() const {
303 if (IsNullPosition())
304 return std::vector<int32_t>();
305 DCHECK(GetAnchor());
306
307 // Embedded object replacement characters are not represented in |kWordStarts|
308 // attribute.
309 if (IsEmptyObjectReplacedByCharacter())
310 return {0};
311
312 return GetAnchor()->data().GetIntListAttribute(
313 ax::mojom::IntListAttribute::kWordStarts);
314 }
315
GetWordEndOffsets() const316 std::vector<int32_t> AXNodePosition::GetWordEndOffsets() const {
317 if (IsNullPosition())
318 return std::vector<int32_t>();
319 DCHECK(GetAnchor());
320
321 // Embedded object replacement characters are not represented in |kWordEnds|
322 // attribute. Since the whole text exposed inside of an embedded object is of
323 // length 1 (the embedded object replacement character), the word end offset
324 // is positioned at 1. Because we want to treat the embedded object
325 // replacement characters as ordinary characters, it wouldn't be consistent to
326 // assume they have no length and return 0 instead of 1.
327 if (IsEmptyObjectReplacedByCharacter())
328 return {1};
329
330 return GetAnchor()->data().GetIntListAttribute(
331 ax::mojom::IntListAttribute::kWordEnds);
332 }
333
GetNextOnLineID(AXNode::AXID node_id) const334 AXNode::AXID AXNodePosition::GetNextOnLineID(AXNode::AXID node_id) const {
335 if (IsNullPosition())
336 return AXNode::kInvalidAXID;
337 AXNode* node = GetNodeInTree(tree_id(), node_id);
338 int next_on_line_id;
339 if (!node || !node->data().GetIntAttribute(
340 ax::mojom::IntAttribute::kNextOnLineId, &next_on_line_id)) {
341 return AXNode::kInvalidAXID;
342 }
343 return static_cast<AXNode::AXID>(next_on_line_id);
344 }
345
GetPreviousOnLineID(AXNode::AXID node_id) const346 AXNode::AXID AXNodePosition::GetPreviousOnLineID(AXNode::AXID node_id) const {
347 if (IsNullPosition())
348 return AXNode::kInvalidAXID;
349 AXNode* node = GetNodeInTree(tree_id(), node_id);
350 int previous_on_line_id;
351 if (!node ||
352 !node->data().GetIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
353 &previous_on_line_id)) {
354 return AXNode::kInvalidAXID;
355 }
356 return static_cast<AXNode::AXID>(previous_on_line_id);
357 }
358
GetParent(AXNode * child,AXTreeID child_tree_id,AXTreeID * parent_tree_id,AXNode::AXID * parent_id)359 AXNode* AXNodePosition::GetParent(AXNode* child,
360 AXTreeID child_tree_id,
361 AXTreeID* parent_tree_id,
362 AXNode::AXID* parent_id) {
363 DCHECK(parent_tree_id);
364 DCHECK(parent_id);
365
366 *parent_tree_id = AXTreeIDUnknown();
367 *parent_id = AXNode::kInvalidAXID;
368
369 if (!child)
370 return nullptr;
371
372 AXNode* parent = child->parent();
373 *parent_tree_id = child_tree_id;
374
375 if (!parent) {
376 AXTreeManager* manager =
377 AXTreeManagerMap::GetInstance().GetManager(child_tree_id);
378 if (manager) {
379 parent = manager->GetParentNodeFromParentTreeAsAXNode();
380 *parent_tree_id = manager->GetParentTreeID();
381 }
382 }
383
384 if (!parent) {
385 *parent_tree_id = AXTreeIDUnknown();
386 return parent;
387 }
388
389 *parent_id = parent->id();
390 return parent;
391 }
392
393 } // namespace ui
394