| // Copyright 2013 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 "ax_tree.h" |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <numeric> |
| #include <utility> |
| |
| #include "ax_enums.h" |
| #include "ax_node.h" |
| #include "ax_node_position.h" |
| #include "ax_role_properties.h" |
| #include "ax_table_info.h" |
| #include "ax_tree_observer.h" |
| #include "base/auto_reset.h" |
| #include "base/string_utils.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| std::string TreeToStringHelper(const AXNode* node, int indent) { |
| if (!node) |
| return ""; |
| |
| return std::accumulate( |
| node->children().cbegin(), node->children().cend(), |
| std::string(2 * indent, ' ') + node->data().ToString() + "\n", |
| [indent](const std::string& str, const auto* child) { |
| return str + TreeToStringHelper(child, indent + 1); |
| }); |
| } |
| |
| template <typename K, typename V> |
| bool KeyValuePairsKeysMatch(std::vector<std::pair<K, V>> pairs1, |
| std::vector<std::pair<K, V>> pairs2) { |
| if (pairs1.size() != pairs2.size()) |
| return false; |
| for (size_t i = 0; i < pairs1.size(); ++i) { |
| if (pairs1[i].first != pairs2[i].first) |
| return false; |
| } |
| return true; |
| } |
| |
| template <typename K, typename V> |
| std::map<K, V> MapFromKeyValuePairs(std::vector<std::pair<K, V>> pairs) { |
| std::map<K, V> result; |
| for (size_t i = 0; i < pairs.size(); ++i) |
| result[pairs[i].first] = pairs[i].second; |
| return result; |
| } |
| |
| // Given two vectors of <K, V> key, value pairs representing an "old" vs "new" |
| // state, or "before" vs "after", calls a callback function for each key that |
| // changed value. Note that if an attribute is removed, that will result in |
| // a call to the callback with the value changing from the previous value to |
| // |empty_value|, and similarly when an attribute is added. |
| template <typename K, typename V, typename F> |
| void CallIfAttributeValuesChanged(const std::vector<std::pair<K, V>>& pairs1, |
| const std::vector<std::pair<K, V>>& pairs2, |
| const V& empty_value, |
| F callback) { |
| // Fast path - if they both have the same keys in the same order. |
| if (KeyValuePairsKeysMatch(pairs1, pairs2)) { |
| for (size_t i = 0; i < pairs1.size(); ++i) { |
| if (pairs1[i].second != pairs2[i].second) |
| callback(pairs1[i].first, pairs1[i].second, pairs2[i].second); |
| } |
| return; |
| } |
| |
| // Slower path - they don't have the same keys in the same order, so |
| // check all keys against each other, using maps to prevent this from |
| // becoming O(n^2) as the size grows. |
| auto map1 = MapFromKeyValuePairs(pairs1); |
| auto map2 = MapFromKeyValuePairs(pairs2); |
| for (size_t i = 0; i < pairs1.size(); ++i) { |
| const auto& new_iter = map2.find(pairs1[i].first); |
| if (pairs1[i].second != empty_value && new_iter == map2.end()) |
| callback(pairs1[i].first, pairs1[i].second, empty_value); |
| } |
| |
| for (size_t i = 0; i < pairs2.size(); ++i) { |
| const auto& iter = map1.find(pairs2[i].first); |
| if (iter == map1.end()) |
| callback(pairs2[i].first, empty_value, pairs2[i].second); |
| else if (iter->second != pairs2[i].second) |
| callback(pairs2[i].first, iter->second, pairs2[i].second); |
| } |
| } |
| |
| bool IsCollapsed(const AXNode* node) { |
| return node && node->data().HasState(ax::mojom::State::kCollapsed); |
| } |
| |
| } // namespace |
| |
| // This object is used to track structure changes that will occur for a specific |
| // AXID. This includes how many times we expect that a node with a specific AXID |
| // will be created and/or destroyed, and how many times a subtree rooted at AXID |
| // expects to be destroyed during an AXTreeUpdate. |
| // |
| // An AXTreeUpdate is a serialized representation of an atomic change to an |
| // AXTree. See also |AXTreeUpdate| which documents the nature and invariants |
| // required to atomically update the AXTree. |
| // |
| // The reason that we must track these counts, and the reason these are counts |
| // rather than a bool/flag is because an AXTreeUpdate may contain multiple |
| // AXNodeData updates for a given AXID. A common way that this occurs is when |
| // multiple AXTreeUpdates are merged together, combining their AXNodeData list. |
| // Additionally AXIDs may be reused after being removed from the tree, |
| // most notably when "reparenting" a node. A "reparent" occurs when an AXID is |
| // first destroyed from the tree then created again in the same AXTreeUpdate, |
| // which may also occur multiple times with merged updates. |
| // |
| // We need to accumulate these counts for 3 reasons : |
| // 1. To determine what structure changes *will* occur before applying |
| // updates to the tree so that we can notify observers of structure changes |
| // when the tree is still in a stable and unchanged state. |
| // 2. Capture any errors *before* applying updates to the tree structure |
| // due to the order of (or lack of) AXNodeData entries in the update |
| // so we can abort a bad update instead of applying it partway. |
| // 3. To validate that the expectations we accumulate actually match |
| // updates that are applied to the tree. |
| // |
| // To reiterate the invariants that this structure is taking a dependency on |
| // from |AXTreeUpdate|, suppose that the next AXNodeData to be applied is |
| // |node|. The following invariants must hold: |
| // 1. Either |
| // a) |node.id| is already in the tree, or |
| // b) the tree is empty, and |
| // |node| is the new root of the tree, and |
| // |node.role| == WebAXRoleRootWebArea. |
| // 2. Every child id in |node.child_ids| must either be already a child |
| // of this node, or a new id not previously in the tree. It is not |
| // allowed to "reparent" a child to this node without first removing |
| // that child from its previous parent. |
| // 3. When a new id appears in |node.child_ids|, the tree should create a |
| // new uninitialized placeholder node for it immediately. That |
| // placeholder must be updated within the same AXTreeUpdate, otherwise |
| // it's a fatal error. This guarantees the tree is always complete |
| // before or after an AXTreeUpdate. |
| struct PendingStructureChanges { |
| explicit PendingStructureChanges(const AXNode* node) |
| : destroy_subtree_count(0), |
| destroy_node_count(0), |
| create_node_count(0), |
| node_exists(!!node), |
| parent_node_id((node && node->parent()) |
| ? std::optional<AXNode::AXID>{node->parent()->id()} |
| : std::nullopt), |
| last_known_data(node ? &node->data() : nullptr) {} |
| |
| // Returns true if this node has any changes remaining. |
| // This includes pending subtree or node destruction, and node creation. |
| bool DoesNodeExpectAnyStructureChanges() const { |
| return DoesNodeExpectSubtreeWillBeDestroyed() || |
| DoesNodeExpectNodeWillBeDestroyed() || |
| DoesNodeExpectNodeWillBeCreated(); |
| } |
| |
| // Returns true if there are any pending changes that require destroying |
| // this node or its subtree. |
| bool DoesNodeExpectSubtreeOrNodeWillBeDestroyed() const { |
| return DoesNodeExpectSubtreeWillBeDestroyed() || |
| DoesNodeExpectNodeWillBeDestroyed(); |
| } |
| |
| // Returns true if the subtree rooted at this node needs to be destroyed |
| // during the update, but this may not be the next action that needs to be |
| // performed on the node. |
| bool DoesNodeExpectSubtreeWillBeDestroyed() const { |
| return destroy_subtree_count; |
| } |
| |
| // Returns true if this node needs to be destroyed during the update, but this |
| // may not be the next action that needs to be performed on the node. |
| bool DoesNodeExpectNodeWillBeDestroyed() const { return destroy_node_count; } |
| |
| // Returns true if this node needs be created during the update, but this |
| // may not be the next action that needs to be performed on the node. |
| bool DoesNodeExpectNodeWillBeCreated() const { return create_node_count; } |
| |
| // Returns true if this node would exist in the tree as of the last pending |
| // update that was processed, and the node has not been provided node data. |
| bool DoesNodeRequireInit() const { return node_exists && !last_known_data; } |
| |
| // Keep track of the number of times the subtree rooted at this node |
| // will be destroyed. |
| // An example of when this count may be larger than 1 is if updates were |
| // merged together. A subtree may be [created,] destroyed, created, and |
| // destroyed again within the same |AXTreeUpdate|. The important takeaway here |
| // is that an update may request destruction of a subtree rooted at an |
| // AXID more than once, not that a specific subtree is being destroyed |
| // more than once. |
| int32_t destroy_subtree_count; |
| |
| // Keep track of the number of times this node will be destroyed. |
| // An example of when this count may be larger than 1 is if updates were |
| // merged together. A node may be [created,] destroyed, created, and destroyed |
| // again within the same |AXTreeUpdate|. The important takeaway here is that |
| // an AXID may request destruction more than once, not that a specific node |
| // is being destroyed more than once. |
| int32_t destroy_node_count; |
| |
| // Keep track of the number of times this node will be created. |
| // An example of when this count may be larger than 1 is if updates were |
| // merged together. A node may be [destroyed,] created, destroyed, and created |
| // again within the same |AXTreeUpdate|. The important takeaway here is that |
| // an AXID may request creation more than once, not that a specific node is |
| // being created more than once. |
| int32_t create_node_count; |
| |
| // Keep track of whether this node exists in the tree as of the last pending |
| // update that was processed. |
| bool node_exists; |
| |
| // Keep track of the parent id for this node as of the last pending |
| // update that was processed. |
| std::optional<AXNode::AXID> parent_node_id; |
| |
| // Keep track of the last known node data for this node. |
| // This will be null either when a node does not exist in the tree, or |
| // when the node is new and has not been initialized with node data yet. |
| // This is needed to determine what children have changed between pending |
| // updates. |
| const AXNodeData* last_known_data; |
| }; |
| |
| // Represents the different states when computing PendingStructureChanges |
| // required for tree Unserialize. |
| enum class AXTreePendingStructureStatus { |
| // PendingStructureChanges have not begun computation. |
| kNotStarted, |
| // PendingStructureChanges are currently being computed. |
| kComputing, |
| // All PendingStructureChanges have successfully been computed. |
| kComplete, |
| // An error occurred when computing pending changes. |
| kFailed, |
| }; |
| |
| // Intermediate state to keep track of during a tree update. |
| struct AXTreeUpdateState { |
| explicit AXTreeUpdateState(const AXTree& tree) |
| : pending_update_status(AXTreePendingStructureStatus::kNotStarted), |
| root_will_be_created(false), |
| tree(tree) {} |
| |
| // Returns whether this update removes |node|. |
| bool IsRemovedNode(const AXNode* node) const { |
| return base::Contains(removed_node_ids, node->id()); |
| } |
| |
| // Returns whether this update creates a node marked by |node_id|. |
| bool IsCreatedNode(AXNode::AXID node_id) const { |
| return base::Contains(new_node_ids, node_id); |
| } |
| |
| // Returns whether this update creates |node|. |
| bool IsCreatedNode(const AXNode* node) const { |
| return IsCreatedNode(node->id()); |
| } |
| |
| // Returns whether this update reparents |node|. |
| bool IsReparentedNode(const AXNode* node) const { |
| if (AXTreePendingStructureStatus::kComplete != pending_update_status) { |
| BASE_LOG() |
| << "This method should not be called before pending changes have " |
| "finished computing."; |
| BASE_UNREACHABLE(); |
| } |
| PendingStructureChanges* data = GetPendingStructureChanges(node->id()); |
| if (!data) |
| return false; |
| // In order to know if the node will be reparented during the update, |
| // we check if either the node will be destroyed or has been destroyed at |
| // least once during the update. |
| // Since this method is only allowed to be called after calculating all |
| // pending structure changes, |node_exists| tells us if the node should |
| // exist after all updates have been applied. |
| return (data->DoesNodeExpectNodeWillBeDestroyed() || IsRemovedNode(node)) && |
| data->node_exists; |
| } |
| |
| // Returns true if the node should exist in the tree but doesn't have |
| // any node data yet. |
| bool DoesPendingNodeRequireInit(AXNode::AXID node_id) const { |
| if (AXTreePendingStructureStatus::kComputing != pending_update_status) { |
| BASE_LOG() << "This method should only be called while computing " |
| "pending changes, " |
| "before updates are made to the tree."; |
| BASE_UNREACHABLE(); |
| } |
| PendingStructureChanges* data = GetPendingStructureChanges(node_id); |
| return data && data->DoesNodeRequireInit(); |
| } |
| |
| // Returns the parent node id for the pending node. |
| std::optional<AXNode::AXID> GetParentIdForPendingNode(AXNode::AXID node_id) { |
| if (AXTreePendingStructureStatus::kComputing != pending_update_status) { |
| BASE_LOG() << "This method should only be called while computing " |
| "pending changes, " |
| "before updates are made to the tree."; |
| BASE_UNREACHABLE(); |
| } |
| PendingStructureChanges* data = GetOrCreatePendingStructureChanges(node_id); |
| BASE_DCHECK(!data->parent_node_id || |
| ShouldPendingNodeExistInTree(*data->parent_node_id)); |
| return data->parent_node_id; |
| } |
| |
| // Returns true if this node should exist in the tree. |
| bool ShouldPendingNodeExistInTree(AXNode::AXID node_id) { |
| if (AXTreePendingStructureStatus::kComputing != pending_update_status) { |
| BASE_LOG() << "This method should only be called while computing " |
| "pending changes, " |
| "before updates are made to the tree."; |
| BASE_UNREACHABLE(); |
| } |
| return GetOrCreatePendingStructureChanges(node_id)->node_exists; |
| } |
| |
| // Returns the last known node data for a pending node. |
| const AXNodeData& GetLastKnownPendingNodeData(AXNode::AXID node_id) const { |
| if (AXTreePendingStructureStatus::kComputing != pending_update_status) { |
| BASE_LOG() << "This method should only be called while computing " |
| "pending changes, " |
| "before updates are made to the tree."; |
| BASE_UNREACHABLE(); |
| } |
| static base::NoDestructor<ui::AXNodeData> empty_data; |
| PendingStructureChanges* data = GetPendingStructureChanges(node_id); |
| return (data && data->last_known_data) ? *data->last_known_data |
| : *empty_data; |
| } |
| |
| // Clear the last known pending data for |node_id|. |
| void ClearLastKnownPendingNodeData(AXNode::AXID node_id) { |
| if (AXTreePendingStructureStatus::kComputing != pending_update_status) { |
| BASE_LOG() << "This method should only be called while computing " |
| "pending changes, " |
| "before updates are made to the tree."; |
| BASE_UNREACHABLE(); |
| } |
| GetOrCreatePendingStructureChanges(node_id)->last_known_data = nullptr; |
| } |
| |
| // Update the last known pending node data for |node_data.id|. |
| void SetLastKnownPendingNodeData(const AXNodeData* node_data) { |
| if (AXTreePendingStructureStatus::kComputing != pending_update_status) { |
| BASE_LOG() << "This method should only be called while computing " |
| "pending changes, " |
| "before updates are made to the tree."; |
| BASE_UNREACHABLE(); |
| } |
| GetOrCreatePendingStructureChanges(node_data->id)->last_known_data = |
| node_data; |
| } |
| |
| // Returns the number of times the update is expected to destroy a |
| // subtree rooted at |node_id|. |
| int32_t GetPendingDestroySubtreeCount(AXNode::AXID node_id) const { |
| if (AXTreePendingStructureStatus::kComplete != pending_update_status) { |
| BASE_LOG() |
| << "This method should not be called before pending changes have " |
| "finished computing."; |
| BASE_UNREACHABLE(); |
| } |
| if (PendingStructureChanges* data = GetPendingStructureChanges(node_id)) |
| return data->destroy_subtree_count; |
| return 0; |
| } |
| |
| // Increments the number of times the update is expected to |
| // destroy a subtree rooted at |node_id|. |
| // Returns true on success, false on failure when the node will not exist. |
| bool IncrementPendingDestroySubtreeCount(AXNode::AXID node_id) { |
| if (AXTreePendingStructureStatus::kComputing != pending_update_status) { |
| BASE_LOG() << "This method should only be called while computing " |
| "pending changes, " |
| "before updates are made to the tree."; |
| BASE_UNREACHABLE(); |
| } |
| PendingStructureChanges* data = GetOrCreatePendingStructureChanges(node_id); |
| if (!data->node_exists) |
| return false; |
| |
| ++data->destroy_subtree_count; |
| return true; |
| } |
| |
| // Decrements the number of times the update is expected to |
| // destroy a subtree rooted at |node_id|. |
| void DecrementPendingDestroySubtreeCount(AXNode::AXID node_id) { |
| if (AXTreePendingStructureStatus::kComplete != pending_update_status) { |
| BASE_LOG() |
| << "This method should not be called before pending changes have " |
| "finished computing."; |
| BASE_UNREACHABLE(); |
| } |
| if (PendingStructureChanges* data = GetPendingStructureChanges(node_id)) { |
| BASE_DCHECK(data->destroy_subtree_count > 0); |
| --data->destroy_subtree_count; |
| } |
| } |
| |
| // Returns the number of times the update is expected to destroy |
| // a node with |node_id|. |
| int32_t GetPendingDestroyNodeCount(AXNode::AXID node_id) const { |
| if (AXTreePendingStructureStatus::kComplete != pending_update_status) { |
| BASE_LOG() |
| << "This method should not be called before pending changes have " |
| "finished computing."; |
| BASE_UNREACHABLE(); |
| } |
| if (PendingStructureChanges* data = GetPendingStructureChanges(node_id)) |
| return data->destroy_node_count; |
| return 0; |
| } |
| |
| // Increments the number of times the update is expected to |
| // destroy a node with |node_id|. |
| // Returns true on success, false on failure when the node will not exist. |
| bool IncrementPendingDestroyNodeCount(AXNode::AXID node_id) { |
| if (AXTreePendingStructureStatus::kComputing != pending_update_status) { |
| BASE_LOG() << "This method should only be called while computing " |
| "pending changes, " |
| "before updates are made to the tree."; |
| BASE_UNREACHABLE(); |
| } |
| PendingStructureChanges* data = GetOrCreatePendingStructureChanges(node_id); |
| if (!data->node_exists) |
| return false; |
| |
| ++data->destroy_node_count; |
| data->node_exists = false; |
| data->last_known_data = nullptr; |
| data->parent_node_id = std::nullopt; |
| if (pending_root_id == node_id) |
| pending_root_id = std::nullopt; |
| return true; |
| } |
| |
| // Decrements the number of times the update is expected to |
| // destroy a node with |node_id|. |
| void DecrementPendingDestroyNodeCount(AXNode::AXID node_id) { |
| if (AXTreePendingStructureStatus::kComplete != pending_update_status) { |
| BASE_LOG() |
| << "This method should not be called before pending changes have " |
| "finished computing."; |
| BASE_UNREACHABLE(); |
| } |
| if (PendingStructureChanges* data = GetPendingStructureChanges(node_id)) { |
| BASE_DCHECK(data->destroy_node_count > 0); |
| --data->destroy_node_count; |
| } |
| } |
| |
| // Returns the number of times the update is expected to create |
| // a node with |node_id|. |
| int32_t GetPendingCreateNodeCount(AXNode::AXID node_id) const { |
| if (AXTreePendingStructureStatus::kComplete != pending_update_status) { |
| BASE_LOG() |
| << "This method should not be called before pending changes have " |
| "finished computing."; |
| BASE_UNREACHABLE(); |
| } |
| if (PendingStructureChanges* data = GetPendingStructureChanges(node_id)) |
| return data->create_node_count; |
| return 0; |
| } |
| |
| // Increments the number of times the update is expected to |
| // create a node with |node_id|. |
| // Returns true on success, false on failure when the node will already exist. |
| bool IncrementPendingCreateNodeCount( |
| AXNode::AXID node_id, |
| std::optional<AXNode::AXID> parent_node_id) { |
| if (AXTreePendingStructureStatus::kComputing != pending_update_status) { |
| BASE_LOG() << "This method should only be called while computing " |
| "pending changes, " |
| "before updates are made to the tree."; |
| BASE_UNREACHABLE(); |
| } |
| PendingStructureChanges* data = GetOrCreatePendingStructureChanges(node_id); |
| if (data->node_exists) |
| return false; |
| |
| ++data->create_node_count; |
| data->node_exists = true; |
| data->parent_node_id = parent_node_id; |
| return true; |
| } |
| |
| // Decrements the number of times the update is expected to |
| // create a node with |node_id|. |
| void DecrementPendingCreateNodeCount(AXNode::AXID node_id) { |
| if (AXTreePendingStructureStatus::kComplete != pending_update_status) { |
| BASE_LOG() |
| << "This method should not be called before pending changes have " |
| "finished computing."; |
| BASE_UNREACHABLE(); |
| } |
| if (PendingStructureChanges* data = GetPendingStructureChanges(node_id)) { |
| BASE_DCHECK(data->create_node_count > 0); |
| --data->create_node_count; |
| } |
| } |
| |
| // Returns whether this update must invalidate the unignored cached |
| // values for |node_id|. |
| bool InvalidatesUnignoredCachedValues(AXNode::AXID node_id) { |
| return base::Contains(invalidate_unignored_cached_values_ids, node_id); |
| } |
| |
| // Adds the parent of |node_id| to the list of nodes to invalidate unignored |
| // cached values. |
| void InvalidateParentNodeUnignoredCacheValues(AXNode::AXID node_id) { |
| if (AXTreePendingStructureStatus::kComputing != pending_update_status) { |
| BASE_LOG() << "This method should only be called while computing " |
| "pending changes, " |
| "before updates are made to the tree."; |
| BASE_UNREACHABLE(); |
| } |
| std::optional<AXNode::AXID> parent_node_id = |
| GetParentIdForPendingNode(node_id); |
| if (parent_node_id) { |
| invalidate_unignored_cached_values_ids.insert(*parent_node_id); |
| } |
| } |
| |
| // Indicates the status for calculating what changes will occur during |
| // an update before the update applies changes. |
| AXTreePendingStructureStatus pending_update_status; |
| |
| // Keeps track of the root node id when calculating what changes will occur |
| // during an update before the update applies changes. |
| std::optional<AXNode::AXID> pending_root_id; |
| |
| // Keeps track of whether the root node will need to be created as a new node. |
| // This may occur either when the root node does not exist before applying |
| // updates to the tree (new tree), or if the root is the |node_id_to_clear| |
| // and will be destroyed before applying AXNodeData updates to the tree. |
| bool root_will_be_created; |
| |
| // During an update, this keeps track of all nodes that have been |
| // implicitly referenced as part of this update, but haven't been |
| // updated yet. It's an error if there are any pending nodes at the |
| // end of Unserialize. |
| std::set<AXNode::AXID> pending_nodes; |
| |
| // Keeps track of nodes whose cached unignored child count, or unignored |
| // index in parent may have changed, and must be updated. |
| std::set<AXNode::AXID> invalidate_unignored_cached_values_ids; |
| |
| // Keeps track of nodes that have changed their node data. |
| std::set<AXNode::AXID> node_data_changed_ids; |
| |
| // Keeps track of new nodes created during this update. |
| std::set<AXNode::AXID> new_node_ids; |
| |
| // Keeps track of any nodes removed. Nodes are removed when their AXID no |
| // longer exist in the parent |child_ids| list, or the node is part of to the |
| // subtree of the AXID that was explicitally cleared with |node_id_to_clear|. |
| // Used to identify re-parented nodes. A re-parented occurs when any AXID |
| // is first removed from the tree then added to the tree again. |
| std::set<AXNode::AXID> removed_node_ids; |
| |
| // Maps between a node id and its pending update information. |
| std::map<AXNode::AXID, std::unique_ptr<PendingStructureChanges>> |
| node_id_to_pending_data; |
| |
| // Maps between a node id and the data it owned before being updated. |
| // We need to keep this around in order to correctly fire post-update events. |
| std::map<AXNode::AXID, AXNodeData> old_node_id_to_data; |
| |
| // Optional copy of the old tree data, only populated when the tree |
| // data has changed. |
| std::optional<AXTreeData> old_tree_data; |
| |
| private: |
| PendingStructureChanges* GetPendingStructureChanges( |
| AXNode::AXID node_id) const { |
| auto iter = node_id_to_pending_data.find(node_id); |
| return (iter != node_id_to_pending_data.cend()) ? iter->second.get() |
| : nullptr; |
| } |
| |
| PendingStructureChanges* GetOrCreatePendingStructureChanges( |
| AXNode::AXID node_id) { |
| auto iter = node_id_to_pending_data.find(node_id); |
| if (iter == node_id_to_pending_data.cend()) { |
| const AXNode* node = tree.GetFromId(node_id); |
| iter = node_id_to_pending_data |
| .emplace(std::make_pair( |
| node_id, std::make_unique<PendingStructureChanges>(node))) |
| .first; |
| } |
| return iter->second.get(); |
| } |
| |
| // We need to hold onto a reference to the AXTree so that we can |
| // lazily initialize |PendingStructureChanges| objects. |
| const AXTree& tree; |
| }; |
| |
| AXTree::NodeSetSizePosInSetInfo::NodeSetSizePosInSetInfo() = default; |
| AXTree::NodeSetSizePosInSetInfo::~NodeSetSizePosInSetInfo() = default; |
| |
| struct AXTree::OrderedSetContent { |
| explicit OrderedSetContent(const AXNode* ordered_set = nullptr) |
| : ordered_set_(ordered_set) {} |
| ~OrderedSetContent() = default; |
| |
| std::vector<const AXNode*> set_items_; |
| |
| // Some ordered set items may not be associated with an ordered set. |
| const AXNode* ordered_set_; |
| }; |
| |
| struct AXTree::OrderedSetItemsMap { |
| OrderedSetItemsMap() = default; |
| ~OrderedSetItemsMap() = default; |
| |
| // Check if a particular hierarchical level exists in this map. |
| bool HierarchicalLevelExists(std::optional<int> level) { |
| if (items_map_.find(level) == items_map_.end()) |
| return false; |
| return true; |
| } |
| |
| // Add the OrderedSetContent to the corresponding hierarchical level in the |
| // map. |
| void Add(std::optional<int> level, |
| const OrderedSetContent& ordered_set_content) { |
| if (!HierarchicalLevelExists(level)) |
| items_map_[level] = std::vector<OrderedSetContent>(); |
| |
| items_map_[level].push_back(ordered_set_content); |
| } |
| |
| // Add an ordered set item to the OrderedSetItemsMap given its hierarchical |
| // level. We always want to append the item to the last OrderedSetContent of |
| // that hierarchical level, due to the following: |
| // - The last OrderedSetContent on any level of the items map is in progress |
| // of being populated. |
| // - All other OrderedSetContent other than the last one on a level |
| // represents a complete ordered set and should not be modified. |
| void AddItemToBack(std::optional<int> level, const AXNode* item) { |
| if (!HierarchicalLevelExists(level)) |
| return; |
| |
| std::vector<OrderedSetContent>& sets_list = items_map_[level]; |
| if (!sets_list.empty()) { |
| OrderedSetContent& ordered_set_content = sets_list.back(); |
| ordered_set_content.set_items_.push_back(item); |
| } |
| } |
| |
| // Retrieve the first OrderedSetContent of the OrderedSetItemsMap. |
| OrderedSetContent* GetFirstOrderedSetContent() { |
| if (items_map_.empty()) |
| return nullptr; |
| |
| std::vector<OrderedSetContent>& sets_list = items_map_.begin()->second; |
| if (sets_list.empty()) |
| return nullptr; |
| |
| return &(sets_list.front()); |
| } |
| |
| // Clears all the content in the map. |
| void Clear() { items_map_.clear(); } |
| |
| // Maps a hierarchical level to a list of OrderedSetContent. |
| std::map<std::optional<int32_t>, std::vector<OrderedSetContent>> items_map_; |
| }; |
| |
| AXTree::AXTree() { |
| AXNodeData root; |
| root.id = AXNode::kInvalidAXID; |
| |
| AXTreeUpdate initial_state; |
| initial_state.root_id = AXNode::kInvalidAXID; |
| initial_state.nodes.push_back(root); |
| if (!Unserialize(initial_state)) { |
| BASE_LOG() << error(); |
| BASE_UNREACHABLE(); |
| } |
| } |
| |
| AXTree::AXTree(const AXTreeUpdate& initial_state) { |
| if (!Unserialize(initial_state)) { |
| BASE_LOG() << error(); |
| BASE_UNREACHABLE(); |
| } |
| } |
| |
| AXTree::~AXTree() { |
| Destroy(); |
| } |
| |
| void AXTree::AddObserver(AXTreeObserver* observer) { |
| observers_.push_back(observer); |
| } |
| |
| bool AXTree::HasObserver(AXTreeObserver* observer) { |
| return std::find(observers_.begin(), observers_.end(), observer) != |
| observers_.end(); |
| } |
| |
| void AXTree::RemoveObserver(AXTreeObserver* observer) { |
| const auto it = std::find(observers_.begin(), observers_.end(), observer); |
| if (it == observers_.end()) |
| return; |
| observers_.erase(it); |
| } |
| |
| AXTreeID AXTree::GetAXTreeID() const { |
| return data().tree_id; |
| } |
| |
| AXNode* AXTree::GetFromId(int32_t id) const { |
| auto iter = id_map_.find(id); |
| return iter != id_map_.end() ? iter->second : nullptr; |
| } |
| |
| void AXTree::Destroy() { |
| table_info_map_.clear(); |
| if (root_) { |
| RecursivelyNotifyNodeDeletedForTreeTeardown(root_); |
| base::AutoReset<bool> update_state_resetter(&tree_update_in_progress_, |
| true); |
| DestroyNodeAndSubtree(root_, nullptr); |
| root_ = nullptr; |
| } |
| } |
| |
| void AXTree::UpdateData(const AXTreeData& new_data) { |
| if (data_ == new_data) |
| return; |
| |
| AXTreeData old_data = data_; |
| data_ = new_data; |
| for (AXTreeObserver* observer : observers_) |
| observer->OnTreeDataChanged(this, old_data, new_data); |
| } |
| |
| gfx::RectF AXTree::RelativeToTreeBoundsInternal(const AXNode* node, |
| gfx::RectF bounds, |
| bool* offscreen, |
| bool clip_bounds, |
| bool allow_recursion) const { |
| // If |bounds| is uninitialized, which is not the same as empty, |
| // start with the node bounds. |
| if (bounds.width() == 0 && bounds.height() == 0) { |
| bounds = node->data().relative_bounds.bounds; |
| |
| // If the node bounds is empty (either width or height is zero), |
| // try to compute good bounds from the children. |
| // If a tree update is in progress, skip this step as children may be in a |
| // bad state. |
| if (bounds.IsEmpty() && !GetTreeUpdateInProgressState() && |
| allow_recursion) { |
| for (size_t i = 0; i < node->children().size(); i++) { |
| ui::AXNode* child = node->children()[i]; |
| |
| bool ignore_offscreen; |
| gfx::RectF child_bounds = RelativeToTreeBoundsInternal( |
| child, gfx::RectF(), &ignore_offscreen, clip_bounds, |
| /* allow_recursion = */ false); |
| bounds.Union(child_bounds); |
| } |
| if (bounds.width() > 0 && bounds.height() > 0) { |
| return bounds; |
| } |
| } |
| } else { |
| bounds.Offset(node->data().relative_bounds.bounds.x(), |
| node->data().relative_bounds.bounds.y()); |
| } |
| |
| const AXNode* original_node = node; |
| while (node != nullptr) { |
| if (node->data().relative_bounds.transform) |
| node->data().relative_bounds.transform->TransformRect(&bounds); |
| // Apply any transforms and offsets for each node and then walk up to |
| // its offset container. If no offset container is specified, coordinates |
| // are relative to the root node. |
| const AXNode* container = |
| GetFromId(node->data().relative_bounds.offset_container_id); |
| if (!container && container != root()) |
| container = root(); |
| if (!container || container == node) |
| break; |
| |
| gfx::RectF container_bounds = container->data().relative_bounds.bounds; |
| bounds.Offset(container_bounds.x(), container_bounds.y()); |
| |
| int scroll_x = 0; |
| int scroll_y = 0; |
| if (container->data().GetIntAttribute(ax::mojom::IntAttribute::kScrollX, |
| &scroll_x) && |
| container->data().GetIntAttribute(ax::mojom::IntAttribute::kScrollY, |
| &scroll_y)) { |
| bounds.Offset(-scroll_x, -scroll_y); |
| } |
| |
| // Get the intersection between the bounds and the container. |
| gfx::RectF intersection = bounds; |
| intersection.Intersect(container_bounds); |
| |
| // Calculate the clipped bounds to determine offscreen state. |
| gfx::RectF clipped = bounds; |
| // If this node has the kClipsChildren attribute set, clip the rect to fit. |
| if (container->data().GetBoolAttribute( |
| ax::mojom::BoolAttribute::kClipsChildren)) { |
| if (!intersection.IsEmpty()) { |
| // We can simply clip it to the container. |
| clipped = intersection; |
| } else { |
| // Totally offscreen. Find the nearest edge or corner. |
| // Make the minimum dimension 1 instead of 0. |
| if (clipped.x() >= container_bounds.width()) { |
| clipped.set_x(container_bounds.right() - 1); |
| clipped.set_width(1); |
| } else if (clipped.x() + clipped.width() <= 0) { |
| clipped.set_x(container_bounds.x()); |
| clipped.set_width(1); |
| } |
| if (clipped.y() >= container_bounds.height()) { |
| clipped.set_y(container_bounds.bottom() - 1); |
| clipped.set_height(1); |
| } else if (clipped.y() + clipped.height() <= 0) { |
| clipped.set_y(container_bounds.y()); |
| clipped.set_height(1); |
| } |
| } |
| } |
| |
| if (clip_bounds) |
| bounds = clipped; |
| |
| if (container->data().GetBoolAttribute( |
| ax::mojom::BoolAttribute::kClipsChildren) && |
| intersection.IsEmpty() && !clipped.IsEmpty()) { |
| // If it is offscreen with respect to its parent, and the node itself is |
| // not empty, label it offscreen. |
| // Here we are extending the definition of offscreen to include elements |
| // that are clipped by their parents in addition to those clipped by |
| // the rootWebArea. |
| // No need to update |offscreen| if |intersection| is not empty, because |
| // it should be false by default. |
| if (offscreen != nullptr) |
| *offscreen |= true; |
| } |
| |
| node = container; |
| } |
| |
| // If we don't have any size yet, try to adjust the bounds to fill the |
| // nearest ancestor that does have bounds. |
| // |
| // The rationale is that it's not useful to the user for an object to |
| // have no width or height and it's probably a bug; it's better to |
| // reflect the bounds of the nearest ancestor rather than a 0x0 box. |
| // Tag this node as 'offscreen' because it has no true size, just a |
| // size inherited from the ancestor. |
| if (bounds.width() == 0 && bounds.height() == 0) { |
| const AXNode* ancestor = original_node->parent(); |
| gfx::RectF ancestor_bounds; |
| while (ancestor) { |
| ancestor_bounds = ancestor->data().relative_bounds.bounds; |
| if (ancestor_bounds.width() > 0 || ancestor_bounds.height() > 0) |
| break; |
| ancestor = ancestor->parent(); |
| } |
| |
| if (ancestor && allow_recursion) { |
| bool ignore_offscreen; |
| bool allow_recursion = false; |
| ancestor_bounds = RelativeToTreeBoundsInternal( |
| ancestor, gfx::RectF(), &ignore_offscreen, clip_bounds, |
| allow_recursion); |
| |
| gfx::RectF original_bounds = original_node->data().relative_bounds.bounds; |
| if (original_bounds.x() == 0 && original_bounds.y() == 0) { |
| bounds = ancestor_bounds; |
| } else { |
| bounds.set_width(std::max(0.0f, ancestor_bounds.right() - bounds.x())); |
| bounds.set_height( |
| std::max(0.0f, ancestor_bounds.bottom() - bounds.y())); |
| } |
| if (offscreen != nullptr) |
| *offscreen |= true; |
| } |
| } |
| |
| return bounds; |
| } |
| |
| gfx::RectF AXTree::RelativeToTreeBounds(const AXNode* node, |
| gfx::RectF bounds, |
| bool* offscreen, |
| bool clip_bounds) const { |
| bool allow_recursion = true; |
| return RelativeToTreeBoundsInternal(node, bounds, offscreen, clip_bounds, |
| allow_recursion); |
| } |
| |
| gfx::RectF AXTree::GetTreeBounds(const AXNode* node, |
| bool* offscreen, |
| bool clip_bounds) const { |
| return RelativeToTreeBounds(node, gfx::RectF(), offscreen, clip_bounds); |
| } |
| |
| std::set<int32_t> AXTree::GetReverseRelations(ax::mojom::IntAttribute attr, |
| int32_t dst_id) const { |
| BASE_DCHECK(IsNodeIdIntAttribute(attr)); |
| |
| // Conceptually, this is the "const" version of: |
| // return int_reverse_relations_[attr][dst_id]; |
| const auto& attr_relations = int_reverse_relations_.find(attr); |
| if (attr_relations != int_reverse_relations_.end()) { |
| const auto& result = attr_relations->second.find(dst_id); |
| if (result != attr_relations->second.end()) |
| return result->second; |
| } |
| return std::set<int32_t>(); |
| } |
| |
| std::set<int32_t> AXTree::GetReverseRelations(ax::mojom::IntListAttribute attr, |
| int32_t dst_id) const { |
| BASE_DCHECK(IsNodeIdIntListAttribute(attr)); |
| |
| // Conceptually, this is the "const" version of: |
| // return intlist_reverse_relations_[attr][dst_id]; |
| const auto& attr_relations = intlist_reverse_relations_.find(attr); |
| if (attr_relations != intlist_reverse_relations_.end()) { |
| const auto& result = attr_relations->second.find(dst_id); |
| if (result != attr_relations->second.end()) |
| return result->second; |
| } |
| return std::set<int32_t>(); |
| } |
| |
| std::set<int32_t> AXTree::GetNodeIdsForChildTreeId( |
| AXTreeID child_tree_id) const { |
| // Conceptually, this is the "const" version of: |
| // return child_tree_id_reverse_map_[child_tree_id]; |
| const auto& result = child_tree_id_reverse_map_.find(child_tree_id); |
| if (result != child_tree_id_reverse_map_.end()) |
| return result->second; |
| return std::set<int32_t>(); |
| } |
| |
| const std::set<AXTreeID> AXTree::GetAllChildTreeIds() const { |
| std::set<AXTreeID> result; |
| for (auto entry : child_tree_id_reverse_map_) |
| result.insert(entry.first); |
| return result; |
| } |
| |
| bool AXTree::Unserialize(const AXTreeUpdate& update) { |
| AXTreeUpdateState update_state(*this); |
| const AXNode::AXID old_root_id = root_ ? root_->id() : AXNode::kInvalidAXID; |
| |
| // Accumulates the work that will be required to update the AXTree. |
| // This allows us to notify observers of structure changes when the |
| // tree is still in a stable and unchanged state. |
| if (!ComputePendingChanges(update, &update_state)) |
| return false; |
| |
| // Notify observers of subtrees and nodes that are about to be destroyed or |
| // reparented, this must be done before applying any updates to the tree. |
| for (auto&& pair : update_state.node_id_to_pending_data) { |
| const AXNode::AXID node_id = pair.first; |
| const std::unique_ptr<PendingStructureChanges>& data = pair.second; |
| if (data->DoesNodeExpectSubtreeOrNodeWillBeDestroyed()) { |
| if (AXNode* node = GetFromId(node_id)) { |
| if (data->DoesNodeExpectSubtreeWillBeDestroyed()) |
| NotifySubtreeWillBeReparentedOrDeleted(node, &update_state); |
| if (data->DoesNodeExpectNodeWillBeDestroyed()) |
| NotifyNodeWillBeReparentedOrDeleted(node, &update_state); |
| } |
| } |
| } |
| |
| // Notify observers of nodes that are about to change their data. |
| // This must be done before applying any updates to the tree. |
| // This is iterating in reverse order so that we only notify once per node id, |
| // and that we only notify the initial node data against the final node data, |
| // unless the node is a new root. |
| std::set<int32_t> notified_node_data_will_change; |
| for (size_t i = update.nodes.size(); i-- > 0;) { |
| const AXNodeData& new_data = update.nodes[i]; |
| const bool is_new_root = |
| update_state.root_will_be_created && new_data.id == update.root_id; |
| if (!is_new_root) { |
| AXNode* node = GetFromId(new_data.id); |
| if (node && notified_node_data_will_change.insert(new_data.id).second) |
| NotifyNodeDataWillChange(node->data(), new_data); |
| } |
| } |
| |
| // Now that we have finished sending events for changes that will happen, |
| // set update state to true. |tree_update_in_progress_| gets set back to |
| // false whenever this function exits. |
| base::AutoReset<bool> update_state_resetter(&tree_update_in_progress_, true); |
| |
| // Handle |node_id_to_clear| before applying ordinary node updates. |
| // We distinguish between updating the root, e.g. changing its children or |
| // some of its attributes, or replacing the root completely. If the root is |
| // being updated, update.node_id_to_clear should hold the current root's ID. |
| // Otherwise if the root is being replaced, update.root_id should hold the ID |
| // of the new root. |
| bool root_updated = false; |
| if (update.node_id_to_clear != AXNode::kInvalidAXID) { |
| if (AXNode* cleared_node = GetFromId(update.node_id_to_clear)) { |
| BASE_DCHECK(root_); |
| if (cleared_node == root_) { |
| // Only destroy the root if the root was replaced and not if it's simply |
| // updated. To figure out if the root was simply updated, we compare |
| // the ID of the new root with the existing root ID. |
| if (update.root_id != old_root_id) { |
| // Clear root_ before calling DestroySubtree so that root_ doesn't |
| // ever point to an invalid node. |
| AXNode* old_root = root_; |
| root_ = nullptr; |
| DestroySubtree(old_root, &update_state); |
| } else { |
| // If the root has simply been updated, we treat it like an update to |
| // any other node. |
| root_updated = true; |
| } |
| } |
| |
| // If the tree doesn't exists any more because the root has just been |
| // replaced, there is nothing more to clear. |
| if (root_) { |
| for (auto* child : cleared_node->children()) |
| DestroySubtree(child, &update_state); |
| std::vector<AXNode*> children; |
| cleared_node->SwapChildren(&children); |
| update_state.pending_nodes.insert(cleared_node->id()); |
| } |
| } |
| } |
| |
| BASE_DCHECK(!GetFromId(update.root_id) == update_state.root_will_be_created); |
| |
| // Update the tree data, do not call |UpdateData| since we want to defer |
| // the |OnTreeDataChanged| event until after the tree has finished updating. |
| if (update.has_tree_data && data_ != update.tree_data) { |
| update_state.old_tree_data = data_; |
| data_ = update.tree_data; |
| } |
| |
| // Update all of the nodes in the update. |
| for (size_t i = 0; i < update.nodes.size(); ++i) { |
| const bool is_new_root = update_state.root_will_be_created && |
| update.nodes[i].id == update.root_id; |
| if (!UpdateNode(update.nodes[i], is_new_root, &update_state)) |
| return false; |
| } |
| |
| if (!root_) { |
| error_ = "Tree has no root."; |
| return false; |
| } |
| |
| if (!ValidatePendingChangesComplete(update_state)) |
| return false; |
| |
| // Look for changes to nodes that are a descendant of a table, |
| // and invalidate their table info if so. We have to walk up the |
| // ancestry of every node that was updated potentially, so keep track of |
| // ids that were checked to eliminate duplicate work. |
| std::set<int32_t> table_ids_checked; |
| for (size_t i = 0; i < update.nodes.size(); ++i) { |
| AXNode* node = GetFromId(update.nodes[i].id); |
| while (node) { |
| if (table_ids_checked.find(node->id()) != table_ids_checked.end()) |
| break; |
| // Remove any table infos. |
| const auto& table_info_entry = table_info_map_.find(node->id()); |
| if (table_info_entry != table_info_map_.end()) |
| table_info_entry->second->Invalidate(); |
| table_ids_checked.insert(node->id()); |
| node = node->parent(); |
| } |
| } |
| |
| // Clears |node_set_size_pos_in_set_info_map_| |
| node_set_size_pos_in_set_info_map_.clear(); |
| |
| std::vector<AXTreeObserver::Change> changes; |
| changes.reserve(update.nodes.size()); |
| std::set<AXNode::AXID> visited_observer_changes; |
| for (size_t i = 0; i < update.nodes.size(); ++i) { |
| AXNode* node = GetFromId(update.nodes[i].id); |
| if (!node || !visited_observer_changes.emplace(update.nodes[i].id).second) |
| continue; |
| |
| bool is_new_node = update_state.IsCreatedNode(node); |
| bool is_reparented_node = update_state.IsReparentedNode(node); |
| |
| AXTreeObserver::ChangeType change = AXTreeObserver::NODE_CHANGED; |
| if (is_new_node) { |
| if (is_reparented_node) { |
| // A reparented subtree is any new node whose parent either doesn't |
| // exist, or whose parent is not new. |
| // Note that we also need to check for the special case when we update |
| // the root without replacing it. |
| bool is_subtree = !node->parent() || |
| !update_state.IsCreatedNode(node->parent()) || |
| (node->parent() == root_ && root_updated); |
| change = is_subtree ? AXTreeObserver::SUBTREE_REPARENTED |
| : AXTreeObserver::NODE_REPARENTED; |
| } else { |
| // A new subtree is any new node whose parent is either not new, or |
| // whose parent happens to be new only because it has been reparented. |
| // Note that we also need to check for the special case when we update |
| // the root without replacing it. |
| bool is_subtree = !node->parent() || |
| !update_state.IsCreatedNode(node->parent()) || |
| update_state.IsRemovedNode(node->parent()) || |
| (node->parent() == root_ && root_updated); |
| change = is_subtree ? AXTreeObserver::SUBTREE_CREATED |
| : AXTreeObserver::NODE_CREATED; |
| } |
| } |
| changes.push_back(AXTreeObserver::Change(node, change)); |
| } |
| |
| // Update the unignored cached values as necessary, ensuring that we only |
| // update once for each unignored node. |
| // If the node is ignored, we must update from an unignored ancestor. |
| std::set<AXNode::AXID> updated_unignored_cached_values_ids; |
| for (AXNode::AXID node_id : |
| update_state.invalidate_unignored_cached_values_ids) { |
| AXNode* node = GetFromId(node_id); |
| while (node && node->data().IsIgnored()) |
| node = node->parent(); |
| if (node && updated_unignored_cached_values_ids.insert(node->id()).second) |
| node->UpdateUnignoredCachedValues(); |
| } |
| |
| // Tree is no longer updating. |
| SetTreeUpdateInProgressState(false); |
| |
| // Now that the tree is stable and its nodes have been updated, notify if |
| // the tree data changed. We must do this after updating nodes in case the |
| // root has been replaced, so observers have the most up-to-date information. |
| if (update_state.old_tree_data) { |
| for (AXTreeObserver* observer : observers_) |
| observer->OnTreeDataChanged(this, *update_state.old_tree_data, data_); |
| } |
| |
| // Now that the unignored cached values are up to date, update observers to |
| // the nodes that were deleted from the tree but not reparented. |
| for (AXNode::AXID node_id : update_state.removed_node_ids) { |
| if (!update_state.IsCreatedNode(node_id)) |
| NotifyNodeHasBeenDeleted(node_id); |
| } |
| |
| // Now that the unignored cached values are up to date, update observers to |
| // new nodes in the tree. |
| for (AXNode::AXID node_id : update_state.new_node_ids) |
| NotifyNodeHasBeenReparentedOrCreated(GetFromId(node_id), &update_state); |
| |
| // Now that the unignored cached values are up to date, update observers to |
| // node changes. |
| for (AXNode::AXID node_data_changed_id : update_state.node_data_changed_ids) { |
| AXNode* node = GetFromId(node_data_changed_id); |
| BASE_DCHECK(node); |
| |
| // If the node exists and is in the old data map, then the node data |
| // may have changed unless this is a new root. |
| const bool is_new_root = update_state.root_will_be_created && |
| node_data_changed_id == update.root_id; |
| if (!is_new_root) { |
| auto it = update_state.old_node_id_to_data.find(node_data_changed_id); |
| if (it != update_state.old_node_id_to_data.end()) { |
| const AXNodeData& old_node_data = it->second; |
| NotifyNodeDataHasBeenChanged(node, old_node_data, node->data()); |
| } |
| } |
| |
| // |OnNodeChanged| should be fired for all nodes that have been updated. |
| for (AXTreeObserver* observer : observers_) |
| observer->OnNodeChanged(this, node); |
| } |
| |
| for (AXTreeObserver* observer : observers_) |
| observer->OnAtomicUpdateFinished(this, root_->id() != old_root_id, changes); |
| |
| return true; |
| } |
| |
| AXTableInfo* AXTree::GetTableInfo(const AXNode* const_table_node) const { |
| BASE_DCHECK(!GetTreeUpdateInProgressState()); |
| // Note: the const_casts are here because we want this function to be able |
| // to be called from a const virtual function on AXNode. AXTableInfo is |
| // computed on demand and cached, but that's an implementation detail |
| // we want to hide from users of this API. |
| AXNode* table_node = const_cast<AXNode*>(const_table_node); |
| AXTree* tree = const_cast<AXTree*>(this); |
| |
| BASE_DCHECK(table_node); |
| const auto& cached = table_info_map_.find(table_node->id()); |
| if (cached != table_info_map_.end()) { |
| // Get existing table info, and update if invalid because the |
| // tree has changed since the last time we accessed it. |
| AXTableInfo* table_info = cached->second.get(); |
| if (!table_info->valid()) { |
| if (!table_info->Update()) { |
| // If Update() returned false, this is no longer a valid table. |
| // Remove it from the map. |
| table_info_map_.erase(table_node->id()); |
| return nullptr; |
| } |
| } |
| return table_info; |
| } |
| |
| AXTableInfo* table_info = AXTableInfo::Create(tree, table_node); |
| if (!table_info) |
| return nullptr; |
| |
| table_info_map_[table_node->id()] = std::unique_ptr<AXTableInfo>(table_info); |
| return table_info; |
| } |
| |
| std::string AXTree::ToString() const { |
| return "AXTree" + data_.ToString() + "\n" + TreeToStringHelper(root_, 0); |
| } |
| |
| AXNode* AXTree::CreateNode(AXNode* parent, |
| AXNode::AXID id, |
| size_t index_in_parent, |
| AXTreeUpdateState* update_state) { |
| BASE_DCHECK(GetTreeUpdateInProgressState()); |
| // |update_state| must already contain information about all of the expected |
| // changes and invalidations to apply. If any of these are missing, observers |
| // may not be notified of changes. |
| BASE_DCHECK(!GetFromId(id)); |
| BASE_DCHECK(update_state->GetPendingCreateNodeCount(id) > 0); |
| BASE_DCHECK(update_state->InvalidatesUnignoredCachedValues(id)); |
| BASE_DCHECK(!parent || |
| update_state->InvalidatesUnignoredCachedValues(parent->id())); |
| update_state->DecrementPendingCreateNodeCount(id); |
| update_state->new_node_ids.insert(id); |
| // If this node is the root, use the given index_in_parent as the unignored |
| // index in parent to provide consistency with index_in_parent. |
| AXNode* new_node = new AXNode(this, parent, id, index_in_parent, |
| parent ? 0 : index_in_parent); |
| id_map_[new_node->id()] = new_node; |
| return new_node; |
| } |
| |
| bool AXTree::ComputePendingChanges(const AXTreeUpdate& update, |
| AXTreeUpdateState* update_state) { |
| if (AXTreePendingStructureStatus::kNotStarted != |
| update_state->pending_update_status) { |
| BASE_LOG() << "Pending changes have already started being computed."; |
| BASE_UNREACHABLE(); |
| } |
| update_state->pending_update_status = |
| AXTreePendingStructureStatus::kComputing; |
| |
| base::AutoReset<std::optional<AXNode::AXID>> pending_root_id_resetter( |
| &update_state->pending_root_id, |
| root_ ? std::optional<AXNode::AXID>{root_->id()} : std::nullopt); |
| |
| // We distinguish between updating the root, e.g. changing its children or |
| // some of its attributes, or replacing the root completely. If the root is |
| // being updated, update.node_id_to_clear should hold the current root's ID. |
| // Otherwise if the root is being replaced, update.root_id should hold the ID |
| // of the new root. |
| if (update.node_id_to_clear != AXNode::kInvalidAXID) { |
| if (AXNode* cleared_node = GetFromId(update.node_id_to_clear)) { |
| BASE_DCHECK(root_); |
| if (cleared_node == root_ && |
| update.root_id != update_state->pending_root_id) { |
| // Only destroy the root if the root was replaced and not if it's simply |
| // updated. To figure out if the root was simply updated, we compare |
| // the ID of the new root with the existing root ID. |
| MarkSubtreeForDestruction(*update_state->pending_root_id, update_state); |
| } |
| |
| // If the tree has been marked for destruction because the root will be |
| // replaced, there is nothing more to clear. |
| if (update_state->ShouldPendingNodeExistInTree(root_->id())) { |
| update_state->invalidate_unignored_cached_values_ids.insert( |
| cleared_node->id()); |
| update_state->ClearLastKnownPendingNodeData(cleared_node->id()); |
| for (AXNode* child : cleared_node->children()) { |
| MarkSubtreeForDestruction(child->id(), update_state); |
| } |
| } |
| } |
| } |
| |
| update_state->root_will_be_created = |
| !GetFromId(update.root_id) || |
| !update_state->ShouldPendingNodeExistInTree(update.root_id); |
| |
| // Populate |update_state| with all of the changes that will be performed |
| // on the tree during the update. |
| for (const AXNodeData& new_data : update.nodes) { |
| bool is_new_root = |
| update_state->root_will_be_created && new_data.id == update.root_id; |
| if (!ComputePendingChangesToNode(new_data, is_new_root, update_state)) { |
| update_state->pending_update_status = |
| AXTreePendingStructureStatus::kFailed; |
| return false; |
| } |
| } |
| |
| update_state->pending_update_status = AXTreePendingStructureStatus::kComplete; |
| return true; |
| } |
| |
| bool AXTree::ComputePendingChangesToNode(const AXNodeData& new_data, |
| bool is_new_root, |
| AXTreeUpdateState* update_state) { |
| // Compare every child's index in parent in the update with the existing |
| // index in parent. If the order has changed, invalidate the cached |
| // unignored index in parent. |
| for (size_t j = 0; j < new_data.child_ids.size(); j++) { |
| AXNode* node = GetFromId(new_data.child_ids[j]); |
| if (node && node->GetIndexInParent() != j) |
| update_state->InvalidateParentNodeUnignoredCacheValues(node->id()); |
| } |
| |
| // If the node does not exist in the tree throw an error unless this |
| // is the new root and it can be created. |
| if (!update_state->ShouldPendingNodeExistInTree(new_data.id)) { |
| if (!is_new_root) { |
| error_ = base::StringPrintf( |
| "%d will not be in the tree and is not the new root", new_data.id); |
| return false; |
| } |
| |
| // Creation is implicit for new root nodes. If |new_data.id| is already |
| // pending for creation, then it must be a duplicate entry in the tree. |
| if (!update_state->IncrementPendingCreateNodeCount(new_data.id, |
| std::nullopt)) { |
| error_ = base::StringPrintf( |
| "Node %d is already pending for creation, cannot be the new root", |
| new_data.id); |
| return false; |
| } |
| if (update_state->pending_root_id) { |
| MarkSubtreeForDestruction(*update_state->pending_root_id, update_state); |
| } |
| update_state->pending_root_id = new_data.id; |
| } |
| |
| // Create a set of new child ids so we can use it to find the nodes that |
| // have been added and removed. Returns false if a duplicate is found. |
| std::set<AXNode::AXID> new_child_id_set; |
| for (AXNode::AXID new_child_id : new_data.child_ids) { |
| if (base::Contains(new_child_id_set, new_child_id)) { |
| error_ = base::StringPrintf("Node %d has duplicate child id %d", |
| new_data.id, new_child_id); |
| return false; |
| } |
| new_child_id_set.insert(new_child_id); |
| } |
| |
| // If the node has not been initialized yet then its node data has either been |
| // cleared when handling |node_id_to_clear|, or it's a new node. |
| // In either case, all children must be created. |
| if (update_state->DoesPendingNodeRequireInit(new_data.id)) { |
| update_state->invalidate_unignored_cached_values_ids.insert(new_data.id); |
| |
| // If this node has been cleared via |node_id_to_clear| or is a new node, |
| // the last-known parent's unignored cache needs to be updated. |
| update_state->InvalidateParentNodeUnignoredCacheValues(new_data.id); |
| |
| for (AXNode::AXID child_id : new_child_id_set) { |
| // If a |child_id| is already pending for creation, then it must be a |
| // duplicate entry in the tree. |
| update_state->invalidate_unignored_cached_values_ids.insert(child_id); |
| if (!update_state->IncrementPendingCreateNodeCount(child_id, |
| new_data.id)) { |
| error_ = base::StringPrintf( |
| "Node %d is already pending for creation, cannot be a new child", |
| child_id); |
| return false; |
| } |
| } |
| |
| update_state->SetLastKnownPendingNodeData(&new_data); |
| return true; |
| } |
| |
| const AXNodeData& old_data = |
| update_state->GetLastKnownPendingNodeData(new_data.id); |
| |
| // Create a set of old child ids so we can use it to find the nodes that |
| // have been added and removed. |
| std::set<AXNode::AXID> old_child_id_set(old_data.child_ids.cbegin(), |
| old_data.child_ids.cend()); |
| |
| std::vector<AXNode::AXID> create_or_destroy_ids; |
| std::set_symmetric_difference( |
| old_child_id_set.cbegin(), old_child_id_set.cend(), |
| new_child_id_set.cbegin(), new_child_id_set.cend(), |
| std::back_inserter(create_or_destroy_ids)); |
| |
| // If the node has changed ignored state or there are any differences in |
| // its children, then its unignored cached values must be invalidated. |
| const bool ignored_changed = old_data.IsIgnored() != new_data.IsIgnored(); |
| if (!create_or_destroy_ids.empty() || ignored_changed) { |
| update_state->invalidate_unignored_cached_values_ids.insert(new_data.id); |
| |
| // If this ignored state had changed also invalidate the parent. |
| update_state->InvalidateParentNodeUnignoredCacheValues(new_data.id); |
| } |
| |
| for (AXNode::AXID child_id : create_or_destroy_ids) { |
| if (base::Contains(new_child_id_set, child_id)) { |
| // This is a serious error - nodes should never be reparented without |
| // first being removed from the tree. If a node exists in the tree already |
| // then adding it to a new parent would mean stealing the node from its |
| // old parent which hadn't been updated to reflect the change. |
| if (update_state->ShouldPendingNodeExistInTree(child_id)) { |
| error_ = base::StringPrintf( |
| "Node %d is not marked for destruction, would be reparented to %d", |
| child_id, new_data.id); |
| return false; |
| } |
| |
| // If a |child_id| is already pending for creation, then it must be a |
| // duplicate entry in the tree. |
| update_state->invalidate_unignored_cached_values_ids.insert(child_id); |
| if (!update_state->IncrementPendingCreateNodeCount(child_id, |
| new_data.id)) { |
| error_ = base::StringPrintf( |
| "Node %d is already pending for creation, cannot be a new child", |
| child_id); |
| return false; |
| } |
| } else { |
| // If |child_id| does not exist in the new set, then it has |
| // been removed from |node|, and the subtree must be deleted. |
| MarkSubtreeForDestruction(child_id, update_state); |
| } |
| } |
| |
| update_state->SetLastKnownPendingNodeData(&new_data); |
| return true; |
| } |
| |
| bool AXTree::UpdateNode(const AXNodeData& src, |
| bool is_new_root, |
| AXTreeUpdateState* update_state) { |
| BASE_DCHECK(GetTreeUpdateInProgressState()); |
| // This method updates one node in the tree based on serialized data |
| // received in an AXTreeUpdate. See AXTreeUpdate for pre and post |
| // conditions. |
| |
| // Look up the node by id. If it's not found, then either the root |
| // of the tree is being swapped, or we're out of sync with the source |
| // and this is a serious error. |
| AXNode* node = GetFromId(src.id); |
| if (node) { |
| update_state->pending_nodes.erase(node->id()); |
| UpdateReverseRelations(node, src); |
| if (!update_state->IsCreatedNode(node) || |
| update_state->IsReparentedNode(node)) { |
| update_state->old_node_id_to_data.insert( |
| std::make_pair(node->id(), node->TakeData())); |
| } |
| node->SetData(src); |
| } else { |
| if (!is_new_root) { |
| error_ = base::StringPrintf("%d is not in the tree and not the new root", |
| src.id); |
| return false; |
| } |
| |
| node = CreateNode(nullptr, src.id, 0, update_state); |
| UpdateReverseRelations(node, src); |
| node->SetData(src); |
| } |
| |
| // If we come across a page breaking object, mark the tree as a paginated root |
| if (src.GetBoolAttribute(ax::mojom::BoolAttribute::kIsPageBreakingObject)) |
| has_pagination_support_ = true; |
| |
| update_state->node_data_changed_ids.insert(node->id()); |
| |
| // First, delete nodes that used to be children of this node but aren't |
| // anymore. |
| DeleteOldChildren(node, src.child_ids, update_state); |
| |
| // Now build a new children vector, reusing nodes when possible, |
| // and swap it in. |
| std::vector<AXNode*> new_children; |
| bool success = |
| CreateNewChildVector(node, src.child_ids, &new_children, update_state); |
| node->SwapChildren(&new_children); |
| |
| // Update the root of the tree if needed. |
| if (is_new_root) { |
| // Make sure root_ always points to something valid or null_, even inside |
| // DestroySubtree. |
| AXNode* old_root = root_; |
| root_ = node; |
| if (old_root && old_root != node) |
| DestroySubtree(old_root, update_state); |
| } |
| |
| return success; |
| } |
| |
| void AXTree::NotifySubtreeWillBeReparentedOrDeleted( |
| AXNode* node, |
| const AXTreeUpdateState* update_state) { |
| BASE_DCHECK(!GetTreeUpdateInProgressState()); |
| if (node->id() == AXNode::kInvalidAXID) |
| return; |
| |
| for (AXTreeObserver* observer : observers_) { |
| if (update_state->IsReparentedNode(node)) { |
| observer->OnSubtreeWillBeReparented(this, node); |
| } else { |
| observer->OnSubtreeWillBeDeleted(this, node); |
| } |
| } |
| } |
| |
| void AXTree::NotifyNodeWillBeReparentedOrDeleted( |
| AXNode* node, |
| const AXTreeUpdateState* update_state) { |
| BASE_DCHECK(!GetTreeUpdateInProgressState()); |
| |
| AXNode::AXID id = node->id(); |
| if (id == AXNode::kInvalidAXID) |
| return; |
| |
| table_info_map_.erase(id); |
| |
| for (AXTreeObserver* observer : observers_) { |
| if (update_state->IsReparentedNode(node)) { |
| observer->OnNodeWillBeReparented(this, node); |
| } else { |
| observer->OnNodeWillBeDeleted(this, node); |
| } |
| } |
| |
| if (table_info_map_.find(id) != table_info_map_.end()) { |
| BASE_LOG() << "Table info should never be recreated during node deletion"; |
| BASE_UNREACHABLE(); |
| } |
| } |
| |
| void AXTree::RecursivelyNotifyNodeDeletedForTreeTeardown(AXNode* node) { |
| BASE_DCHECK(!GetTreeUpdateInProgressState()); |
| if (node->id() == AXNode::kInvalidAXID) |
| return; |
| |
| for (AXTreeObserver* observer : observers_) |
| observer->OnNodeDeleted(this, node->id()); |
| for (auto* child : node->children()) |
| RecursivelyNotifyNodeDeletedForTreeTeardown(child); |
| } |
| |
| void AXTree::NotifyNodeHasBeenDeleted(AXNode::AXID node_id) { |
| BASE_DCHECK(!GetTreeUpdateInProgressState()); |
| |
| if (node_id == AXNode::kInvalidAXID) |
| return; |
| |
| for (AXTreeObserver* observer : observers_) |
| observer->OnNodeDeleted(this, node_id); |
| } |
| |
| void AXTree::NotifyNodeHasBeenReparentedOrCreated( |
| AXNode* node, |
| const AXTreeUpdateState* update_state) { |
| BASE_DCHECK(!GetTreeUpdateInProgressState()); |
| if (node->id() == AXNode::kInvalidAXID) |
| return; |
| |
| for (AXTreeObserver* observer : observers_) { |
| if (update_state->IsReparentedNode(node)) { |
| observer->OnNodeReparented(this, node); |
| } else { |
| observer->OnNodeCreated(this, node); |
| } |
| } |
| } |
| |
| void AXTree::NotifyNodeDataWillChange(const AXNodeData& old_data, |
| const AXNodeData& new_data) { |
| BASE_DCHECK(!GetTreeUpdateInProgressState()); |
| if (new_data.id == AXNode::kInvalidAXID) |
| return; |
| |
| for (AXTreeObserver* observer : observers_) |
| observer->OnNodeDataWillChange(this, old_data, new_data); |
| } |
| |
| void AXTree::NotifyNodeDataHasBeenChanged(AXNode* node, |
| const AXNodeData& old_data, |
| const AXNodeData& new_data) { |
| BASE_DCHECK(!GetTreeUpdateInProgressState()); |
| if (node->id() == AXNode::kInvalidAXID) |
| return; |
| |
| for (AXTreeObserver* observer : observers_) |
| observer->OnNodeDataChanged(this, old_data, new_data); |
| |
| if (old_data.role != new_data.role) { |
| for (AXTreeObserver* observer : observers_) |
| observer->OnRoleChanged(this, node, old_data.role, new_data.role); |
| } |
| |
| if (old_data.state != new_data.state) { |
| for (int32_t i = static_cast<int32_t>(ax::mojom::State::kNone) + 1; |
| i <= static_cast<int32_t>(ax::mojom::State::kMaxValue); ++i) { |
| ax::mojom::State state = static_cast<ax::mojom::State>(i); |
| if (old_data.HasState(state) != new_data.HasState(state)) { |
| for (AXTreeObserver* observer : observers_) |
| observer->OnStateChanged(this, node, state, new_data.HasState(state)); |
| } |
| } |
| } |
| |
| auto string_callback = [this, node](ax::mojom::StringAttribute attr, |
| const std::string& old_string, |
| const std::string& new_string) { |
| for (AXTreeObserver* observer : observers_) { |
| observer->OnStringAttributeChanged(this, node, attr, old_string, |
| new_string); |
| } |
| }; |
| CallIfAttributeValuesChanged(old_data.string_attributes, |
| new_data.string_attributes, std::string(), |
| string_callback); |
| |
| auto bool_callback = [this, node](ax::mojom::BoolAttribute attr, |
| const bool& old_bool, |
| const bool& new_bool) { |
| for (AXTreeObserver* observer : observers_) |
| observer->OnBoolAttributeChanged(this, node, attr, new_bool); |
| }; |
| CallIfAttributeValuesChanged(old_data.bool_attributes, |
| new_data.bool_attributes, false, bool_callback); |
| |
| auto float_callback = [this, node](ax::mojom::FloatAttribute attr, |
| const float& old_float, |
| const float& new_float) { |
| for (AXTreeObserver* observer : observers_) |
| observer->OnFloatAttributeChanged(this, node, attr, old_float, new_float); |
| }; |
| CallIfAttributeValuesChanged(old_data.float_attributes, |
| new_data.float_attributes, 0.0f, float_callback); |
| |
| auto int_callback = [this, node](ax::mojom::IntAttribute attr, |
| const int& old_int, const int& new_int) { |
| for (AXTreeObserver* observer : observers_) |
| observer->OnIntAttributeChanged(this, node, attr, old_int, new_int); |
| }; |
| CallIfAttributeValuesChanged(old_data.int_attributes, new_data.int_attributes, |
| 0, int_callback); |
| |
| auto intlist_callback = [this, node]( |
| ax::mojom::IntListAttribute attr, |
| const std::vector<int32_t>& old_intlist, |
| const std::vector<int32_t>& new_intlist) { |
| for (AXTreeObserver* observer : observers_) |
| observer->OnIntListAttributeChanged(this, node, attr, old_intlist, |
| new_intlist); |
| }; |
| CallIfAttributeValuesChanged(old_data.intlist_attributes, |
| new_data.intlist_attributes, |
| std::vector<int32_t>(), intlist_callback); |
| |
| auto stringlist_callback = |
| [this, node](ax::mojom::StringListAttribute attr, |
| const std::vector<std::string>& old_stringlist, |
| const std::vector<std::string>& new_stringlist) { |
| for (AXTreeObserver* observer : observers_) |
| observer->OnStringListAttributeChanged( |
| this, node, attr, old_stringlist, new_stringlist); |
| }; |
| CallIfAttributeValuesChanged(old_data.stringlist_attributes, |
| new_data.stringlist_attributes, |
| std::vector<std::string>(), stringlist_callback); |
| } |
| |
| void AXTree::UpdateReverseRelations(AXNode* node, const AXNodeData& new_data) { |
| BASE_DCHECK(GetTreeUpdateInProgressState()); |
| const AXNodeData& old_data = node->data(); |
| int id = new_data.id; |
| auto int_callback = [this, id](ax::mojom::IntAttribute attr, |
| const int& old_id, const int& new_id) { |
| if (!IsNodeIdIntAttribute(attr)) |
| return; |
| |
| // Remove old_id -> id from the map, and clear map keys if their |
| // values are now empty. |
| auto& map = int_reverse_relations_[attr]; |
| if (map.find(old_id) != map.end()) { |
| map[old_id].erase(id); |
| if (map[old_id].empty()) |
| map.erase(old_id); |
| } |
| |
| // Add new_id -> id to the map, unless new_id is zero indicating that |
| // we're only removing a relation. |
| if (new_id) |
| map[new_id].insert(id); |
| }; |
| CallIfAttributeValuesChanged(old_data.int_attributes, new_data.int_attributes, |
| 0, int_callback); |
| |
| auto intlist_callback = [this, id](ax::mojom::IntListAttribute attr, |
| const std::vector<int32_t>& old_idlist, |
| const std::vector<int32_t>& new_idlist) { |
| if (!IsNodeIdIntListAttribute(attr)) |
| return; |
| |
| auto& map = intlist_reverse_relations_[attr]; |
| for (int32_t old_id : old_idlist) { |
| if (map.find(old_id) != map.end()) { |
| map[old_id].erase(id); |
| if (map[old_id].empty()) |
| map.erase(old_id); |
| } |
| } |
| for (int32_t new_id : new_idlist) |
| intlist_reverse_relations_[attr][new_id].insert(id); |
| }; |
| CallIfAttributeValuesChanged(old_data.intlist_attributes, |
| new_data.intlist_attributes, |
| std::vector<int32_t>(), intlist_callback); |
| |
| auto string_callback = [this, id](ax::mojom::StringAttribute attr, |
| const std::string& old_string, |
| const std::string& new_string) { |
| if (attr == ax::mojom::StringAttribute::kChildTreeId) { |
| // Remove old_string -> id from the map, and clear map keys if |
| // their values are now empty. |
| AXTreeID old_ax_tree_id = AXTreeID::FromString(old_string); |
| if (child_tree_id_reverse_map_.find(old_ax_tree_id) != |
| child_tree_id_reverse_map_.end()) { |
| child_tree_id_reverse_map_[old_ax_tree_id].erase(id); |
| if (child_tree_id_reverse_map_[old_ax_tree_id].empty()) |
| child_tree_id_reverse_map_.erase(old_ax_tree_id); |
| } |
| |
| // Add new_string -> id to the map, unless new_id is zero indicating that |
| // we're only removing a relation. |
| if (!new_string.empty()) { |
| AXTreeID new_ax_tree_id = AXTreeID::FromString(new_string); |
| child_tree_id_reverse_map_[new_ax_tree_id].insert(id); |
| } |
| } |
| }; |
| |
| CallIfAttributeValuesChanged(old_data.string_attributes, |
| new_data.string_attributes, std::string(), |
| string_callback); |
| } |
| |
| bool AXTree::ValidatePendingChangesComplete( |
| const AXTreeUpdateState& update_state) { |
| if (!update_state.pending_nodes.empty()) { |
| error_ = "Nodes left pending by the update:"; |
| for (const AXNode::AXID pending_id : update_state.pending_nodes) { |
| error_ += base::StringPrintf(" %d", pending_id); |
| } |
| return false; |
| } |
| |
| if (!update_state.node_id_to_pending_data.empty()) { |
| std::string destroy_subtree_ids; |
| std::string destroy_node_ids; |
| std::string create_node_ids; |
| |
| bool has_pending_changes = false; |
| for (auto&& pair : update_state.node_id_to_pending_data) { |
| const AXNode::AXID pending_id = pair.first; |
| const std::unique_ptr<PendingStructureChanges>& data = pair.second; |
| if (data->DoesNodeExpectAnyStructureChanges()) { |
| if (data->DoesNodeExpectSubtreeWillBeDestroyed()) |
| destroy_subtree_ids += base::StringPrintf(" %d", pending_id); |
| if (data->DoesNodeExpectNodeWillBeDestroyed()) |
| destroy_node_ids += base::StringPrintf(" %d", pending_id); |
| if (data->DoesNodeExpectNodeWillBeCreated()) |
| create_node_ids += base::StringPrintf(" %d", pending_id); |
| has_pending_changes = true; |
| } |
| } |
| if (has_pending_changes) { |
| std::ostringstream stringStream; |
| stringStream << "Changes left pending by the update; destroy subtrees: " |
| << destroy_subtree_ids.c_str() |
| << ", destroy nodes: " << destroy_node_ids.c_str() |
| << ", create nodes: " << create_node_ids.c_str(); |
| error_ = stringStream.str(); |
| } |
| return !has_pending_changes; |
| } |
| |
| return true; |
| } |
| |
| void AXTree::MarkSubtreeForDestruction(AXNode::AXID node_id, |
| AXTreeUpdateState* update_state) { |
| update_state->IncrementPendingDestroySubtreeCount(node_id); |
| MarkNodesForDestructionRecursive(node_id, update_state); |
| } |
| |
| void AXTree::MarkNodesForDestructionRecursive(AXNode::AXID node_id, |
| AXTreeUpdateState* update_state) { |
| // If this subtree has already been marked for destruction, return so |
| // we don't walk it again. |
| if (!update_state->ShouldPendingNodeExistInTree(node_id)) |
| return; |
| |
| const AXNodeData& last_known_data = |
| update_state->GetLastKnownPendingNodeData(node_id); |
| |
| update_state->IncrementPendingDestroyNodeCount(node_id); |
| for (AXNode::AXID child_id : last_known_data.child_ids) { |
| MarkNodesForDestructionRecursive(child_id, update_state); |
| } |
| } |
| |
| void AXTree::DestroySubtree(AXNode* node, AXTreeUpdateState* update_state) { |
| BASE_DCHECK(GetTreeUpdateInProgressState()); |
| // |update_state| must already contain information about all of the expected |
| // changes and invalidations to apply. If any of these are missing, observers |
| // may not be notified of changes. |
| BASE_DCHECK(update_state); |
| BASE_DCHECK(update_state->GetPendingDestroySubtreeCount(node->id()) > 0); |
| BASE_DCHECK(!node->parent() || update_state->InvalidatesUnignoredCachedValues( |
| node->parent()->id())); |
| update_state->DecrementPendingDestroySubtreeCount(node->id()); |
| DestroyNodeAndSubtree(node, update_state); |
| } |
| |
| void AXTree::DestroyNodeAndSubtree(AXNode* node, |
| AXTreeUpdateState* update_state) { |
| BASE_DCHECK(GetTreeUpdateInProgressState()); |
| BASE_DCHECK(!update_state || |
| update_state->GetPendingDestroyNodeCount(node->id()) > 0); |
| |
| // Clear out any reverse relations. |
| AXNodeData empty_data; |
| empty_data.id = node->id(); |
| UpdateReverseRelations(node, empty_data); |
| |
| id_map_.erase(node->id()); |
| for (auto* child : node->children()) |
| DestroyNodeAndSubtree(child, update_state); |
| if (update_state) { |
| update_state->pending_nodes.erase(node->id()); |
| update_state->DecrementPendingDestroyNodeCount(node->id()); |
| update_state->removed_node_ids.insert(node->id()); |
| update_state->new_node_ids.erase(node->id()); |
| update_state->node_data_changed_ids.erase(node->id()); |
| if (update_state->IsReparentedNode(node)) { |
| update_state->old_node_id_to_data.emplace( |
| std::make_pair(node->id(), node->TakeData())); |
| } |
| } |
| node->Destroy(); |
| } |
| |
| void AXTree::DeleteOldChildren(AXNode* node, |
| const std::vector<int32_t>& new_child_ids, |
| AXTreeUpdateState* update_state) { |
| BASE_DCHECK(GetTreeUpdateInProgressState()); |
| // Create a set of child ids in |src| for fast lookup, we know the set does |
| // not contain duplicate entries already, because that was handled when |
| // populating |update_state| with information about all of the expected |
| // changes to be applied. |
| std::set<int32_t> new_child_id_set(new_child_ids.begin(), |
| new_child_ids.end()); |
| |
| // Delete the old children. |
| for (AXNode* child : node->children()) { |
| if (!base::Contains(new_child_id_set, child->id())) |
| DestroySubtree(child, update_state); |
| } |
| } |
| |
| bool AXTree::CreateNewChildVector(AXNode* node, |
| const std::vector<int32_t>& new_child_ids, |
| std::vector<AXNode*>* new_children, |
| AXTreeUpdateState* update_state) { |
| BASE_DCHECK(GetTreeUpdateInProgressState()); |
| bool success = true; |
| for (size_t i = 0; i < new_child_ids.size(); ++i) { |
| int32_t child_id = new_child_ids[i]; |
| AXNode* child = GetFromId(child_id); |
| if (child) { |
| if (child->parent() != node) { |
| // This is a serious error - nodes should never be reparented. |
| // If this case occurs, continue so this node isn't left in an |
| // inconsistent state, but return failure at the end. |
| error_ = base::StringPrintf( |
| "Node %d reparented from %d to %d", child->id(), |
| child->parent() ? child->parent()->id() : 0, node->id()); |
| success = false; |
| continue; |
| } |
| child->SetIndexInParent(i); |
| } else { |
| child = CreateNode(node, child_id, i, update_state); |
| update_state->pending_nodes.insert(child->id()); |
| } |
| new_children->push_back(child); |
| } |
| |
| return success; |
| } |
| |
| void AXTree::SetEnableExtraMacNodes(bool enabled) { |
| if (enable_extra_mac_nodes_ == enabled) |
| return; // No change. |
| if (enable_extra_mac_nodes_ && !enabled) { |
| BASE_LOG() |
| << "We don't support disabling the extra Mac nodes once enabled."; |
| BASE_UNREACHABLE(); |
| return; |
| } |
| |
| BASE_DCHECK(0U == table_info_map_.size()); |
| enable_extra_mac_nodes_ = enabled; |
| } |
| |
| int32_t AXTree::GetNextNegativeInternalNodeId() { |
| int32_t return_value = next_negative_internal_node_id_; |
| next_negative_internal_node_id_--; |
| if (next_negative_internal_node_id_ > 0) |
| next_negative_internal_node_id_ = -1; |
| return return_value; |
| } |
| |
| void AXTree::PopulateOrderedSetItemsMap( |
| const AXNode& original_node, |
| const AXNode* ordered_set, |
| OrderedSetItemsMap* items_map_to_be_populated) const { |
| // Ignored nodes are not a part of ordered sets. |
| if (original_node.IsIgnored()) |
| return; |
| |
| // Not all ordered set containers support hierarchical level, but their set |
| // items may support hierarchical level. For example, container <tree> does |
| // not support level, but <treeitem> supports level. For ordered sets like |
| // this, the set container (e.g. <tree>) will take on the min of the levels |
| // of its direct children(e.g. <treeitem>), if the children's levels are |
| // defined. |
| std::optional<int> ordered_set_min_level = |
| ordered_set->GetHierarchicalLevel(); |
| |
| for (AXNode::UnignoredChildIterator child = |
| ordered_set->UnignoredChildrenBegin(); |
| child != ordered_set->UnignoredChildrenEnd(); ++child) { |
| std::optional<int> child_level = child->GetHierarchicalLevel(); |
| if (child_level) { |
| ordered_set_min_level = ordered_set_min_level |
| ? std::min(child_level, ordered_set_min_level) |
| : child_level; |
| } |
| } |
| |
| RecursivelyPopulateOrderedSetItemsMap(original_node, ordered_set, ordered_set, |
| ordered_set_min_level, std::nullopt, |
| items_map_to_be_populated); |
| |
| // If after RecursivelyPopulateOrderedSetItemsMap() call, the corresponding |
| // level (i.e. |ordered_set_min_level|) does not exist in |
| // |items_map_to_be_populated|, and |original_node| equals |ordered_set|, we |
| // know |original_node| is an empty ordered set and contains no set items. |
| // However, |original_node| may still have set size attribute, so we still |
| // want to add this empty set (i.e. original_node/ordered_set) to |
| // |items_map_to_be_populated|. |
| if (&original_node == ordered_set && |
| !items_map_to_be_populated->HierarchicalLevelExists( |
| ordered_set_min_level)) { |
| items_map_to_be_populated->Add(ordered_set_min_level, |
| OrderedSetContent(&original_node)); |
| } |
| } |
| |
| void AXTree::RecursivelyPopulateOrderedSetItemsMap( |
| const AXNode& original_node, |
| const AXNode* ordered_set, |
| const AXNode* local_parent, |
| std::optional<int> ordered_set_min_level, |
| std::optional<int> prev_level, |
| OrderedSetItemsMap* items_map_to_be_populated) const { |
| // For optimization purpose, we want to only populate set items that are |
| // direct descendants of |ordered_set|, since we will only be calculating |
| // PosInSet & SetSize of items of that level. So we skip items on deeper |
| // levels by stop searching recursively on node |local_parent| that turns out |
| // to be an ordered set whose role matches that of |ordered_set|. However, |
| // when we encounter a flattened structure such as the following: |
| // <div role="tree"> |
| // <div role="treeitem" aria-level="1"></div> |
| // <div role="treeitem" aria-level="2"></div> |
| // <div role="treeitem" aria-level="3"></div> |
| // </div> |
| // This optimization won't apply, we will end up populating items from all |
| // levels. |
| if (ordered_set->data().role == local_parent->data().role && |
| ordered_set != local_parent) |
| return; |
| |
| for (AXNode::UnignoredChildIterator itr = |
| local_parent->UnignoredChildrenBegin(); |
| itr != local_parent->UnignoredChildrenEnd(); ++itr) { |
| const AXNode* child = itr.get(); |
| |
| // Invisible children should not be counted. |
| // However, in the collapsed container case (e.g. a combobox), items can |
| // still be chosen/navigated. However, the options in these collapsed |
| // containers are historically marked invisible. Therefore, in that case, |
| // count the invisible items. Only check 2 levels up, as combobox containers |
| // are never higher. |
| if (child->data().HasState(ax::mojom::State::kInvisible) && |
| !IsCollapsed(local_parent) && !IsCollapsed(local_parent->parent())) { |
| continue; |
| } |
| |
| std::optional<int> curr_level = child->GetHierarchicalLevel(); |
| |
| // Add child to |items_map_to_be_populated| if role matches with the role of |
| // |ordered_set|. If role of node is kRadioButton, don't add items of other |
| // roles, even if item role matches the role of |ordered_set|. |
| if (child->data().role == ax::mojom::Role::kComment || |
| (original_node.data().role == ax::mojom::Role::kRadioButton && |
| child->data().role == ax::mojom::Role::kRadioButton) || |
| (original_node.data().role != ax::mojom::Role::kRadioButton && |
| child->SetRoleMatchesItemRole(ordered_set))) { |
| // According to WAI-ARIA spec, some ordered set items do not support |
| // hierarchical level while its ordered set container does. For example, |
| // <tab> does not support level, while <tablist> supports level. |
| // https://www.w3.org/WAI/PF/aria/roles#tab |
| // https://www.w3.org/WAI/PF/aria/roles#tablist |
| // For this special case, when we add set items (e.g. tab) to |
| // |items_map_to_be_populated|, set item is placed at the same level as |
| // its container (e.g. tablist) in |items_map_to_be_populated|. |
| if (!curr_level && child->GetUnignoredParent() == ordered_set) |
| curr_level = ordered_set_min_level; |
| |
| // We only add child to |items_map_to_be_populated| if the child set item |
| // is at the same hierarchical level as |ordered_set|'s level. |
| if (!items_map_to_be_populated->HierarchicalLevelExists(curr_level)) { |
| bool use_ordered_set = child->SetRoleMatchesItemRole(ordered_set) && |
| ordered_set_min_level == curr_level; |
| const AXNode* child_ordered_set = |
| use_ordered_set ? ordered_set : nullptr; |
| items_map_to_be_populated->Add(curr_level, |
| OrderedSetContent(child_ordered_set)); |
| } |
| |
| items_map_to_be_populated->AddItemToBack(curr_level, child); |
| } |
| |
| // If |child| is an ignored container for ordered set and should not be used |
| // to contribute to |items_map_to_be_populated|, we recurse into |child|'s |
| // descendants to populate |items_map_to_be_populated|. |
| if (child->IsIgnoredContainerForOrderedSet()) { |
| RecursivelyPopulateOrderedSetItemsMap(original_node, ordered_set, child, |
| ordered_set_min_level, curr_level, |
| items_map_to_be_populated); |
| } |
| |
| // If |curr_level| goes up one level from |prev_level|, which indicates |
| // the ordered set of |prev_level| is closed, we add a new OrderedSetContent |
| // on the previous level of |items_map_to_be_populated| to signify this. |
| // Consider the example below: |
| // <div role="tree"> |
| // <div role="treeitem" aria-level="1"></div> |
| // <!--- set1-level2 --> |
| // <div role="treeitem" aria-level="2"></div> |
| // <div role="treeitem" aria-level="2"></div> <--|prev_level| |
| // <div role="treeitem" aria-level="1" id="item2-level1"> <--|curr_level| |
| // </div> |
| // <!--- set2-level2 --> |
| // <div role="treeitem" aria-level="2"></div> |
| // <div role="treeitem" aria-level="2"></div> |
| // </div> |
| // |prev_level| is on the last item of "set1-level2" and |curr_level| is on |
| // "item2-level1". Since |curr_level| is up one level from |prev_level|, we |
| // already completed adding all items from "set1-level2" to |
| // |items_map_to_be_populated|. So we close up "set1-level2" by adding a new |
| // OrderedSetContent to level 2. When |curr_level| ends up on the items of |
| // "set2-level2" next, it has a fresh new set to be populated. |
| if (child->SetRoleMatchesItemRole(ordered_set) && curr_level < prev_level) |
| items_map_to_be_populated->Add(prev_level, OrderedSetContent()); |
| |
| prev_level = curr_level; |
| } |
| } |
| |
| // Given an ordered_set, compute pos_in_set and set_size for all of its items |
| // and store values in cache. |
| // Ordered_set should never be nullptr. |
| void AXTree::ComputeSetSizePosInSetAndCache(const AXNode& node, |
| const AXNode* ordered_set) { |
| BASE_DCHECK(ordered_set); |
| |
| // Set items role::kComment and role::kRadioButton are special cases and do |
| // not necessarily need to be contained in an ordered set. |
| if (node.data().role != ax::mojom::Role::kComment && |
| node.data().role != ax::mojom::Role::kRadioButton && |
| !node.SetRoleMatchesItemRole(ordered_set) && !IsSetLike(node.data().role)) |
| return; |
| |
| // Find all items within ordered_set and add to |items_map_to_be_populated|. |
| OrderedSetItemsMap items_map_to_be_populated; |
| PopulateOrderedSetItemsMap(node, ordered_set, &items_map_to_be_populated); |
| |
| // If ordered_set role is kPopUpButton and it wraps a kMenuListPopUp, then we |
| // would like it to inherit the SetSize from the kMenuListPopUp it wraps. To |
| // do this, we treat the kMenuListPopUp as the ordered_set and eventually |
| // assign its SetSize value to the kPopUpButton. |
| if (node.data().role == ax::mojom::Role::kPopUpButton && |
| node.GetUnignoredChildCount() > 0) { |
| // kPopUpButtons are only allowed to contain one kMenuListPopUp. |
| // The single element is guaranteed to be a kMenuListPopUp because that is |
| // the only item role that matches the ordered set role of kPopUpButton. |
| // Please see AXNode::SetRoleMatchesItemRole for more details. |
| OrderedSetContent* set_content = |
| items_map_to_be_populated.GetFirstOrderedSetContent(); |
| if (set_content && set_content->set_items_.size() == 1) { |
| const AXNode* menu_list_popup = set_content->set_items_.front(); |
| if (menu_list_popup->data().role == ax::mojom::Role::kMenuListPopup) { |
| items_map_to_be_populated.Clear(); |
| PopulateOrderedSetItemsMap(node, menu_list_popup, |
| &items_map_to_be_populated); |
| set_content = items_map_to_be_populated.GetFirstOrderedSetContent(); |
| // Replace |set_content|'s ordered set container with |node| |
| // (Role::kPopUpButton), which acts as the set container for nodes with |
| // Role::kMenuListOptions (children of |menu_list_popup|). |
| if (set_content) |
| set_content->ordered_set_ = &node; |
| } |
| } |
| } |
| |
| // Iterate over all items from OrderedSetItemsMap to compute and cache each |
| // ordered set item's PosInSet and SetSize and corresponding ordered set |
| // container's SetSize. |
| for (auto element : items_map_to_be_populated.items_map_) { |
| for (const OrderedSetContent& ordered_set_content : element.second) { |
| ComputeSetSizePosInSetAndCacheHelper(ordered_set_content); |
| } |
| } |
| } |
| |
| void AXTree::ComputeSetSizePosInSetAndCacheHelper( |
| const OrderedSetContent& ordered_set_content) { |
| // Keep track of number of items in the set. |
| int32_t num_elements = 0; |
| // Keep track of largest ordered set item's |aria-setsize| attribute value. |
| int32_t max_item_set_size_from_attribute = 0; |
| |
| for (const AXNode* item : ordered_set_content.set_items_) { |
| // |item|'s PosInSet value is the maximum of accumulated number of |
| // elements count and the value from its |aria-posinset| attribute. |
| int32_t pos_in_set_value = |
| std::max(num_elements + 1, |
| item->GetIntAttribute(ax::mojom::IntAttribute::kPosInSet)); |
| |
| // For |item| that has defined hierarchical level and |aria-posinset| |
| // attribute, the attribute value takes precedence. |
| // Note: According to WAI-ARIA spec, items that support |
| // |aria-posinset| do not necessarily support hierarchical level. |
| if (item->GetHierarchicalLevel() && |
| item->HasIntAttribute(ax::mojom::IntAttribute::kPosInSet)) |
| pos_in_set_value = |
| item->GetIntAttribute(ax::mojom::IntAttribute::kPosInSet); |
| |
| num_elements = pos_in_set_value; |
| |
| // Cache computed PosInSet value for |item|. |
| node_set_size_pos_in_set_info_map_[item->id()] = NodeSetSizePosInSetInfo(); |
| node_set_size_pos_in_set_info_map_[item->id()].pos_in_set = |
| pos_in_set_value; |
| |
| // Track the largest set size for this OrderedSetContent. |
| max_item_set_size_from_attribute = |
| std::max(max_item_set_size_from_attribute, |
| item->GetIntAttribute(ax::mojom::IntAttribute::kSetSize)); |
| } // End of iterating over each item in |ordered_set_content|. |
| |
| // The SetSize of an ordered set (and all of its items) is the maximum of |
| // the following values: |
| // 1. The number of elements in the ordered set. |
| // 2. The largest item set size from |aria-setsize| attribute. |
| // 3. The ordered set container's |aria-setsize| attribute value. |
| int32_t set_size_value = |
| std::max(num_elements, max_item_set_size_from_attribute); |
| |
| // Cache the hierarchical level and set size of |ordered_set_content|'s set |
| // container, if the container exists. |
| if (const AXNode* ordered_set = ordered_set_content.ordered_set_) { |
| set_size_value = std::max( |
| set_size_value, |
| ordered_set->GetIntAttribute(ax::mojom::IntAttribute::kSetSize)); |
| |
| // Cache |ordered_set|'s hierarchical level. |
| std::optional<int> ordered_set_level = ordered_set->GetHierarchicalLevel(); |
| if (node_set_size_pos_in_set_info_map_.find(ordered_set->id()) == |
| node_set_size_pos_in_set_info_map_.end()) { |
| node_set_size_pos_in_set_info_map_[ordered_set->id()] = |
| NodeSetSizePosInSetInfo(); |
| node_set_size_pos_in_set_info_map_[ordered_set->id()] |
| .lowest_hierarchical_level = ordered_set_level; |
| } else if (node_set_size_pos_in_set_info_map_[ordered_set->id()] |
| .lowest_hierarchical_level > ordered_set_level) { |
| node_set_size_pos_in_set_info_map_[ordered_set->id()] |
| .lowest_hierarchical_level = ordered_set_level; |
| } |
| // Cache |ordered_set|'s set size. |
| node_set_size_pos_in_set_info_map_[ordered_set->id()].set_size = |
| set_size_value; |
| } |
| |
| // Cache the set size of |ordered_set_content|'s set items. |
| for (const AXNode* item : ordered_set_content.set_items_) { |
| // If item's hierarchical level and |aria-setsize| attribute are specified, |
| // the item's |aria-setsize| value takes precedence. |
| if (item->GetHierarchicalLevel() && |
| item->HasIntAttribute(ax::mojom::IntAttribute::kSetSize)) |
| node_set_size_pos_in_set_info_map_[item->id()].set_size = |
| item->GetIntAttribute(ax::mojom::IntAttribute::kSetSize); |
| else |
| node_set_size_pos_in_set_info_map_[item->id()].set_size = set_size_value; |
| } // End of iterating over each item in |ordered_set_content|. |
| } |
| |
| std::optional<int> AXTree::GetPosInSet(const AXNode& node) { |
| if (node.data().role == ax::mojom::Role::kPopUpButton && |
| node.GetUnignoredChildCount() == 0 && |
| node.HasIntAttribute(ax::mojom::IntAttribute::kPosInSet)) { |
| return node.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet); |
| } |
| |
| if (node_set_size_pos_in_set_info_map_.find(node.id()) != |
| node_set_size_pos_in_set_info_map_.end()) { |
| // If item's id is in the cache, return stored PosInSet value. |
| return node_set_size_pos_in_set_info_map_[node.id()].pos_in_set; |
| } |
| |
| if (GetTreeUpdateInProgressState()) |
| return std::nullopt; |
| |
| // Only allow this to be called on nodes that can hold PosInSet values, |
| // which are defined in the ARIA spec. |
| if (!node.IsOrderedSetItem() || node.IsIgnored()) |
| return std::nullopt; |
| |
| const AXNode* ordered_set = node.GetOrderedSet(); |
| if (!ordered_set) |
| return std::nullopt; |
| |
| // Compute, cache, then return. |
| ComputeSetSizePosInSetAndCache(node, ordered_set); |
| std::optional<int> pos_in_set = |
| node_set_size_pos_in_set_info_map_[node.id()].pos_in_set; |
| if (pos_in_set.has_value() && pos_in_set.value() < 1) |
| return std::nullopt; |
| |
| return pos_in_set; |
| } |
| |
| std::optional<int> AXTree::GetSetSize(const AXNode& node) { |
| if (node.data().role == ax::mojom::Role::kPopUpButton && |
| node.GetUnignoredChildCount() == 0 && |
| node.HasIntAttribute(ax::mojom::IntAttribute::kSetSize)) { |
| return node.GetIntAttribute(ax::mojom::IntAttribute::kSetSize); |
| } |
| |
| if (node_set_size_pos_in_set_info_map_.find(node.id()) != |
| node_set_size_pos_in_set_info_map_.end()) { |
| // If item's id is in the cache, return stored SetSize value. |
| return node_set_size_pos_in_set_info_map_[node.id()].set_size; |
| } |
| |
| if (GetTreeUpdateInProgressState()) |
| return std::nullopt; |
| |
| // Only allow this to be called on nodes that can hold SetSize values, which |
| // are defined in the ARIA spec. However, we allow set-like items to receive |
| // SetSize values for internal purposes. |
| if ((!node.IsOrderedSetItem() && !node.IsOrderedSet()) || node.IsIgnored() || |
| node.IsEmbeddedGroup()) { |
| return std::nullopt; |
| } |
| |
| // If |node| is item-like, find its outerlying ordered set. Otherwise, |
| // |node| is the ordered set. |
| const AXNode* ordered_set = &node; |
| if (IsItemLike(node.data().role)) |
| ordered_set = node.GetOrderedSet(); |
| if (!ordered_set) |
| return std::nullopt; |
| |
| // For popup buttons that control a single element, inherit the controlled |
| // item's SetSize. Skip this block if the popup button controls itself. |
| if (node.data().role == ax::mojom::Role::kPopUpButton) { |
| const auto& controls_ids = node.data().GetIntListAttribute( |
| ax::mojom::IntListAttribute::kControlsIds); |
| if (controls_ids.size() == 1 && GetFromId(controls_ids[0]) && |
| controls_ids[0] != node.id()) { |
| const AXNode& controlled_item = *GetFromId(controls_ids[0]); |
| |
| std::optional<int> controlled_item_set_size = GetSetSize(controlled_item); |
| node_set_size_pos_in_set_info_map_[node.id()].set_size = |
| controlled_item_set_size; |
| return controlled_item_set_size; |
| } |
| } |
| |
| // Compute, cache, then return. |
| ComputeSetSizePosInSetAndCache(node, ordered_set); |
| std::optional<int> set_size = |
| node_set_size_pos_in_set_info_map_[node.id()].set_size; |
| if (set_size.has_value() && set_size.value() < 0) |
| return std::nullopt; |
| |
| return set_size; |
| } |
| |
| AXTree::Selection AXTree::GetUnignoredSelection() const { |
| Selection unignored_selection = { |
| data().sel_is_backward, data().sel_anchor_object_id, |
| data().sel_anchor_offset, data().sel_anchor_affinity, |
| data().sel_focus_object_id, data().sel_focus_offset, |
| data().sel_focus_affinity}; |
| AXNode* anchor_node = GetFromId(data().sel_anchor_object_id); |
| AXNode* focus_node = GetFromId(data().sel_focus_object_id); |
| |
| AXNodePosition::AXPositionInstance anchor_position = |
| anchor_node ? AXNodePosition::CreatePosition(*anchor_node, |
| data().sel_anchor_offset, |
| data().sel_anchor_affinity) |
| : AXNodePosition::CreateNullPosition(); |
| |
| // Null positions are never ignored. |
| if (anchor_position->IsIgnored()) { |
| anchor_position = anchor_position->AsUnignoredPosition( |
| data().sel_is_backward ? AXPositionAdjustmentBehavior::kMoveForward |
| : AXPositionAdjustmentBehavior::kMoveBackward); |
| |
| // Any selection endpoint that is inside a leaf node is expressed as a text |
| // position in AXTreeData. |
| if (anchor_position->IsLeafTreePosition()) |
| anchor_position = anchor_position->AsTextPosition(); |
| |
| // We do not expect the selection to have an endpoint on an inline text |
| // box as this will create issues with parts of the code that don't use |
| // inline text boxes. |
| if (anchor_position->IsTextPosition() && |
| anchor_position->GetAnchor()->data().role == |
| ax::mojom::Role::kInlineTextBox) { |
| anchor_position = anchor_position->CreateParentPosition(); |
| } |
| |
| switch (anchor_position->kind()) { |
| case AXPositionKind::NULL_POSITION: |
| // If one of the selection endpoints is invalid, then both endpoints |
| // should be unset. |
| unignored_selection.anchor_object_id = AXNode::kInvalidAXID; |
| unignored_selection.anchor_offset = -1; |
| unignored_selection.anchor_affinity = |
| ax::mojom::TextAffinity::kDownstream; |
| unignored_selection.focus_object_id = AXNode::kInvalidAXID; |
| unignored_selection.focus_offset = -1; |
| unignored_selection.focus_affinity = |
| ax::mojom::TextAffinity::kDownstream; |
| return unignored_selection; |
| case AXPositionKind::TREE_POSITION: |
| unignored_selection.anchor_object_id = anchor_position->anchor_id(); |
| unignored_selection.anchor_offset = anchor_position->child_index(); |
| unignored_selection.anchor_affinity = |
| ax::mojom::TextAffinity::kDownstream; |
| break; |
| case AXPositionKind::TEXT_POSITION: |
| unignored_selection.anchor_object_id = anchor_position->anchor_id(); |
| unignored_selection.anchor_offset = anchor_position->text_offset(); |
| unignored_selection.anchor_affinity = anchor_position->affinity(); |
| break; |
| } |
| } |
| |
| AXNodePosition::AXPositionInstance focus_position = |
| focus_node |
| ? AXNodePosition::CreatePosition(*focus_node, data().sel_focus_offset, |
| data().sel_focus_affinity) |
| : AXNodePosition::CreateNullPosition(); |
| |
| // Null positions are never ignored. |
| if (focus_position->IsIgnored()) { |
| focus_position = focus_position->AsUnignoredPosition( |
| !data().sel_is_backward ? AXPositionAdjustmentBehavior::kMoveForward |
| : AXPositionAdjustmentBehavior::kMoveBackward); |
| |
| // Any selection endpoint that is inside a leaf node is expressed as a text |
| // position in AXTreeData. |
| if (focus_position->IsLeafTreePosition()) |
| focus_position = focus_position->AsTextPosition(); |
| |
| // We do not expect the selection to have an endpoint on an inline text |
| // box as this will create issues with parts of the code that don't use |
| // inline text boxes. |
| if (focus_position->IsTextPosition() && |
| focus_position->GetAnchor()->data().role == |
| ax::mojom::Role::kInlineTextBox) { |
| focus_position = focus_position->CreateParentPosition(); |
| } |
| |
| switch (focus_position->kind()) { |
| case AXPositionKind::NULL_POSITION: |
| // If one of the selection endpoints is invalid, then both endpoints |
| // should be unset. |
| unignored_selection.anchor_object_id = AXNode::kInvalidAXID; |
| unignored_selection.anchor_offset = -1; |
| unignored_selection.anchor_affinity = |
| ax::mojom::TextAffinity::kDownstream; |
| unignored_selection.focus_object_id = AXNode::kInvalidAXID; |
| unignored_selection.focus_offset = -1; |
| unignored_selection.focus_affinity = |
| ax::mojom::TextAffinity::kDownstream; |
| return unignored_selection; |
| case AXPositionKind::TREE_POSITION: |
| unignored_selection.focus_object_id = focus_position->anchor_id(); |
| unignored_selection.focus_offset = focus_position->child_index(); |
| unignored_selection.focus_affinity = |
| ax::mojom::TextAffinity::kDownstream; |
| break; |
| case AXPositionKind::TEXT_POSITION: |
| unignored_selection.focus_object_id = focus_position->anchor_id(); |
| unignored_selection.focus_offset = focus_position->text_offset(); |
| unignored_selection.focus_affinity = focus_position->affinity(); |
| break; |
| } |
| } |
| |
| return unignored_selection; |
| } |
| |
| bool AXTree::GetTreeUpdateInProgressState() const { |
| return tree_update_in_progress_; |
| } |
| |
| void AXTree::SetTreeUpdateInProgressState(bool set_tree_update_value) { |
| tree_update_in_progress_ = set_tree_update_value; |
| } |
| |
| bool AXTree::HasPaginationSupport() const { |
| return has_pagination_support_; |
| } |
| |
| } // namespace ui |