1 // Copyright 2015 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 "content/browser/accessibility/one_shot_accessibility_tree_search.h"
6
7 #include <stdint.h>
8
9 #include "base/i18n/case_conversion.h"
10 #include "base/strings/string16.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "content/browser/accessibility/browser_accessibility.h"
13 #include "content/browser/accessibility/browser_accessibility_manager.h"
14 #include "ui/accessibility/ax_enums.mojom.h"
15 #include "ui/accessibility/ax_node.h"
16 #include "ui/accessibility/ax_role_properties.h"
17
18 namespace content {
19
20 // Given a node, populate a vector with all of the strings from that node's
21 // attributes that might be relevant for a text search.
GetNodeStrings(BrowserAccessibility * node,std::vector<base::string16> * strings)22 void GetNodeStrings(BrowserAccessibility* node,
23 std::vector<base::string16>* strings) {
24 base::string16 value;
25 if (node->GetString16Attribute(ax::mojom::StringAttribute::kName, &value))
26 strings->push_back(value);
27 if (node->GetString16Attribute(ax::mojom::StringAttribute::kDescription,
28 &value)) {
29 strings->push_back(value);
30 }
31 value = node->GetValueForControl();
32 if (!value.empty())
33 strings->push_back(value);
34 }
35
OneShotAccessibilityTreeSearch(BrowserAccessibility * scope)36 OneShotAccessibilityTreeSearch::OneShotAccessibilityTreeSearch(
37 BrowserAccessibility* scope)
38 : tree_(scope->manager()),
39 scope_node_(scope),
40 start_node_(scope),
41 direction_(OneShotAccessibilityTreeSearch::FORWARDS),
42 result_limit_(UNLIMITED_RESULTS),
43 immediate_descendants_only_(false),
44 can_wrap_to_last_element_(false),
45 onscreen_only_(false),
46 did_search_(false) {}
47
~OneShotAccessibilityTreeSearch()48 OneShotAccessibilityTreeSearch::~OneShotAccessibilityTreeSearch() {}
49
SetStartNode(BrowserAccessibility * start_node)50 void OneShotAccessibilityTreeSearch::SetStartNode(
51 BrowserAccessibility* start_node) {
52 DCHECK(!did_search_);
53 CHECK(start_node);
54
55 if (!scope_node_->PlatformGetParent() ||
56 start_node->IsDescendantOf(scope_node_->PlatformGetParent())) {
57 start_node_ = start_node;
58 }
59 }
60
SetDirection(Direction direction)61 void OneShotAccessibilityTreeSearch::SetDirection(Direction direction) {
62 DCHECK(!did_search_);
63 direction_ = direction;
64 }
65
SetResultLimit(int result_limit)66 void OneShotAccessibilityTreeSearch::SetResultLimit(int result_limit) {
67 DCHECK(!did_search_);
68 result_limit_ = result_limit;
69 }
70
SetImmediateDescendantsOnly(bool immediate_descendants_only)71 void OneShotAccessibilityTreeSearch::SetImmediateDescendantsOnly(
72 bool immediate_descendants_only) {
73 DCHECK(!did_search_);
74 immediate_descendants_only_ = immediate_descendants_only;
75 }
76
SetCanWrapToLastElement(bool can_wrap_to_last_element)77 void OneShotAccessibilityTreeSearch::SetCanWrapToLastElement(
78 bool can_wrap_to_last_element) {
79 DCHECK(!did_search_);
80 can_wrap_to_last_element_ = can_wrap_to_last_element;
81 }
82
SetOnscreenOnly(bool onscreen_only)83 void OneShotAccessibilityTreeSearch::SetOnscreenOnly(bool onscreen_only) {
84 DCHECK(!did_search_);
85 onscreen_only_ = onscreen_only;
86 }
87
SetSearchText(const std::string & text)88 void OneShotAccessibilityTreeSearch::SetSearchText(const std::string& text) {
89 DCHECK(!did_search_);
90 search_text_ = text;
91 }
92
AddPredicate(AccessibilityMatchPredicate predicate)93 void OneShotAccessibilityTreeSearch::AddPredicate(
94 AccessibilityMatchPredicate predicate) {
95 DCHECK(!did_search_);
96 predicates_.push_back(predicate);
97 }
98
CountMatches()99 size_t OneShotAccessibilityTreeSearch::CountMatches() {
100 if (!did_search_)
101 Search();
102
103 return matches_.size();
104 }
105
GetMatchAtIndex(size_t index)106 BrowserAccessibility* OneShotAccessibilityTreeSearch::GetMatchAtIndex(
107 size_t index) {
108 if (!did_search_)
109 Search();
110
111 CHECK(index < matches_.size());
112 return matches_[index];
113 }
114
Search()115 void OneShotAccessibilityTreeSearch::Search() {
116 if (immediate_descendants_only_) {
117 SearchByIteratingOverChildren();
118 } else {
119 SearchByWalkingTree();
120 }
121 did_search_ = true;
122 }
123
SearchByIteratingOverChildren()124 void OneShotAccessibilityTreeSearch::SearchByIteratingOverChildren() {
125 // Iterate over the children of scope_node_.
126 // If start_node_ is specified, iterate over the first child past that
127 // node.
128
129 uint32_t count = scope_node_->PlatformChildCount();
130 if (count == 0)
131 return;
132
133 // We only care about immediate children of scope_node_, so walk up
134 // start_node_ until we get to an immediate child. If it isn't a child,
135 // we ignore start_node_.
136 while (start_node_ && start_node_->PlatformGetParent() != scope_node_)
137 start_node_ = start_node_->PlatformGetParent();
138
139 uint32_t index = (direction_ == FORWARDS ? 0 : count - 1);
140 if (start_node_) {
141 index = start_node_->GetIndexInParent();
142 if (direction_ == FORWARDS)
143 index++;
144 else
145 index--;
146 }
147
148 while (index < count && (result_limit_ == UNLIMITED_RESULTS ||
149 static_cast<int>(matches_.size()) < result_limit_)) {
150 BrowserAccessibility* node = scope_node_->PlatformGetChild(index);
151 if (Matches(node))
152 matches_.push_back(node);
153
154 if (direction_ == FORWARDS)
155 index++;
156 else
157 index--;
158 }
159 }
160
SearchByWalkingTree()161 void OneShotAccessibilityTreeSearch::SearchByWalkingTree() {
162 BrowserAccessibility* node = nullptr;
163 node = start_node_;
164 if (node != scope_node_ || result_limit_ == 1) {
165 if (direction_ == FORWARDS)
166 node = tree_->NextInTreeOrder(start_node_);
167 else
168 node = tree_->PreviousInTreeOrder(start_node_, can_wrap_to_last_element_);
169 }
170
171 BrowserAccessibility* stop_node = scope_node_->PlatformGetParent();
172 while (node && node != stop_node &&
173 (result_limit_ == UNLIMITED_RESULTS ||
174 static_cast<int>(matches_.size()) < result_limit_)) {
175 if (Matches(node))
176 matches_.push_back(node);
177
178 if (direction_ == FORWARDS) {
179 node = tree_->NextInTreeOrder(node);
180 } else {
181 // This needs to be handled carefully. If not, there is a chance of
182 // getting into infinite loop.
183 if (can_wrap_to_last_element_ && !stop_node &&
184 node->manager()->GetRoot() == node) {
185 stop_node = node;
186 }
187 node = tree_->PreviousInTreeOrder(node, can_wrap_to_last_element_);
188 }
189 }
190 }
191
Matches(BrowserAccessibility * node)192 bool OneShotAccessibilityTreeSearch::Matches(BrowserAccessibility* node) {
193 for (size_t i = 0; i < predicates_.size(); ++i) {
194 if (!predicates_[i](start_node_, node))
195 return false;
196 }
197
198 if (node->HasState(ax::mojom::State::kInvisible))
199 return false; // Programmatically hidden, e.g. aria-hidden or via CSS.
200
201 if (onscreen_only_ && node->IsOffscreen())
202 return false; // Partly scrolled offscreen.
203
204 if (!search_text_.empty()) {
205 base::string16 search_text_lower =
206 base::i18n::ToLower(base::UTF8ToUTF16(search_text_));
207 std::vector<base::string16> node_strings;
208 GetNodeStrings(node, &node_strings);
209 bool found_text_match = false;
210 for (size_t i = 0; i < node_strings.size(); ++i) {
211 base::string16 node_string_lower = base::i18n::ToLower(node_strings[i]);
212 if (node_string_lower.find(search_text_lower) != base::string16::npos) {
213 found_text_match = true;
214 break;
215 }
216 }
217 if (!found_text_match)
218 return false;
219 }
220
221 return true;
222 }
223
224 //
225 // Predicates
226 //
227
AccessibilityArticlePredicate(BrowserAccessibility * start,BrowserAccessibility * node)228 bool AccessibilityArticlePredicate(BrowserAccessibility* start,
229 BrowserAccessibility* node) {
230 return node->GetRole() == ax::mojom::Role::kArticle;
231 }
232
AccessibilityButtonPredicate(BrowserAccessibility * start,BrowserAccessibility * node)233 bool AccessibilityButtonPredicate(BrowserAccessibility* start,
234 BrowserAccessibility* node) {
235 switch (node->GetRole()) {
236 case ax::mojom::Role::kButton:
237 case ax::mojom::Role::kPopUpButton:
238 case ax::mojom::Role::kSwitch:
239 case ax::mojom::Role::kToggleButton:
240 return true;
241 default:
242 return false;
243 }
244 }
245
AccessibilityBlockquotePredicate(BrowserAccessibility * start,BrowserAccessibility * node)246 bool AccessibilityBlockquotePredicate(BrowserAccessibility* start,
247 BrowserAccessibility* node) {
248 return node->GetRole() == ax::mojom::Role::kBlockquote;
249 }
250
AccessibilityCheckboxPredicate(BrowserAccessibility * start,BrowserAccessibility * node)251 bool AccessibilityCheckboxPredicate(BrowserAccessibility* start,
252 BrowserAccessibility* node) {
253 return (node->GetRole() == ax::mojom::Role::kCheckBox ||
254 node->GetRole() == ax::mojom::Role::kMenuItemCheckBox);
255 }
256
AccessibilityComboboxPredicate(BrowserAccessibility * start,BrowserAccessibility * node)257 bool AccessibilityComboboxPredicate(BrowserAccessibility* start,
258 BrowserAccessibility* node) {
259 return (node->GetRole() == ax::mojom::Role::kComboBoxGrouping ||
260 node->GetRole() == ax::mojom::Role::kComboBoxMenuButton ||
261 node->GetRole() == ax::mojom::Role::kTextFieldWithComboBox ||
262 node->GetRole() == ax::mojom::Role::kPopUpButton);
263 }
264
AccessibilityControlPredicate(BrowserAccessibility * start,BrowserAccessibility * node)265 bool AccessibilityControlPredicate(BrowserAccessibility* start,
266 BrowserAccessibility* node) {
267 if (ui::IsControl(node->GetRole()))
268 return true;
269 if (node->HasState(ax::mojom::State::kFocusable) &&
270 node->GetRole() != ax::mojom::Role::kIframe &&
271 node->GetRole() != ax::mojom::Role::kIframePresentational &&
272 !ui::IsLink(node->GetRole()) &&
273 node->GetRole() != ax::mojom::Role::kWebArea &&
274 node->GetRole() != ax::mojom::Role::kRootWebArea) {
275 return true;
276 }
277 return false;
278 }
279
AccessibilityFocusablePredicate(BrowserAccessibility * start,BrowserAccessibility * node)280 bool AccessibilityFocusablePredicate(BrowserAccessibility* start,
281 BrowserAccessibility* node) {
282 bool focusable = node->HasState(ax::mojom::State::kFocusable);
283 if (node->GetRole() == ax::mojom::Role::kIframe ||
284 node->GetRole() == ax::mojom::Role::kIframePresentational ||
285 node->GetRole() == ax::mojom::Role::kWebArea ||
286 node->GetRole() == ax::mojom::Role::kRootWebArea) {
287 focusable = false;
288 }
289 return focusable;
290 }
291
AccessibilityGraphicPredicate(BrowserAccessibility * start,BrowserAccessibility * node)292 bool AccessibilityGraphicPredicate(BrowserAccessibility* start,
293 BrowserAccessibility* node) {
294 return ui::IsImageOrVideo(node->GetRole());
295 }
296
AccessibilityHeadingPredicate(BrowserAccessibility * start,BrowserAccessibility * node)297 bool AccessibilityHeadingPredicate(BrowserAccessibility* start,
298 BrowserAccessibility* node) {
299 return ui::IsHeading(node->GetRole());
300 }
301
AccessibilityH1Predicate(BrowserAccessibility * start,BrowserAccessibility * node)302 bool AccessibilityH1Predicate(BrowserAccessibility* start,
303 BrowserAccessibility* node) {
304 return (ui::IsHeading(node->GetRole()) &&
305 node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
306 1);
307 }
308
AccessibilityH2Predicate(BrowserAccessibility * start,BrowserAccessibility * node)309 bool AccessibilityH2Predicate(BrowserAccessibility* start,
310 BrowserAccessibility* node) {
311 return (ui::IsHeading(node->GetRole()) &&
312 node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
313 2);
314 }
315
AccessibilityH3Predicate(BrowserAccessibility * start,BrowserAccessibility * node)316 bool AccessibilityH3Predicate(BrowserAccessibility* start,
317 BrowserAccessibility* node) {
318 return (ui::IsHeading(node->GetRole()) &&
319 node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
320 3);
321 }
322
AccessibilityH4Predicate(BrowserAccessibility * start,BrowserAccessibility * node)323 bool AccessibilityH4Predicate(BrowserAccessibility* start,
324 BrowserAccessibility* node) {
325 return (ui::IsHeading(node->GetRole()) &&
326 node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
327 4);
328 }
329
AccessibilityH5Predicate(BrowserAccessibility * start,BrowserAccessibility * node)330 bool AccessibilityH5Predicate(BrowserAccessibility* start,
331 BrowserAccessibility* node) {
332 return (ui::IsHeading(node->GetRole()) &&
333 node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
334 5);
335 }
336
AccessibilityH6Predicate(BrowserAccessibility * start,BrowserAccessibility * node)337 bool AccessibilityH6Predicate(BrowserAccessibility* start,
338 BrowserAccessibility* node) {
339 return (ui::IsHeading(node->GetRole()) &&
340 node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
341 6);
342 }
343
AccessibilityHeadingSameLevelPredicate(BrowserAccessibility * start,BrowserAccessibility * node)344 bool AccessibilityHeadingSameLevelPredicate(BrowserAccessibility* start,
345 BrowserAccessibility* node) {
346 return (
347 node->GetRole() == ax::mojom::Role::kHeading &&
348 start->GetRole() == ax::mojom::Role::kHeading &&
349 (node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
350 start->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel)));
351 }
352
AccessibilityFramePredicate(BrowserAccessibility * start,BrowserAccessibility * node)353 bool AccessibilityFramePredicate(BrowserAccessibility* start,
354 BrowserAccessibility* node) {
355 if (node->IsWebAreaForPresentationalIframe())
356 return false;
357 if (!node->PlatformGetParent())
358 return false;
359 return (node->GetRole() == ax::mojom::Role::kWebArea ||
360 node->GetRole() == ax::mojom::Role::kRootWebArea);
361 }
362
AccessibilityLandmarkPredicate(BrowserAccessibility * start,BrowserAccessibility * node)363 bool AccessibilityLandmarkPredicate(BrowserAccessibility* start,
364 BrowserAccessibility* node) {
365 switch (node->GetRole()) {
366 case ax::mojom::Role::kApplication:
367 case ax::mojom::Role::kArticle:
368 case ax::mojom::Role::kBanner:
369 case ax::mojom::Role::kComplementary:
370 case ax::mojom::Role::kContentInfo:
371 case ax::mojom::Role::kMain:
372 case ax::mojom::Role::kNavigation:
373 case ax::mojom::Role::kRegion:
374 case ax::mojom::Role::kSearch:
375 case ax::mojom::Role::kSection:
376 return true;
377 default:
378 return false;
379 }
380 }
381
AccessibilityLinkPredicate(BrowserAccessibility * start,BrowserAccessibility * node)382 bool AccessibilityLinkPredicate(BrowserAccessibility* start,
383 BrowserAccessibility* node) {
384 return ui::IsLink(node->GetRole());
385 }
386
AccessibilityListPredicate(BrowserAccessibility * start,BrowserAccessibility * node)387 bool AccessibilityListPredicate(BrowserAccessibility* start,
388 BrowserAccessibility* node) {
389 return ui::IsList(node->GetRole());
390 }
391
AccessibilityListItemPredicate(BrowserAccessibility * start,BrowserAccessibility * node)392 bool AccessibilityListItemPredicate(BrowserAccessibility* start,
393 BrowserAccessibility* node) {
394 return ui::IsListItem(node->GetRole());
395 }
396
AccessibilityLiveRegionPredicate(BrowserAccessibility * start,BrowserAccessibility * node)397 bool AccessibilityLiveRegionPredicate(BrowserAccessibility* start,
398 BrowserAccessibility* node) {
399 return node->HasStringAttribute(ax::mojom::StringAttribute::kLiveStatus);
400 }
401
AccessibilityMainPredicate(BrowserAccessibility * start,BrowserAccessibility * node)402 bool AccessibilityMainPredicate(BrowserAccessibility* start,
403 BrowserAccessibility* node) {
404 return (node->GetRole() == ax::mojom::Role::kMain);
405 }
406
AccessibilityMediaPredicate(BrowserAccessibility * start,BrowserAccessibility * node)407 bool AccessibilityMediaPredicate(BrowserAccessibility* start,
408 BrowserAccessibility* node) {
409 const std::string& tag =
410 node->GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag);
411 return tag == "audio" || tag == "video";
412 }
413
AccessibilityRadioButtonPredicate(BrowserAccessibility * start,BrowserAccessibility * node)414 bool AccessibilityRadioButtonPredicate(BrowserAccessibility* start,
415 BrowserAccessibility* node) {
416 return (node->GetRole() == ax::mojom::Role::kRadioButton ||
417 node->GetRole() == ax::mojom::Role::kMenuItemRadio);
418 }
419
AccessibilityRadioGroupPredicate(BrowserAccessibility * start,BrowserAccessibility * node)420 bool AccessibilityRadioGroupPredicate(BrowserAccessibility* start,
421 BrowserAccessibility* node) {
422 return node->GetRole() == ax::mojom::Role::kRadioGroup;
423 }
424
AccessibilityTablePredicate(BrowserAccessibility * start,BrowserAccessibility * node)425 bool AccessibilityTablePredicate(BrowserAccessibility* start,
426 BrowserAccessibility* node) {
427 return ui::IsTableLike(node->GetRole());
428 }
429
AccessibilityTextfieldPredicate(BrowserAccessibility * start,BrowserAccessibility * node)430 bool AccessibilityTextfieldPredicate(BrowserAccessibility* start,
431 BrowserAccessibility* node) {
432 return (node->IsPlainTextField() || node->IsRichTextField());
433 }
434
AccessibilityTextStyleBoldPredicate(BrowserAccessibility * start,BrowserAccessibility * node)435 bool AccessibilityTextStyleBoldPredicate(BrowserAccessibility* start,
436 BrowserAccessibility* node) {
437 return node->GetData().HasTextStyle(ax::mojom::TextStyle::kBold);
438 }
439
AccessibilityTextStyleItalicPredicate(BrowserAccessibility * start,BrowserAccessibility * node)440 bool AccessibilityTextStyleItalicPredicate(BrowserAccessibility* start,
441 BrowserAccessibility* node) {
442 return node->GetData().HasTextStyle(ax::mojom::TextStyle::kItalic);
443 }
444
AccessibilityTextStyleUnderlinePredicate(BrowserAccessibility * start,BrowserAccessibility * node)445 bool AccessibilityTextStyleUnderlinePredicate(BrowserAccessibility* start,
446 BrowserAccessibility* node) {
447 return node->GetData().HasTextStyle(ax::mojom::TextStyle::kUnderline);
448 }
449
AccessibilityTreePredicate(BrowserAccessibility * start,BrowserAccessibility * node)450 bool AccessibilityTreePredicate(BrowserAccessibility* start,
451 BrowserAccessibility* node) {
452 return (node->IsPlainTextField() || node->IsRichTextField());
453 }
454
AccessibilityUnvisitedLinkPredicate(BrowserAccessibility * start,BrowserAccessibility * node)455 bool AccessibilityUnvisitedLinkPredicate(BrowserAccessibility* start,
456 BrowserAccessibility* node) {
457 return node->GetRole() == ax::mojom::Role::kLink &&
458 !node->HasState(ax::mojom::State::kVisited);
459 }
460
AccessibilityVisitedLinkPredicate(BrowserAccessibility * start,BrowserAccessibility * node)461 bool AccessibilityVisitedLinkPredicate(BrowserAccessibility* start,
462 BrowserAccessibility* node) {
463 return node->GetRole() == ax::mojom::Role::kLink &&
464 node->HasState(ax::mojom::State::kVisited);
465 }
466
467 } // namespace content
468