| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/accessibility/platform/one_shot_accessibility_tree_search.h" |
| |
| #include <stdint.h> |
| |
| #include <string> |
| |
| #include "base/i18n/case_conversion.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| #include "ui/accessibility/platform/browser_accessibility.h" |
| #include "ui/accessibility/platform/browser_accessibility_manager.h" |
| |
| namespace ui { |
| |
| // 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<std::u16string>* strings) { |
| std::u16string 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); |
| } |
| |
| namespace { |
| |
| bool IsBlockContainer(BrowserAccessibility* node) { |
| if (!node || !node->node()) { |
| return false; |
| } |
| const AXNode* ax_node = node->node(); |
| return (ax_node->GetBoolAttribute( |
| ax::mojom::BoolAttribute::kIsLineBreakingObject) || |
| ax_node->GetRole() == ax::mojom::Role::kParagraph) && |
| ax_node->GetRole() != ax::mojom::Role::kLineBreak && |
| ax_node->GetRole() != ax::mojom::Role::kInlineTextBox; |
| } |
| |
| // Helper that checks if node has any block container descendants or control |
| // descendants. |
| bool HasBlockDescendantOrControl(BrowserAccessibility* node) { |
| if (!node) { |
| return false; |
| } |
| for (size_t i = 0; i < node->PlatformChildCount(); ++i) { |
| BrowserAccessibility* child = node->PlatformGetChild(i); |
| if (IsControl(child->GetRole()) || IsBlockContainer(child) || |
| HasBlockDescendantOrControl(child)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| OneShotAccessibilityTreeSearch::OneShotAccessibilityTreeSearch( |
| BrowserAccessibility* scope) |
| : tree_(scope->manager()), scope_node_(scope), start_node_(scope) {} |
| |
| OneShotAccessibilityTreeSearch::~OneShotAccessibilityTreeSearch() = default; |
| |
| 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(size_t result_limit) { |
| DCHECK(!did_search_); |
| result_limit_ = result_limit; |
| } |
| |
| void OneShotAccessibilityTreeSearch::SetSearchLimit(size_t search_limit) { |
| DCHECK(!did_search_); |
| search_limit_ = search_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. |
| |
| size_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(); |
| |
| size_t index = (direction_ == FORWARDS ? 0 : count - 1); |
| if (start_node_) { |
| index = start_node_->GetIndexInParent().value(); |
| if (direction_ == FORWARDS) |
| index++; |
| else |
| index--; |
| } |
| |
| size_t nodes_checked_count = 0; |
| while (index < count && (!result_limit_ || matches_.size() < result_limit_) && |
| (!search_limit_ || nodes_checked_count < search_limit_)) { |
| BrowserAccessibility* node = scope_node_->PlatformGetChild(index); |
| |
| if (Matches(node)) { |
| matches_.push_back(node); |
| } |
| nodes_checked_count++; |
| |
| 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 = nullptr; |
| if (direction_ == FORWARDS) { |
| // Walk up the ancestor chain until we find an ancestor that has a next |
| // platform sibling. That sibling is the node that comes immediately after |
| // the scope_node's subtree. |
| for (BrowserAccessibility* current_node = scope_node_; current_node; |
| current_node = current_node->PlatformGetParent()) { |
| if (auto* next_sibling = current_node->PlatformGetNextSibling()) { |
| stop_node = next_sibling; |
| break; |
| } |
| } |
| } else { |
| stop_node = tree_->PreviousInTreeOrder(scope_node_, /* wrap */ false); |
| } |
| |
| size_t nodes_checked_count = 0; |
| while (node && node != stop_node && |
| (!result_limit_ || matches_.size() < result_limit_) && |
| (!search_limit_ || nodes_checked_count < search_limit_)) { |
| if (Matches(node)) { |
| matches_.push_back(node); |
| } |
| nodes_checked_count++; |
| |
| 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()->GetBrowserAccessibilityRoot() == node) { |
| stop_node = node; |
| } |
| node = tree_->PreviousInTreeOrder(node, can_wrap_to_last_element_); |
| } |
| } |
| } |
| |
| bool OneShotAccessibilityTreeSearch::Matches(BrowserAccessibility* node) { |
| if (!predicates_.empty()) { |
| bool found_predicate_match = false; |
| for (auto predicate : predicates_) { |
| if (predicate(start_node_, node)) { |
| found_predicate_match = true; |
| break; |
| } |
| } |
| if (!found_predicate_match) |
| 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()) { |
| std::u16string search_text_lower = |
| base::i18n::ToLower(base::UTF8ToUTF16(search_text_)); |
| std::vector<std::u16string> node_strings; |
| GetNodeStrings(node, &node_strings); |
| bool found_text_match = false; |
| for (auto node_string : node_strings) { |
| std::u16string node_string_lower = base::i18n::ToLower(node_string); |
| if (node_string_lower.contains(search_text_lower)) { |
| 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::kComboBoxSelect); |
| } |
| |
| bool AccessibilityContainedInAtomicLiveRegionPredicate( |
| BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| // Nodes contained in an atomic live region must record the ID of their root |
| // node. If it is not present, we should not store the node as a match. |
| if (!node->HasIntAttribute(ax::mojom::IntAttribute::kMemberOfId)) { |
| return false; |
| } |
| bool is_contained = |
| node->GetBoolAttribute(ax::mojom::BoolAttribute::kContainerLiveAtomic); |
| int node_root_id = |
| node->GetIntAttribute(ax::mojom::IntAttribute::kMemberOfId); |
| int start_id = start->GetData().id; |
| // We are only interested in nodes that are contained in the live region |
| // rooted at `start`. |
| return is_contained && (node_root_id == start_id); |
| } |
| |
| bool AccessibilityControlPredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| if (IsControl(node->GetRole())) { |
| return true; |
| } |
| if (node->HasState(ax::mojom::State::kFocusable) && |
| node->GetRole() != ax::mojom::Role::kIframe && |
| node->GetRole() != ax::mojom::Role::kIframePresentational && |
| !IsLink(node->GetRole()) && !IsPlatformDocument(node->GetRole())) { |
| return true; |
| } |
| return false; |
| } |
| |
| bool AccessibilityFocusablePredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| bool focusable = node->HasState(ax::mojom::State::kFocusable); |
| if (IsIframe(node->GetRole()) || IsPlatformDocument(node->GetRole())) { |
| focusable = false; |
| } |
| return focusable; |
| } |
| |
| bool AccessibilityGraphicPredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return IsImageOrVideo(node->GetRole()); |
| } |
| |
| bool AccessibilityHeadingPredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return IsHeading(node->GetRole()); |
| } |
| |
| bool AccessibilityH1Predicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return (IsHeading(node->GetRole()) && |
| node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) == |
| 1); |
| } |
| |
| bool AccessibilityH2Predicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return (IsHeading(node->GetRole()) && |
| node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) == |
| 2); |
| } |
| |
| bool AccessibilityH3Predicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return (IsHeading(node->GetRole()) && |
| node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) == |
| 3); |
| } |
| |
| bool AccessibilityH4Predicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return (IsHeading(node->GetRole()) && |
| node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) == |
| 4); |
| } |
| |
| bool AccessibilityH5Predicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return (IsHeading(node->GetRole()) && |
| node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) == |
| 5); |
| } |
| |
| bool AccessibilityH6Predicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return (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->IsRootWebAreaForPresentationalIframe()) |
| return false; |
| if (!node->PlatformGetParent()) |
| return false; |
| return IsPlatformDocument(node->GetRole()); |
| } |
| |
| 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::kFooter: |
| case ax::mojom::Role::kForm: |
| case ax::mojom::Role::kHeader: |
| 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 IsLink(node->GetRole()); |
| } |
| |
| bool AccessibilityListPredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return IsList(node->GetRole()); |
| } |
| |
| bool AccessibilityListItemPredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return 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 AccessibilityParagraphPredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| if (start->IsDescendantOf(node)) { |
| return false; |
| } |
| |
| ax::mojom::Role role = node->GetRole(); |
| |
| if (role == ax::mojom::Role::kParagraph) { |
| return true; |
| } |
| |
| if (!IsBlockContainer(node)) { |
| return false; |
| } |
| |
| // Exclude roles that are definitely not paragraphs (containers or controls). |
| if (IsList(role) || IsTableLike(role) || IsPlatformDocument(role) || |
| role == ax::mojom::Role::kDocument || IsControl(role)) { |
| return false; |
| } |
| |
| // Exclude block containers that have block descendants (we only want leaf |
| // blocks) or descendants that are interactive controls (e.g. <div> wrapping |
| // buttons). |
| if (HasBlockDescendantOrControl(node)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| 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 AccessibilitySectionPredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return AccessibilityLandmarkPredicate(start, node) || |
| node->GetRole() == ax::mojom::Role::kHeading; |
| } |
| |
| bool AccessibilityTablePredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return IsTableLike(node->GetRole()); |
| } |
| |
| bool AccessibilityTextfieldPredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return node->IsTextField(); |
| } |
| |
| bool AccessibilityTextStyleBoldPredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return node->HasTextStyle(ax::mojom::TextStyle::kBold); |
| } |
| |
| bool AccessibilityTextStyleItalicPredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return node->HasTextStyle(ax::mojom::TextStyle::kItalic); |
| } |
| |
| bool AccessibilityTextStyleUnderlinePredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return node->HasTextStyle(ax::mojom::TextStyle::kUnderline); |
| } |
| |
| bool AccessibilityTreePredicate(BrowserAccessibility* start, |
| BrowserAccessibility* node) { |
| return node->IsTextField(); |
| } |
| |
| 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 ui |