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