| // Copyright 2017 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 "chrome/browser/chromeos/arc/accessibility/ax_tree_source_arc.h" |
| |
| #include <stack> |
| #include <string> |
| #include <utility> |
| |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "chrome/browser/chromeos/arc/accessibility/accessibility_node_info_data_wrapper.h" |
| #include "chrome/browser/chromeos/arc/accessibility/accessibility_window_info_data_wrapper.h" |
| #include "chrome/browser/chromeos/arc/accessibility/arc_accessibility_util.h" |
| #include "chrome/browser/chromeos/arc/accessibility/geometry_util.h" |
| #include "extensions/browser/api/automation_internal/automation_event_router.h" |
| #include "extensions/common/extension_messages.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| namespace arc { |
| |
| using AXBooleanProperty = mojom::AccessibilityBooleanProperty; |
| using AXEventData = mojom::AccessibilityEventData; |
| using AXEventIntListProperty = mojom::AccessibilityEventIntListProperty; |
| using AXEventIntProperty = mojom::AccessibilityEventIntProperty; |
| using AXEventType = mojom::AccessibilityEventType; |
| using AXIntProperty = mojom::AccessibilityIntProperty; |
| using AXIntListProperty = mojom::AccessibilityIntListProperty; |
| using AXNodeInfoData = mojom::AccessibilityNodeInfoData; |
| using AXStringProperty = mojom::AccessibilityStringProperty; |
| using AXWindowInfoData = mojom::AccessibilityWindowInfoData; |
| using AXWindowIntListProperty = mojom::AccessibilityWindowIntListProperty; |
| |
| namespace { |
| bool IsDrawerLayout(AXNodeInfoData* node) { |
| if (!node || !node->string_properties) |
| return false; |
| |
| auto it = node->string_properties->find(AXStringProperty::CLASS_NAME); |
| if (it == node->string_properties->end()) |
| return false; |
| |
| return it->second == "androidx.drawerlayout.widget.DrawerLayout" || |
| it->second == "android.support.v4.widget.DrawerLayout"; |
| } |
| } // namespace |
| |
| AXTreeSourceArc::AXTreeSourceArc(Delegate* delegate, float device_scale_factor) |
| : device_scale_factor_(device_scale_factor), |
| current_tree_serializer_(new AXTreeArcSerializer(this)), |
| is_notification_(false), |
| is_input_method_window_(false), |
| delegate_(delegate) {} |
| |
| AXTreeSourceArc::~AXTreeSourceArc() { |
| Reset(); |
| } |
| |
| void AXTreeSourceArc::NotifyAccessibilityEvent(AXEventData* event_data) { |
| root_id_.reset(); |
| |
| window_id_ = event_data->window_id; |
| is_notification_ = event_data->notification_key.has_value(); |
| is_input_method_window_ = event_data->is_input_method_window; |
| |
| // Prepare the wrapper objects of mojom data from Android. |
| CHECK(event_data->window_data); |
| root_id_ = event_data->window_data->at(0)->window_id; |
| for (size_t i = 0; i < event_data->window_data->size(); ++i) { |
| int32_t window_id = event_data->window_data->at(i)->window_id; |
| int32_t root_node_id = event_data->window_data->at(i)->root_node_id; |
| AXWindowInfoData* window = event_data->window_data->at(i).get(); |
| if (root_node_id) |
| parent_map_[root_node_id] = window_id; |
| |
| tree_map_[window_id] = |
| std::make_unique<AccessibilityWindowInfoDataWrapper>(this, window); |
| |
| std::vector<int32_t> children; |
| if (GetProperty(window->int_list_properties, |
| AXWindowIntListProperty::CHILD_WINDOW_IDS, &children)) { |
| for (const int32_t child : children) { |
| DCHECK(child != root_id_); |
| parent_map_[child] = window_id; |
| } |
| } |
| } |
| |
| for (size_t i = 0; i < event_data->node_data.size(); ++i) { |
| int32_t node_id = event_data->node_data[i]->id; |
| AXNodeInfoData* node = event_data->node_data[i].get(); |
| tree_map_[node_id] = |
| std::make_unique<AccessibilityNodeInfoDataWrapper>(this, node); |
| |
| std::vector<int32_t> children; |
| if (GetProperty(event_data->node_data[i].get()->int_list_properties, |
| AXIntListProperty::CHILD_NODE_IDS, &children)) { |
| for (const int32_t child : children) |
| parent_map_[child] = node_id; |
| } |
| } |
| |
| // Compute each node's bounds, based on its descendants. |
| // Assuming |nodeData| is in pre-order, compute cached bounds in post-order to |
| // avoid an O(n^2) amount of work as the computed bounds uses descendant |
| // bounds. |
| for (int i = event_data->node_data.size() - 1; i >= 0; --i) { |
| int32_t id = event_data->node_data[i]->id; |
| computed_bounds_[id] = ComputeEnclosingBounds(tree_map_[id].get()); |
| } |
| for (int i = event_data->window_data->size() - 1; i >= 0; --i) { |
| int32_t id = event_data->window_data->at(i)->window_id; |
| computed_bounds_[id] = ComputeEnclosingBounds(tree_map_[id].get()); |
| } |
| |
| if (!UpdateAndroidFocusedId(*event_data)) { |
| // Exit this function if the focused node doesn't exist nor isn't visible. |
| return; |
| } |
| |
| if (event_data->event_type == AXEventType::WINDOW_STATE_CHANGED && |
| event_data->event_text) { |
| AccessibilityInfoDataWrapper* source_node = |
| GetFromId(event_data->source_id); |
| if (IsValid(source_node)) |
| UpdateAXNameCache(source_node, *event_data->event_text); |
| } |
| |
| ApplyCachedProperties(); |
| |
| ExtensionMsg_AccessibilityEventBundleParams event_bundle; |
| event_bundle.tree_id = ax_tree_id(); |
| |
| AccessibilityInfoDataWrapper* focused_node = |
| android_focused_id_.has_value() ? GetFromId(*android_focused_id_) |
| : nullptr; |
| event_bundle.events.emplace_back(); |
| ui::AXEvent& event = event_bundle.events.back(); |
| event.event_type = ToAXEvent( |
| event_data->event_type, |
| GetPropertyOrNull( |
| event_data->int_list_properties, |
| arc::mojom::AccessibilityEventIntListProperty::CONTENT_CHANGE_TYPES), |
| GetFromId(event_data->source_id), focused_node); |
| event.id = event_data->source_id; |
| |
| if (HasProperty(event_data->int_properties, |
| arc::mojom::AccessibilityEventIntProperty::ACTION)) { |
| event.event_from = ax::mojom::EventFrom::kAction; |
| } |
| |
| HandleLiveRegions(&event_bundle.events); |
| |
| event_bundle.updates.emplace_back(); |
| |
| // Force the tree, to update, so unignored fields get updated. |
| // On event type of WINDOW_STATE_CHANGED, update the entire tree so that |
| // window location is correctly calculated. |
| int32_t node_id_to_clear = |
| (event_data->event_type == AXEventType::WINDOW_STATE_CHANGED) |
| ? *root_id_ |
| : event_data->source_id; |
| event_bundle.updates[0].node_id_to_clear = node_id_to_clear; |
| current_tree_serializer_->InvalidateSubtree(GetFromId(node_id_to_clear)); |
| |
| current_tree_serializer_->SerializeChanges(GetFromId(node_id_to_clear), |
| &event_bundle.updates.back()); |
| |
| GetAutomationEventRouter()->DispatchAccessibilityEvents(event_bundle); |
| |
| // Clear maps in order to prevent invalid access from dead pointers. |
| tree_map_.clear(); |
| parent_map_.clear(); |
| computed_bounds_.clear(); |
| } |
| |
| void AXTreeSourceArc::NotifyActionResult(const ui::AXActionData& data, |
| bool result) { |
| GetAutomationEventRouter()->DispatchActionResult(data, result); |
| } |
| |
| void AXTreeSourceArc::NotifyGetTextLocationDataResult( |
| const ui::AXActionData& data, |
| const base::Optional<gfx::Rect>& rect) { |
| GetAutomationEventRouter()->DispatchGetTextLocationDataResult(data, rect); |
| } |
| |
| bool AXTreeSourceArc::IsScreenReaderMode() const { |
| return delegate_->IsScreenReaderEnabled(); |
| } |
| |
| void AXTreeSourceArc::InvalidateTree() { |
| current_tree_serializer_->Reset(); |
| } |
| |
| bool AXTreeSourceArc::IsRootOfNodeTree(int32_t id) const { |
| const auto& node_it = tree_map_.find(id); |
| if (node_it == tree_map_.end()) |
| return false; |
| |
| if (!node_it->second->IsNode()) |
| return false; |
| |
| const auto& parent_it = parent_map_.find(id); |
| if (parent_it == parent_map_.end()) |
| return true; |
| |
| const auto& parent_tree_it = tree_map_.find(parent_it->second); |
| CHECK(parent_tree_it != tree_map_.end()); |
| return !parent_tree_it->second->IsNode(); |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceArc::GetFirstImportantAncestor( |
| AccessibilityInfoDataWrapper* info_data) const { |
| AccessibilityInfoDataWrapper* parent = GetParent(info_data); |
| while (parent && parent->IsNode() && !parent->IsImportantInAndroid()) { |
| parent = GetParent(parent); |
| } |
| return parent; |
| } |
| |
| bool AXTreeSourceArc::GetTreeData(ui::AXTreeData* data) const { |
| data->tree_id = ax_tree_id(); |
| if (android_focused_id_.has_value()) |
| data->focus_id = *android_focused_id_; |
| return true; |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceArc::GetRoot() const { |
| return root_id_.has_value() ? GetFromId(*root_id_) : nullptr; |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceArc::GetFromId(int32_t id) const { |
| auto it = tree_map_.find(id); |
| if (it == tree_map_.end()) |
| return nullptr; |
| return it->second.get(); |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceArc::GetParent( |
| AccessibilityInfoDataWrapper* info_data) const { |
| if (!info_data) |
| return nullptr; |
| auto it = parent_map_.find(info_data->GetId()); |
| if (it != parent_map_.end()) |
| return GetFromId(it->second); |
| return nullptr; |
| } |
| |
| void AXTreeSourceArc::SerializeNode(AccessibilityInfoDataWrapper* info_data, |
| ui::AXNodeData* out_data) const { |
| if (!info_data) |
| return; |
| |
| info_data->Serialize(out_data); |
| } |
| |
| extensions::AutomationEventRouterInterface* |
| AXTreeSourceArc::GetAutomationEventRouter() const { |
| return extensions::AutomationEventRouter::GetInstance(); |
| } |
| |
| gfx::Rect AXTreeSourceArc::ComputeEnclosingBounds( |
| AccessibilityInfoDataWrapper* info_data) const { |
| DCHECK(info_data); |
| gfx::Rect computed_bounds; |
| // Exit early if the node or window is invisible. |
| if (!info_data->IsVisibleToUser()) |
| return computed_bounds; |
| |
| ComputeEnclosingBoundsInternal(info_data, &computed_bounds); |
| return computed_bounds; |
| } |
| |
| void AXTreeSourceArc::ComputeEnclosingBoundsInternal( |
| AccessibilityInfoDataWrapper* info_data, |
| gfx::Rect* computed_bounds) const { |
| DCHECK(computed_bounds); |
| auto cached_bounds = computed_bounds_.find(info_data->GetId()); |
| if (cached_bounds != computed_bounds_.end()) { |
| computed_bounds->Union(cached_bounds->second); |
| return; |
| } |
| |
| if (!info_data->IsVisibleToUser()) |
| return; |
| if (info_data->CanBeAccessibilityFocused()) { |
| // Only consider nodes that can possibly be accessibility focused. |
| computed_bounds->Union(info_data->GetBounds()); |
| return; |
| } |
| std::vector<AccessibilityInfoDataWrapper*> children; |
| info_data->GetChildren(&children); |
| if (children.empty()) |
| return; |
| for (AccessibilityInfoDataWrapper* child : children) |
| ComputeEnclosingBoundsInternal(child, computed_bounds); |
| return; |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceArc::FindFirstFocusableNode( |
| AccessibilityInfoDataWrapper* info_data) const { |
| if (!IsValid(info_data)) |
| return nullptr; |
| |
| if (info_data->IsVisibleToUser() && info_data->CanBeAccessibilityFocused()) |
| return info_data; |
| |
| std::vector<AccessibilityInfoDataWrapper*> children; |
| GetChildren(info_data, &children); |
| for (AccessibilityInfoDataWrapper* child : children) { |
| AccessibilityInfoDataWrapper* candidate = FindFirstFocusableNode(child); |
| if (candidate) |
| return candidate; |
| } |
| |
| return nullptr; |
| } |
| |
| AccessibilityInfoDataWrapper* |
| AXTreeSourceArc::GetSelectedNodeInfoFromAdapterView( |
| const AXEventData& event_data) const { |
| AccessibilityInfoDataWrapper* source_node = GetFromId(event_data.source_id); |
| if (!source_node || !source_node->IsNode()) |
| return nullptr; |
| |
| AXNodeInfoData* node_info = source_node->GetNode(); |
| if (!node_info) |
| return nullptr; |
| |
| AccessibilityInfoDataWrapper* selected_node = source_node; |
| if (!node_info->collection_item_info) { |
| // The event source is not an item of AdapterView. If the event source is |
| // AdapterView, select the child. Otherwise, this is an unrelated event. |
| int item_count, from_index, current_item_index; |
| if (!GetProperty(event_data.int_properties, AXEventIntProperty::ITEM_COUNT, |
| &item_count) || |
| !GetProperty(event_data.int_properties, AXEventIntProperty::FROM_INDEX, |
| &from_index) || |
| !GetProperty(event_data.int_properties, |
| AXEventIntProperty::CURRENT_ITEM_INDEX, |
| ¤t_item_index)) { |
| return nullptr; |
| } |
| |
| int index = current_item_index - from_index; |
| if (index < 0) |
| return nullptr; |
| |
| std::vector<AccessibilityInfoDataWrapper*> children; |
| source_node->GetChildren(&children); |
| if (index >= static_cast<int>(children.size())) |
| return nullptr; |
| |
| selected_node = children[index]; |
| } |
| |
| // Sometimes a collection item is wrapped by a non-focusable node. |
| // Find a node with focusable property. |
| while (selected_node && !GetBooleanProperty(selected_node->GetNode(), |
| AXBooleanProperty::FOCUSABLE)) { |
| std::vector<AccessibilityInfoDataWrapper*> children; |
| selected_node->GetChildren(&children); |
| if (children.size() != 1) |
| break; |
| selected_node = children[0]; |
| } |
| return selected_node; |
| } |
| |
| bool AXTreeSourceArc::UpdateAndroidFocusedId(const AXEventData& event_data) { |
| if (event_data.event_type == AXEventType::VIEW_FOCUSED) { |
| AccessibilityInfoDataWrapper* source_node = GetFromId(event_data.source_id); |
| if (source_node && source_node->IsVisibleToUser()) { |
| // Sometimes Android sets focus on unfocusable node, e.g. ListView. |
| AccessibilityInfoDataWrapper* adjusted_node = |
| FindFirstFocusableNode(source_node); |
| android_focused_id_ = IsValid(adjusted_node) ? adjusted_node->GetId() |
| : event_data.source_id; |
| } |
| } else if (event_data.event_type == AXEventType::VIEW_SELECTED) { |
| // In Android, VIEW_SELECTED event is dispatched in the two cases below: |
| // 1. Changing a value in ProgressBar or TimePicker in ARC P. |
| // 2. Selecting an item in the context of an AdapterView. |
| AccessibilityInfoDataWrapper* source_node = GetFromId(event_data.source_id); |
| if (!source_node || !source_node->IsNode()) |
| return false; |
| |
| AXNodeInfoData* node_info = source_node->GetNode(); |
| DCHECK(node_info); |
| |
| bool is_range_change = !node_info->range_info.is_null(); |
| if (!is_range_change) { |
| AccessibilityInfoDataWrapper* selected_node = |
| GetSelectedNodeInfoFromAdapterView(event_data); |
| if (!selected_node || !selected_node->IsVisibleToUser()) |
| return false; |
| |
| android_focused_id_ = selected_node->GetId(); |
| } |
| } else if (event_data.event_type == AXEventType::WINDOW_STATE_CHANGED) { |
| // When accessibility window changed, a11y event of WINDOW_CONTENT_CHANGED |
| // is fired from Android multiple times. |
| // The event of WINDOW_STATE_CHANGED is fired only once for each window |
| // change and use it as a trigger to move the a11y focus to the first node. |
| AccessibilityInfoDataWrapper* source_node = GetFromId(event_data.source_id); |
| AccessibilityInfoDataWrapper* new_focus = |
| FindFirstFocusableNode(source_node); |
| if (IsValid(new_focus)) |
| android_focused_id_ = new_focus->GetId(); |
| } |
| |
| if (!android_focused_id_ || !GetFromId(*android_focused_id_)) { |
| AccessibilityInfoDataWrapper* root = GetRoot(); |
| DCHECK(IsValid(root)); |
| android_focused_id_ = root_id_; |
| } |
| |
| AccessibilityInfoDataWrapper* focused_node = |
| android_focused_id_.has_value() ? GetFromId(*android_focused_id_) |
| : nullptr; |
| |
| // Ensure that the focused node correctly gets focus. |
| while (focused_node && !focused_node->IsImportantInAndroid()) { |
| AccessibilityInfoDataWrapper* parent = GetParent(focused_node); |
| if (parent) { |
| android_focused_id_ = parent->GetId(); |
| focused_node = parent; |
| } else { |
| break; |
| } |
| } |
| |
| return true; |
| } |
| |
| void AXTreeSourceArc::UpdateAXNameCache( |
| AccessibilityInfoDataWrapper* source_node, |
| const std::vector<std::string>& event_text) { |
| if (IsDrawerLayout(source_node->GetNode())) { |
| // When drawer menu opened, make the menu title announced. |
| // When focus is changed, ChromeVox computes the diff in ancestry between |
| // the previously focused and new focused node. |
| // As the DrawerLayout is LCA of them, set the new title to be the first |
| // visible child node (which is usually drawer menu). |
| std::vector<AccessibilityInfoDataWrapper*> children; |
| source_node->GetChildren(&children); |
| for (auto* child : children) { |
| if (child->IsNode() && child->IsVisibleToUser() && |
| GetBooleanProperty(child->GetNode(), AXBooleanProperty::IMPORTANCE)) { |
| cached_roles_[child->GetId()] = ax::mojom::Role::kMenu; |
| if (!event_text.empty()) |
| cached_names_[child->GetId()] = base::JoinString(event_text, " "); |
| return; |
| } |
| } |
| } |
| } |
| |
| void AXTreeSourceArc::ApplyCachedProperties() { |
| for (auto it = cached_names_.begin(); it != cached_names_.end();) { |
| AccessibilityInfoDataWrapper* node = GetFromId(it->first); |
| if (node) { |
| static_cast<AccessibilityNodeInfoDataWrapper*>(node)->set_cached_name( |
| it->second); |
| it++; |
| } else { |
| it = cached_names_.erase(it); |
| } |
| } |
| |
| for (auto it = cached_roles_.begin(); it != cached_roles_.end();) { |
| AccessibilityInfoDataWrapper* node = GetFromId(it->first); |
| if (node) { |
| static_cast<AccessibilityNodeInfoDataWrapper*>(node)->set_role( |
| it->second); |
| it++; |
| } else { |
| it = cached_roles_.erase(it); |
| } |
| } |
| } |
| |
| void AXTreeSourceArc::HandleLiveRegions(std::vector<ui::AXEvent>* events) { |
| std::map<int32_t, std::string> new_live_region_map; |
| |
| // Cache current live region's name. |
| for (auto const& it : tree_map_) { |
| if (!it.second->IsNode()) |
| continue; |
| |
| AccessibilityInfoDataWrapper* node_info = it.second.get(); |
| int32_t live_region_type_int = 0; |
| if (!GetProperty(node_info->GetNode()->int_properties, |
| AXIntProperty::LIVE_REGION, &live_region_type_int)) |
| continue; |
| |
| mojom::AccessibilityLiveRegionType live_region_type = |
| static_cast<mojom::AccessibilityLiveRegionType>(live_region_type_int); |
| if (live_region_type == mojom::AccessibilityLiveRegionType::NONE) |
| continue; |
| |
| // |node_info| has a live region property. |
| std::stack<AccessibilityInfoDataWrapper*> stack; |
| stack.push(node_info); |
| while (!stack.empty()) { |
| AccessibilityInfoDataWrapper* node = stack.top(); |
| stack.pop(); |
| DCHECK(node); |
| DCHECK(node->IsNode()); |
| static_cast<AccessibilityNodeInfoDataWrapper*>(node) |
| ->set_container_live_status(live_region_type); |
| |
| ui::AXNodeData data; |
| SerializeNode(node, &data); |
| std::string name; |
| data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name); |
| new_live_region_map[node->GetId()] = name; |
| |
| std::vector<int32_t> children; |
| if (GetProperty(node->GetNode()->int_list_properties, |
| AXIntListProperty::CHILD_NODE_IDS, &children)) { |
| for (const int32_t child : children) |
| stack.push(GetFromId(child)); |
| } |
| } |
| } |
| |
| // Compare to the previous one, and add an event if needed. |
| for (const auto& it : new_live_region_map) { |
| auto prev_it = previous_live_region_name_.find(it.first); |
| if (prev_it == previous_live_region_name_.end()) |
| continue; |
| |
| if (prev_it->second != it.second) { |
| events->emplace_back(); |
| ui::AXEvent& event = events->back(); |
| event.event_type = ax::mojom::Event::kLiveRegionChanged; |
| event.id = it.first; |
| } |
| } |
| |
| std::swap(previous_live_region_name_, new_live_region_map); |
| } |
| |
| void AXTreeSourceArc::Reset() { |
| tree_map_.clear(); |
| parent_map_.clear(); |
| computed_bounds_.clear(); |
| current_tree_serializer_.reset(new AXTreeArcSerializer(this)); |
| root_id_.reset(); |
| window_id_.reset(); |
| android_focused_id_.reset(); |
| extensions::AutomationEventRouterInterface* router = |
| GetAutomationEventRouter(); |
| if (!router) |
| return; |
| |
| router->DispatchTreeDestroyedEvent(ax_tree_id(), nullptr); |
| } |
| |
| int32_t AXTreeSourceArc::GetId(AccessibilityInfoDataWrapper* info_data) const { |
| if (!info_data) |
| return ui::AXNode::kInvalidAXID; |
| return info_data->GetId(); |
| } |
| |
| void AXTreeSourceArc::GetChildren( |
| AccessibilityInfoDataWrapper* info_data, |
| std::vector<AccessibilityInfoDataWrapper*>* out_children) const { |
| if (!info_data) |
| return; |
| |
| info_data->GetChildren(out_children); |
| if (out_children->empty()) |
| return; |
| |
| if (info_data->IsVirtualNode()) |
| return; |
| |
| std::map<int32_t, size_t> id_to_index; |
| for (size_t i = 0; i < out_children->size(); i++) { |
| if (out_children->at(i)->IsVirtualNode()) |
| return; |
| id_to_index[out_children->at(i)->GetId()] = i; |
| } |
| |
| // Sort children based on their enclosing bounding rectangles, based on their |
| // descendants. |
| std::sort( |
| out_children->begin(), out_children->end(), |
| [this, &id_to_index](auto left, auto right) { |
| auto left_bounds = ComputeEnclosingBounds(left); |
| auto right_bounds = ComputeEnclosingBounds(right); |
| |
| if (left_bounds.IsEmpty() || right_bounds.IsEmpty()) { |
| return id_to_index.at(left->GetId()) < id_to_index.at(right->GetId()); |
| } |
| |
| // Top to bottom sort (non-overlapping). |
| if (!left_bounds.Intersects(right_bounds)) |
| return left_bounds.y() < right_bounds.y(); |
| |
| // Overlapping |
| // Left to right. |
| int left_difference = left_bounds.x() - right_bounds.x(); |
| if (left_difference != 0) |
| return left_difference < 0; |
| |
| // Top to bottom. |
| int top_difference = left_bounds.y() - right_bounds.y(); |
| if (top_difference != 0) |
| return top_difference < 0; |
| |
| // Larger to smaller. |
| int height_difference = left_bounds.height() - right_bounds.height(); |
| if (height_difference != 0) |
| return height_difference > 0; |
| |
| int width_difference = left_bounds.width() - right_bounds.width(); |
| if (width_difference != 0) |
| return width_difference > 0; |
| |
| // The rects are equal. |
| return id_to_index.at(left->GetId()) < id_to_index.at(right->GetId()); |
| }); |
| } |
| |
| bool AXTreeSourceArc::IsIgnored(AccessibilityInfoDataWrapper* info_data) const { |
| return false; |
| } |
| |
| bool AXTreeSourceArc::IsValid(AccessibilityInfoDataWrapper* info_data) const { |
| return info_data; |
| } |
| |
| bool AXTreeSourceArc::IsEqual(AccessibilityInfoDataWrapper* info_data1, |
| AccessibilityInfoDataWrapper* info_data2) const { |
| if (!info_data1 || !info_data2) |
| return false; |
| return info_data1->GetId() == info_data2->GetId(); |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceArc::GetNull() const { |
| return nullptr; |
| } |
| |
| void AXTreeSourceArc::PerformAction(const ui::AXActionData& data) { |
| delegate_->OnAction(data); |
| } |
| |
| } // namespace arc |