blob: 2904cd87dd8b5f54ccacf402f6872746e12ff312 [file] [log] [blame]
// Copyright 2015 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 "content/browser/accessibility/one_shot_accessibility_tree_search.h"
#include <stdint.h>
#include "base/i18n/case_conversion.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/accessibility/browser_accessibility.h"
#include "content/browser/accessibility/browser_accessibility_manager.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h"
namespace content {
// Given a node, populate a vector with all of the strings from that node's
// attributes that might be relevant for a text search.
void GetNodeStrings(BrowserAccessibility* node,
std::vector<base::string16>* strings) {
base::string16 value;
if (node->GetString16Attribute(ax::mojom::StringAttribute::kName, &value))
strings->push_back(value);
if (node->GetString16Attribute(ax::mojom::StringAttribute::kDescription,
&value)) {
strings->push_back(value);
}
value = node->GetValueForControl();
if (!value.empty())
strings->push_back(value);
}
OneShotAccessibilityTreeSearch::OneShotAccessibilityTreeSearch(
BrowserAccessibility* scope)
: tree_(scope->manager()),
scope_node_(scope),
start_node_(scope),
direction_(OneShotAccessibilityTreeSearch::FORWARDS),
result_limit_(UNLIMITED_RESULTS),
immediate_descendants_only_(false),
can_wrap_to_last_element_(false),
onscreen_only_(false),
did_search_(false) {}
OneShotAccessibilityTreeSearch::~OneShotAccessibilityTreeSearch() {}
void OneShotAccessibilityTreeSearch::SetStartNode(
BrowserAccessibility* start_node) {
DCHECK(!did_search_);
CHECK(start_node);
if (!scope_node_->PlatformGetParent() ||
start_node->IsDescendantOf(scope_node_->PlatformGetParent())) {
start_node_ = start_node;
}
}
void OneShotAccessibilityTreeSearch::SetDirection(Direction direction) {
DCHECK(!did_search_);
direction_ = direction;
}
void OneShotAccessibilityTreeSearch::SetResultLimit(int result_limit) {
DCHECK(!did_search_);
result_limit_ = result_limit;
}
void OneShotAccessibilityTreeSearch::SetImmediateDescendantsOnly(
bool immediate_descendants_only) {
DCHECK(!did_search_);
immediate_descendants_only_ = immediate_descendants_only;
}
void OneShotAccessibilityTreeSearch::SetCanWrapToLastElement(
bool can_wrap_to_last_element) {
DCHECK(!did_search_);
can_wrap_to_last_element_ = can_wrap_to_last_element;
}
void OneShotAccessibilityTreeSearch::SetOnscreenOnly(bool onscreen_only) {
DCHECK(!did_search_);
onscreen_only_ = onscreen_only;
}
void OneShotAccessibilityTreeSearch::SetSearchText(const std::string& text) {
DCHECK(!did_search_);
search_text_ = text;
}
void OneShotAccessibilityTreeSearch::AddPredicate(
AccessibilityMatchPredicate predicate) {
DCHECK(!did_search_);
predicates_.push_back(predicate);
}
size_t OneShotAccessibilityTreeSearch::CountMatches() {
if (!did_search_)
Search();
return matches_.size();
}
BrowserAccessibility* OneShotAccessibilityTreeSearch::GetMatchAtIndex(
size_t index) {
if (!did_search_)
Search();
CHECK(index < matches_.size());
return matches_[index];
}
void OneShotAccessibilityTreeSearch::Search() {
if (immediate_descendants_only_) {
SearchByIteratingOverChildren();
} else {
SearchByWalkingTree();
}
did_search_ = true;
}
void OneShotAccessibilityTreeSearch::SearchByIteratingOverChildren() {
// Iterate over the children of scope_node_.
// If start_node_ is specified, iterate over the first child past that
// node.
uint32_t count = scope_node_->PlatformChildCount();
if (count == 0)
return;
// We only care about immediate children of scope_node_, so walk up
// start_node_ until we get to an immediate child. If it isn't a child,
// we ignore start_node_.
while (start_node_ && start_node_->PlatformGetParent() != scope_node_)
start_node_ = start_node_->PlatformGetParent();
uint32_t index = (direction_ == FORWARDS ? 0 : count - 1);
if (start_node_) {
index = start_node_->GetIndexInParent();
if (direction_ == FORWARDS)
index++;
else
index--;
}
while (index < count && (result_limit_ == UNLIMITED_RESULTS ||
static_cast<int>(matches_.size()) < result_limit_)) {
BrowserAccessibility* node = scope_node_->PlatformGetChild(index);
if (Matches(node))
matches_.push_back(node);
if (direction_ == FORWARDS)
index++;
else
index--;
}
}
void OneShotAccessibilityTreeSearch::SearchByWalkingTree() {
BrowserAccessibility* node = nullptr;
node = start_node_;
if (node != scope_node_ || result_limit_ == 1) {
if (direction_ == FORWARDS)
node = tree_->NextInTreeOrder(start_node_);
else
node = tree_->PreviousInTreeOrder(start_node_, can_wrap_to_last_element_);
}
BrowserAccessibility* stop_node = scope_node_->PlatformGetParent();
while (node && node != stop_node &&
(result_limit_ == UNLIMITED_RESULTS ||
static_cast<int>(matches_.size()) < result_limit_)) {
if (Matches(node))
matches_.push_back(node);
if (direction_ == FORWARDS) {
node = tree_->NextInTreeOrder(node);
} else {
// This needs to be handled carefully. If not, there is a chance of
// getting into infinite loop.
if (can_wrap_to_last_element_ && !stop_node &&
node->manager()->GetRoot() == node) {
stop_node = node;
}
node = tree_->PreviousInTreeOrder(node, can_wrap_to_last_element_);
}
}
}
bool OneShotAccessibilityTreeSearch::Matches(BrowserAccessibility* node) {
for (size_t i = 0; i < predicates_.size(); ++i) {
if (!predicates_[i](start_node_, node))
return false;
}
if (node->IsInvisibleOrIgnored())
return false; // Programmatically hidden, e.g. aria-hidden or via CSS.
if (onscreen_only_ && node->IsOffscreen())
return false; // Partly scrolled offscreen.
if (!search_text_.empty()) {
base::string16 search_text_lower =
base::i18n::ToLower(base::UTF8ToUTF16(search_text_));
std::vector<base::string16> node_strings;
GetNodeStrings(node, &node_strings);
bool found_text_match = false;
for (size_t i = 0; i < node_strings.size(); ++i) {
base::string16 node_string_lower = base::i18n::ToLower(node_strings[i]);
if (node_string_lower.find(search_text_lower) != base::string16::npos) {
found_text_match = true;
break;
}
}
if (!found_text_match)
return false;
}
return true;
}
//
// Predicates
//
bool AccessibilityArticlePredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return node->GetRole() == ax::mojom::Role::kArticle;
}
bool AccessibilityButtonPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
switch (node->GetRole()) {
case ax::mojom::Role::kButton:
case ax::mojom::Role::kPopUpButton:
case ax::mojom::Role::kSwitch:
case ax::mojom::Role::kToggleButton:
return true;
default:
return false;
}
}
bool AccessibilityBlockquotePredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return node->GetRole() == ax::mojom::Role::kBlockquote;
}
bool AccessibilityCheckboxPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (node->GetRole() == ax::mojom::Role::kCheckBox ||
node->GetRole() == ax::mojom::Role::kMenuItemCheckBox);
}
bool AccessibilityComboboxPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (node->GetRole() == ax::mojom::Role::kComboBoxGrouping ||
node->GetRole() == ax::mojom::Role::kComboBoxMenuButton ||
node->GetRole() == ax::mojom::Role::kTextFieldWithComboBox ||
node->GetRole() == ax::mojom::Role::kPopUpButton);
}
bool AccessibilityControlPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
if (ui::IsControl(node->GetRole()))
return true;
if (node->HasState(ax::mojom::State::kFocusable) &&
node->GetRole() != ax::mojom::Role::kIframe &&
node->GetRole() != ax::mojom::Role::kIframePresentational &&
!ui::IsLink(node->GetRole()) &&
node->GetRole() != ax::mojom::Role::kWebArea &&
node->GetRole() != ax::mojom::Role::kRootWebArea) {
return true;
}
return false;
}
bool AccessibilityFocusablePredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
bool focusable = node->HasState(ax::mojom::State::kFocusable);
if (node->GetRole() == ax::mojom::Role::kIframe ||
node->GetRole() == ax::mojom::Role::kIframePresentational ||
node->GetRole() == ax::mojom::Role::kWebArea ||
node->GetRole() == ax::mojom::Role::kRootWebArea) {
focusable = false;
}
return focusable;
}
bool AccessibilityGraphicPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return ui::IsImageOrVideo(node->GetRole());
}
bool AccessibilityHeadingPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return ui::IsHeading(node->GetRole());
}
bool AccessibilityH1Predicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (ui::IsHeading(node->GetRole()) &&
node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
1);
}
bool AccessibilityH2Predicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (ui::IsHeading(node->GetRole()) &&
node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
2);
}
bool AccessibilityH3Predicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (ui::IsHeading(node->GetRole()) &&
node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
3);
}
bool AccessibilityH4Predicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (ui::IsHeading(node->GetRole()) &&
node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
4);
}
bool AccessibilityH5Predicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (ui::IsHeading(node->GetRole()) &&
node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
5);
}
bool AccessibilityH6Predicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (ui::IsHeading(node->GetRole()) &&
node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
6);
}
bool AccessibilityHeadingSameLevelPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (
node->GetRole() == ax::mojom::Role::kHeading &&
start->GetRole() == ax::mojom::Role::kHeading &&
(node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
start->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel)));
}
bool AccessibilityFramePredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
if (node->IsWebAreaForPresentationalIframe())
return false;
if (!node->PlatformGetParent())
return false;
return (node->GetRole() == ax::mojom::Role::kWebArea ||
node->GetRole() == ax::mojom::Role::kRootWebArea);
}
bool AccessibilityLandmarkPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
switch (node->GetRole()) {
case ax::mojom::Role::kApplication:
case ax::mojom::Role::kArticle:
case ax::mojom::Role::kBanner:
case ax::mojom::Role::kComplementary:
case ax::mojom::Role::kContentInfo:
case ax::mojom::Role::kMain:
case ax::mojom::Role::kNavigation:
case ax::mojom::Role::kRegion:
case ax::mojom::Role::kSearch:
case ax::mojom::Role::kSection:
return true;
default:
return false;
}
}
bool AccessibilityLinkPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return ui::IsLink(node->GetRole());
}
bool AccessibilityListPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return ui::IsList(node->GetRole());
}
bool AccessibilityListItemPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return ui::IsListItem(node->GetRole());
}
bool AccessibilityLiveRegionPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return node->HasStringAttribute(ax::mojom::StringAttribute::kLiveStatus);
}
bool AccessibilityMainPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (node->GetRole() == ax::mojom::Role::kMain);
}
bool AccessibilityMediaPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
const std::string& tag =
node->GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag);
return tag == "audio" || tag == "video";
}
bool AccessibilityRadioButtonPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (node->GetRole() == ax::mojom::Role::kRadioButton ||
node->GetRole() == ax::mojom::Role::kMenuItemRadio);
}
bool AccessibilityRadioGroupPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return node->GetRole() == ax::mojom::Role::kRadioGroup;
}
bool AccessibilityTablePredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return ui::IsTableLike(node->GetRole());
}
bool AccessibilityTextfieldPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (node->IsPlainTextField() || node->IsRichTextField());
}
bool AccessibilityTextStyleBoldPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return node->GetData().HasTextStyle(ax::mojom::TextStyle::kBold);
}
bool AccessibilityTextStyleItalicPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return node->GetData().HasTextStyle(ax::mojom::TextStyle::kItalic);
}
bool AccessibilityTextStyleUnderlinePredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return node->GetData().HasTextStyle(ax::mojom::TextStyle::kUnderline);
}
bool AccessibilityTreePredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return (node->IsPlainTextField() || node->IsRichTextField());
}
bool AccessibilityUnvisitedLinkPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return node->GetRole() == ax::mojom::Role::kLink &&
!node->HasState(ax::mojom::State::kVisited);
}
bool AccessibilityVisitedLinkPredicate(BrowserAccessibility* start,
BrowserAccessibility* node) {
return node->GetRole() == ax::mojom::Role::kLink &&
node->HasState(ax::mojom::State::kVisited);
}
} // namespace content