Commit 05afcecb authored by Adam Ettenberger's avatar Adam Ettenberger Committed by Commit Bot

Adjust AXTree::Unserialize to notify events outside of UpdateNode [2/3]

This change separates updating AXNodeData in AXTree::UpdateNode from
notifying that the node data will change, and node data has changed.

---

Problem :
  AXTree notifies AXTreeObservers of node and subtree changes in the middle
  of a tree update, which means observers may be seeing a tree state that's
  incomplete. This can lead to subtle bugs when observers access nodes in the
  AXTree other than the node being updated.
  e.g. |GetUnignoredParent| may be invalid during a notification, but
  correct afterwards.

Proposed solution :
  Separate notifications from updates in AXTree::Unserialize.

  This requires the following steps :
    1. Record all operations that would be performed on the tree.
    2. Fire all on destroy and OnNodeDataWillChange callbacks.
    3. Apply all update operations.
    4. Fire all created and node data changed callbacks.


  For additional context, see these comments by Nektarios and Dominic :
  https://chromium-review.googlesource.com/c/chromium/src/+/1650222/20#message-ffe9bf96b616a915bebf2e69d7803bcae6a18b24
  https://chromium-review.googlesource.com/c/chromium/src/+/1535171/69#message-7b62970b193439a1878d7339cdc94d2556d0a416

  I  plan to submit approximately 3 CLs to resolve this :
    1. Handle Pre/Post Tree changes (create node / destroy subtree)
    2. Handle Pre/Post Node data changes (attributes will / have changes)
    3. Add DCHECKs to AXNode::*Unignored* accessors, along with any remaining
       work required to do so, to help harden these paths.

Bug: 974444
Change-Id: Iade343a572003e31d79039b08cced689fb6e9cbe
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1710128
Commit-Queue: Adam Ettenberger <adettenb@microsoft.com>
Reviewed-by: default avatarNektarios Paisios <nektar@chromium.org>
Reviewed-by: default avatarKurt Catti-Schmidt <kschmi@microsoft.com>
Cr-Commit-Position: refs/heads/master@{#684391}
parent b1cadc00
...@@ -234,20 +234,20 @@ static void EstablishEmbeddedRelationship(AtkObject* document_object) { ...@@ -234,20 +234,20 @@ static void EstablishEmbeddedRelationship(AtkObject* document_object) {
document_platform_node->SetEmbeddingWindow(window); document_platform_node->SetEmbeddingWindow(window);
} }
void BrowserAccessibilityManagerAuraLinux::OnStateChanged( void BrowserAccessibilityManagerAuraLinux::OnNodeDataWillChange(
ui::AXTree* tree, ui::AXTree* tree,
ui::AXNode* node, const ui::AXNodeData& old_node_data,
ax::mojom::State state, const ui::AXNodeData& new_node_data) {
bool new_value) {
DCHECK_EQ(ax_tree(), tree); DCHECK_EQ(ax_tree(), tree);
// Since AuraLinux needs to send the children-changed::remove event with the // Since AuraLinux needs to send the children-changed::remove event with the
// index in parent, the event must be fired before the node becomes ignored. // index in parent, the event must be fired before the node becomes ignored.
// children-changed:add is handled with the generated Event::IGNORED_CHANGED. // children-changed:add is handled with the generated Event::IGNORED_CHANGED.
if (state == ax::mojom::State::kIgnored && new_value) { if (!old_node_data.HasState(ax::mojom::State::kIgnored) &&
DCHECK(!node->data().HasState(ax::mojom::State::kIgnored)); new_node_data.HasState(ax::mojom::State::kIgnored)) {
BrowserAccessibility* obj = GetFromAXNode(node); BrowserAccessibility* obj = GetFromID(old_node_data.id);
if (obj && obj->IsNative() && obj->GetParent()) { if (obj && obj->IsNative() && obj->GetParent()) {
DCHECK(!obj->HasState(ax::mojom::State::kIgnored));
g_signal_emit_by_name(obj->GetParent(), "children-changed::remove", g_signal_emit_by_name(obj->GetParent(), "children-changed::remove",
obj->GetIndexInParent(), obj->GetIndexInParent(),
obj->GetNativeViewAccessible()); obj->GetNativeViewAccessible());
......
...@@ -42,10 +42,9 @@ class CONTENT_EXPORT BrowserAccessibilityManagerAuraLinux ...@@ -42,10 +42,9 @@ class CONTENT_EXPORT BrowserAccessibilityManagerAuraLinux
protected: protected:
// AXTreeObserver methods. // AXTreeObserver methods.
void OnStateChanged(ui::AXTree* tree, void OnNodeDataWillChange(ui::AXTree* tree,
ui::AXNode* node, const ui::AXNodeData& old_node_data,
ax::mojom::State state, const ui::AXNodeData& new_node_data) override;
bool new_value) override;
void OnSubtreeWillBeDeleted(ui::AXTree* tree, ui::AXNode* node) override; void OnSubtreeWillBeDeleted(ui::AXTree* tree, ui::AXNode* node) override;
void OnAtomicUpdateFinished( void OnAtomicUpdateFinished(
ui::AXTree* tree, ui::AXTree* tree,
......
...@@ -421,7 +421,7 @@ AutomationAXTreeWrapper::GetChildTreeIDReverseMap() { ...@@ -421,7 +421,7 @@ AutomationAXTreeWrapper::GetChildTreeIDReverseMap() {
return *child_tree_id_reverse_map; return *child_tree_id_reverse_map;
} }
void AutomationAXTreeWrapper::OnNodeDataWillChange( void AutomationAXTreeWrapper::OnNodeDataChanged(
ui::AXTree* tree, ui::AXTree* tree,
const ui::AXNodeData& old_node_data, const ui::AXNodeData& old_node_data,
const ui::AXNodeData& new_node_data) { const ui::AXNodeData& new_node_data) {
......
...@@ -52,9 +52,9 @@ class AutomationAXTreeWrapper : public ui::AXTreeObserver { ...@@ -52,9 +52,9 @@ class AutomationAXTreeWrapper : public ui::AXTreeObserver {
private: private:
// AXTreeObserver overrides. // AXTreeObserver overrides.
void OnNodeDataWillChange(ui::AXTree* tree, void OnNodeDataChanged(ui::AXTree* tree,
const ui::AXNodeData& old_node_data, const ui::AXNodeData& old_node_data,
const ui::AXNodeData& new_node_data) override; const ui::AXNodeData& new_node_data) override;
void OnNodeWillBeDeleted(ui::AXTree* tree, ui::AXNode* node) override; void OnNodeWillBeDeleted(ui::AXTree* tree, ui::AXNode* node) override;
void OnAtomicUpdateFinished(ui::AXTree* tree, void OnAtomicUpdateFinished(ui::AXTree* tree,
bool root_changed, bool root_changed,
......
...@@ -143,9 +143,9 @@ void AXEventGenerator::AddEvent(AXNode* node, AXEventGenerator::Event event) { ...@@ -143,9 +143,9 @@ void AXEventGenerator::AddEvent(AXNode* node, AXEventGenerator::Event event) {
node_events.emplace(event, ax::mojom::EventFrom::kNone); node_events.emplace(event, ax::mojom::EventFrom::kNone);
} }
void AXEventGenerator::OnNodeDataWillChange(AXTree* tree, void AXEventGenerator::OnNodeDataChanged(AXTree* tree,
const AXNodeData& old_node_data, const AXNodeData& old_node_data,
const AXNodeData& new_node_data) { const AXNodeData& new_node_data) {
DCHECK_EQ(tree_, tree); DCHECK_EQ(tree_, tree);
// Fire CHILDREN_CHANGED events when the list of children updates. // Fire CHILDREN_CHANGED events when the list of children updates.
// Internally we store inline text box nodes as children of a static text // Internally we store inline text box nodes as children of a static text
...@@ -179,8 +179,6 @@ void AXEventGenerator::OnStateChanged(AXTree* tree, ...@@ -179,8 +179,6 @@ void AXEventGenerator::OnStateChanged(AXTree* tree,
case ax::mojom::State::kExpanded: case ax::mojom::State::kExpanded:
AddEvent(node, new_value ? Event::EXPANDED : Event::COLLAPSED); AddEvent(node, new_value ? Event::EXPANDED : Event::COLLAPSED);
// TODO(dtseng): tree in the midst of updates. Disallow access to
// |node|.
if (node->data().role == ax::mojom::Role::kRow || if (node->data().role == ax::mojom::Role::kRow ||
node->data().role == ax::mojom::Role::kTreeItem) { node->data().role == ax::mojom::Role::kTreeItem) {
AXNode* container = node; AXNode* container = node;
...@@ -248,8 +246,6 @@ void AXEventGenerator::OnStringAttributeChanged(AXTree* tree, ...@@ -248,8 +246,6 @@ void AXEventGenerator::OnStringAttributeChanged(AXTree* tree,
// Fire a LIVE_REGION_CREATED if the previous value was off, and the new // Fire a LIVE_REGION_CREATED if the previous value was off, and the new
// value is not-off. // value is not-off.
// TODO(dtseng): tree in the midst of updates. Disallow access to
// |node|.
if (!IsAlert(node->data().role)) { if (!IsAlert(node->data().role)) {
bool old_state = !old_value.empty() && old_value != "off"; bool old_state = !old_value.empty() && old_value != "off";
bool new_state = !new_value.empty() && new_value != "off"; bool new_state = !new_value.empty() && new_value != "off";
...@@ -263,8 +259,6 @@ void AXEventGenerator::OnStringAttributeChanged(AXTree* tree, ...@@ -263,8 +259,6 @@ void AXEventGenerator::OnStringAttributeChanged(AXTree* tree,
if (node != tree->root()) if (node != tree->root())
AddEvent(node, Event::NAME_CHANGED); AddEvent(node, Event::NAME_CHANGED);
// TODO(dtseng): tree in the midst of updates. Disallow
// access to |node|.
if (node->data().HasStringAttribute( if (node->data().HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus)) { ax::mojom::StringAttribute::kContainerLiveStatus)) {
FireLiveRegionEvents(node); FireLiveRegionEvents(node);
......
...@@ -165,9 +165,9 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver { ...@@ -165,9 +165,9 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver {
protected: protected:
// AXTreeObserver overrides. // AXTreeObserver overrides.
void OnNodeDataWillChange(AXTree* tree, void OnNodeDataChanged(AXTree* tree,
const AXNodeData& old_node_data, const AXNodeData& old_node_data,
const AXNodeData& new_node_data) override; const AXNodeData& new_node_data) override;
void OnRoleChanged(AXTree* tree, void OnRoleChanged(AXTree* tree,
AXNode* node, AXNode* node,
ax::mojom::Role old_role, ax::mojom::Role old_role,
......
...@@ -649,6 +649,10 @@ base::Optional<int> AXNode::GetPosInSet() { ...@@ -649,6 +649,10 @@ base::Optional<int> AXNode::GetPosInSet() {
return base::nullopt; return base::nullopt;
} }
if (data().HasState(ax::mojom::State::kIgnored)) {
return base::nullopt;
}
const AXNode* ordered_set = GetOrderedSet(); const AXNode* ordered_set = GetOrderedSet();
if (!ordered_set) { if (!ordered_set) {
return base::nullopt; return base::nullopt;
...@@ -669,6 +673,10 @@ base::Optional<int> AXNode::GetSetSize() { ...@@ -669,6 +673,10 @@ base::Optional<int> AXNode::GetSetSize() {
if (!(IsOrderedSetItem() || IsOrderedSet())) if (!(IsOrderedSetItem() || IsOrderedSet()))
return base::nullopt; return base::nullopt;
if (data().HasState(ax::mojom::State::kIgnored)) {
return base::nullopt;
}
// If node is item-like, find its outerlying ordered set. Otherwise, // If node is item-like, find its outerlying ordered set. Otherwise,
// this node is the ordered set. // this node is the ordered set.
const AXNode* ordered_set = this; const AXNode* ordered_set = this;
......
...@@ -239,7 +239,9 @@ struct PendingStructureChanges { ...@@ -239,7 +239,9 @@ struct PendingStructureChanges {
// Intermediate state to keep track of during a tree update. // Intermediate state to keep track of during a tree update.
struct AXTreeUpdateState { struct AXTreeUpdateState {
AXTreeUpdateState(const AXTree& tree) AXTreeUpdateState(const AXTree& tree)
: computing_pending_changes(false), new_root(nullptr), tree(tree) {} : computing_pending_changes(false),
root_will_be_created(false),
tree(tree) {}
// Returns whether this update removes |node|. // Returns whether this update removes |node|.
bool IsRemovedNode(const AXNode* node) const { bool IsRemovedNode(const AXNode* node) const {
...@@ -446,6 +448,12 @@ struct AXTreeUpdateState { ...@@ -446,6 +448,12 @@ struct AXTreeUpdateState {
// during an update before the update applies changes. // during an update before the update applies changes.
base::Optional<AXNode::AXID> pending_root_id; base::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 // During an update, this keeps track of all nodes that have been
// implicitly referenced as part of this update, but haven't 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 // updated yet. It's an error if there are any pending nodes at the
...@@ -467,9 +475,6 @@ struct AXTreeUpdateState { ...@@ -467,9 +475,6 @@ struct AXTreeUpdateState {
// Keeps track of new nodes created during this update. // Keeps track of new nodes created during this update.
std::set<AXNode::AXID> new_node_ids; std::set<AXNode::AXID> new_node_ids;
// The new root in this update, if any.
AXNode* new_root;
// Keeps track of any nodes removed. Nodes are removed when their AXID no // 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 // 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|. // subtree of the AXID that was explicitally cleared with |node_id_to_clear|.
...@@ -481,9 +486,9 @@ struct AXTreeUpdateState { ...@@ -481,9 +486,9 @@ struct AXTreeUpdateState {
std::map<AXNode::AXID, std::unique_ptr<PendingStructureChanges>> std::map<AXNode::AXID, std::unique_ptr<PendingStructureChanges>>
node_id_to_pending_data; node_id_to_pending_data;
// Maps between a node id and its data. We need to keep this around because // Maps between a node id and the data it owned before being updated.
// the reparented nodes in this update were actually deleted. // We need to keep this around in order to correctly fire post-update events.
std::map<AXNode::AXID, AXNodeData> reparented_node_id_to_data; std::map<AXNode::AXID, AXNodeData> old_node_id_to_data;
// Optional copy of the old tree data, only populated when the tree // Optional copy of the old tree data, only populated when the tree
// data has changed. // data has changed.
...@@ -790,6 +795,23 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) { ...@@ -790,6 +795,23 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) {
} }
} }
// 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);
}
}
// Handle |node_id_to_clear| before applying ordinary node updates. // Handle |node_id_to_clear| before applying ordinary node updates.
// We distinguish between updating the root, e.g. changing its children or // 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 // some of its attributes, or replacing the root completely. If the root is
...@@ -829,6 +851,8 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) { ...@@ -829,6 +851,8 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) {
} }
} }
DCHECK_EQ(!GetFromId(update.root_id), update_state.root_will_be_created);
// Update the tree data, do not call |UpdateData| since we want to defer // Update the tree data, do not call |UpdateData| since we want to defer
// the |OnTreeDataChanged| event until after the tree has finished updating. // the |OnTreeDataChanged| event until after the tree has finished updating.
if (update.has_tree_data && data_ != update.tree_data) { if (update.has_tree_data && data_ != update.tree_data) {
...@@ -836,11 +860,10 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) { ...@@ -836,11 +860,10 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) {
data_ = update.tree_data; data_ = update.tree_data;
} }
const bool root_exists = GetFromId(update.root_id) != nullptr;
// Update all of the nodes in the update. // Update all of the nodes in the update.
for (size_t i = 0; i < update.nodes.size(); ++i) { for (size_t i = 0; i < update.nodes.size(); ++i) {
bool is_new_root = !root_exists && update.nodes[i].id == update.root_id; 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)) if (!UpdateNode(update.nodes[i], is_new_root, &update_state))
return false; return false;
} }
...@@ -926,6 +949,9 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) { ...@@ -926,6 +949,9 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) {
node->UpdateUnignoredCachedValues(); node->UpdateUnignoredCachedValues();
} }
// Tree is no longer updating.
SetTreeUpdateInProgressState(false);
// Now that the tree is stable and its nodes have been updated, notify if // 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 // 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. // root has been replaced, so observers have the most up-to-date information.
...@@ -944,14 +970,24 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) { ...@@ -944,14 +970,24 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) {
// node changes. // node changes.
for (AXNode::AXID node_data_changed_id : update_state.node_data_changed_ids) { for (AXNode::AXID node_data_changed_id : update_state.node_data_changed_ids) {
AXNode* node = GetFromId(node_data_changed_id); AXNode* node = GetFromId(node_data_changed_id);
if (node) { DCHECK(node);
for (AXTreeObserver& observer : observers_)
observer.OnNodeChanged(this, 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());
}
} }
}
// Tree is no longer updating. // |OnNodeChanged| should be fired for all nodes that have been updated.
SetTreeUpdateInProgressState(false); for (AXTreeObserver& observer : observers_)
observer.OnNodeChanged(this, node);
}
for (AXTreeObserver& observer : observers_) { for (AXTreeObserver& observer : observers_) {
observer.OnAtomicUpdateFinished(this, root_->id() != old_root_id, changes); observer.OnAtomicUpdateFinished(this, root_->id() != old_root_id, changes);
...@@ -961,6 +997,7 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) { ...@@ -961,6 +997,7 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) {
} }
AXTableInfo* AXTree::GetTableInfo(const AXNode* const_table_node) const { AXTableInfo* AXTree::GetTableInfo(const AXNode* const_table_node) const {
DCHECK(!GetTreeUpdateInProgressState());
// Note: the const_casts are here because we want this function to be able // 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 // to be called from a const virtual function on AXNode. AXTableInfo is
// computed on demand and cached, but that's an implementation detail // computed on demand and cached, but that's an implementation detail
...@@ -1064,14 +1101,15 @@ bool AXTree::ComputePendingChanges(const AXTreeUpdate& update, ...@@ -1064,14 +1101,15 @@ bool AXTree::ComputePendingChanges(const AXTreeUpdate& update,
} }
} }
const bool root_exists = update_state.root_will_be_created =
!!GetFromId(update.root_id) && !GetFromId(update.root_id) ||
update_state.ShouldPendingNodeExistInTree(update.root_id); !update_state.ShouldPendingNodeExistInTree(update.root_id);
// Populate |update_state| with all of the changes that will be performed // Populate |update_state| with all of the changes that will be performed
// on the tree during the update. // on the tree during the update.
for (const AXNodeData& new_data : update.nodes) { for (const AXNodeData& new_data : update.nodes) {
bool is_new_root = !root_exists && new_data.id == update.root_id; 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)) { if (!ComputePendingChangesToNode(new_data, is_new_root, &update_state)) {
return false; return false;
} }
...@@ -1219,19 +1257,12 @@ bool AXTree::UpdateNode(const AXNodeData& src, ...@@ -1219,19 +1257,12 @@ bool AXTree::UpdateNode(const AXNodeData& src,
AXNode* node = GetFromId(src.id); AXNode* node = GetFromId(src.id);
if (node) { if (node) {
update_state->pending_nodes.erase(node->id()); update_state->pending_nodes.erase(node->id());
UpdateReverseRelations(node, src);
// TODO(accessibility): CallNodeChangeCallbacks should not pass |node|,
// since the tree and the node data are not yet in a consistent
// state. Possibly only pass id.
if (!update_state->IsCreatedNode(node) || if (!update_state->IsCreatedNode(node) ||
update_state->IsReparentedNode(node)) { update_state->IsReparentedNode(node)) {
auto it = update_state->reparented_node_id_to_data.find(node->id()); update_state->old_node_id_to_data.insert(
const AXNodeData& old_data = std::make_pair(node->id(), node->TakeData()));
it != update_state->reparented_node_id_to_data.end() ? it->second
: node->data();
CallNodeChangeCallbacks(node, old_data, src);
} }
UpdateReverseRelations(node, src);
node->SetData(src); node->SetData(src);
} else { } else {
if (!is_new_root) { if (!is_new_root) {
...@@ -1240,8 +1271,7 @@ bool AXTree::UpdateNode(const AXNodeData& src, ...@@ -1240,8 +1271,7 @@ bool AXTree::UpdateNode(const AXNodeData& src,
return false; return false;
} }
update_state->new_root = CreateNode(nullptr, src.id, 0, update_state); node = CreateNode(nullptr, src.id, 0, update_state);
node = update_state->new_root;
UpdateReverseRelations(node, src); UpdateReverseRelations(node, src);
node->SetData(src); node->SetData(src);
} }
...@@ -1327,14 +1357,23 @@ void AXTree::NotifyNodeHasBeenReparentedOrCreated( ...@@ -1327,14 +1357,23 @@ void AXTree::NotifyNodeHasBeenReparentedOrCreated(
} }
} }
void AXTree::CallNodeChangeCallbacks(AXNode* node, void AXTree::NotifyNodeDataWillChange(const AXNodeData& old_data,
const AXNodeData& old_data, const AXNodeData& new_data) {
const AXNodeData& new_data) { if (new_data.id == AXNode::kInvalidAXID)
if (node->id() == AXNode::kInvalidAXID)
return; return;
for (AXTreeObserver& observer : observers_) for (AXTreeObserver& observer : observers_)
observer.OnNodeDataWillChange(this, old_data, new_data); observer.OnNodeDataWillChange(this, old_data, new_data);
}
void AXTree::NotifyNodeDataHasBeenChanged(AXNode* node,
const AXNodeData& old_data,
const AXNodeData& new_data) {
if (node->id() == AXNode::kInvalidAXID)
return;
for (AXTreeObserver& observer : observers_)
observer.OnNodeDataChanged(this, old_data, new_data);
if (old_data.role != new_data.role) { if (old_data.role != new_data.role) {
for (AXTreeObserver& observer : observers_) for (AXTreeObserver& observer : observers_)
...@@ -1590,11 +1629,11 @@ void AXTree::DestroyNodeAndSubtree(AXNode* node, ...@@ -1590,11 +1629,11 @@ void AXTree::DestroyNodeAndSubtree(AXNode* node,
update_state->DecrementPendingDestroyNodeCount(node->id()); update_state->DecrementPendingDestroyNodeCount(node->id());
update_state->removed_node_ids.insert(node->id()); update_state->removed_node_ids.insert(node->id());
update_state->new_node_ids.erase(node->id()); update_state->new_node_ids.erase(node->id());
} update_state->node_data_changed_ids.erase(node->id());
if (update_state->IsReparentedNode(node)) {
if (update_state && update_state->IsReparentedNode(node)) { update_state->old_node_id_to_data.emplace(
update_state->reparented_node_id_to_data.insert( std::make_pair(node->id(), node->TakeData()));
std::make_pair(node->id(), node->TakeData())); }
} }
node->Destroy(); node->Destroy();
} }
......
...@@ -214,9 +214,14 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { ...@@ -214,9 +214,14 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree {
AXNode* node, AXNode* node,
const AXTreeUpdateState* update_state); const AXTreeUpdateState* update_state);
void CallNodeChangeCallbacks(AXNode* node, // Notify the delegate that a node will change its data.
const AXNodeData& old_data, void NotifyNodeDataWillChange(const AXNodeData& old_data,
const AXNodeData& new_data); const AXNodeData& new_data);
// Notify the delegate that |node| has changed its data.
void NotifyNodeDataHasBeenChanged(AXNode* node,
const AXNodeData& old_data,
const AXNodeData& new_data);
void UpdateReverseRelations(AXNode* node, const AXNodeData& new_data); void UpdateReverseRelations(AXNode* node, const AXNodeData& new_data);
......
...@@ -18,33 +18,34 @@ struct AXTreeData; ...@@ -18,33 +18,34 @@ struct AXTreeData;
// Used when you want to be notified when changes happen to an AXTree. // Used when you want to be notified when changes happen to an AXTree.
// //
// Some of the notifications are called in the middle of an update operation. // |OnAtomicUpdateFinished| is notified at the end of an atomic update.
// Be careful, as the tree may be in an inconsistent state at this time; // It provides a vector of nodes that were added or changed, for final
// don't walk the parents and children at this time: // postprocessing.
// OnNodeWillBeDeleted
// OnSubtreeWillBeDeleted
// OnNodeWillBeReparented
// OnSubtreeWillBeReparented
// OnNodeCreated
// OnNodeReparented
// OnNodeChanged
//
// In addition, one additional notification is fired at the end of an
// atomic update, and it provides a vector of nodes that were added or
// changed, for final postprocessing:
// OnAtomicUpdateFinished
//
class AX_EXPORT AXTreeObserver : public base::CheckedObserver { class AX_EXPORT AXTreeObserver : public base::CheckedObserver {
public: public:
AXTreeObserver(); AXTreeObserver();
~AXTreeObserver() override; ~AXTreeObserver() override;
// Called before a node's data gets updated. // Called before any tree modifications have occurred, notifying that a single
// node will change its data. Its id and data will be valid, but its links to
// parents and children are only valid within this callstack. Do not hold a
// reference to the node or any relative nodes such as ancestors or
// descendants described by the node data outside of this event.
virtual void OnNodeDataWillChange(AXTree* tree, virtual void OnNodeDataWillChange(AXTree* tree,
const AXNodeData& old_node_data, const AXNodeData& old_node_data,
const AXNodeData& new_node_data) {} const AXNodeData& new_node_data) {}
// Called after all tree modifications have occurred, notifying that a single
// node has changed its data. Its id, data, and links to parent and children
// will all be valid, since the tree is in a stable state after updating.
virtual void OnNodeDataChanged(AXTree* tree,
const AXNodeData& old_node_data,
const AXNodeData& new_node_data) {}
// Individual callbacks for every attribute of AXNodeData that can change. // Individual callbacks for every attribute of AXNodeData that can change.
// Called after all tree mutations have occurred, notifying that a single node
// changed its data. Its id, data, and links to parent and children will all
// be valid, since the tree is in a stable state after updating.
virtual void OnRoleChanged(AXTree* tree, virtual void OnRoleChanged(AXTree* tree,
AXNode* node, AXNode* node,
ax::mojom::Role old_role, ax::mojom::Role old_role,
...@@ -115,8 +116,10 @@ class AX_EXPORT AXTreeObserver : public base::CheckedObserver { ...@@ -115,8 +116,10 @@ class AX_EXPORT AXTreeObserver : public base::CheckedObserver {
// Same as |OnNodeCreated|, but called for nodes that have been reparented. // Same as |OnNodeCreated|, but called for nodes that have been reparented.
virtual void OnNodeReparented(AXTree* tree, AXNode* node) {} virtual void OnNodeReparented(AXTree* tree, AXNode* node) {}
// Called when a node changes its data or children. The tree may be in // Called after all tree mutations have occurred, notifying that a single node
// the middle of an update, don't walk the parents and children now. // has updated its data or children. Its id, data, and links to parent and
// children will all be valid, since the tree is in a stable state after
// updating.
virtual void OnNodeChanged(AXTree* tree, AXNode* node) {} virtual void OnNodeChanged(AXTree* tree, AXNode* node) {}
enum ChangeType { enum ChangeType {
......
...@@ -66,6 +66,9 @@ class TestAXTreeObserver : public AXTreeObserver { ...@@ -66,6 +66,9 @@ class TestAXTreeObserver : public AXTreeObserver {
void OnNodeDataWillChange(AXTree* tree, void OnNodeDataWillChange(AXTree* tree,
const AXNodeData& old_node_data, const AXNodeData& old_node_data,
const AXNodeData& new_node_data) override {} const AXNodeData& new_node_data) override {}
void OnNodeDataChanged(AXTree* tree,
const AXNodeData& old_node_data,
const AXNodeData& new_node_data) override {}
void OnTreeDataChanged(AXTree* tree, void OnTreeDataChanged(AXTree* tree,
const ui::AXTreeData& old_data, const ui::AXTreeData& old_data,
const ui::AXTreeData& new_data) override { const ui::AXTreeData& new_data) override {
...@@ -104,8 +107,6 @@ class TestAXTreeObserver : public AXTreeObserver { ...@@ -104,8 +107,6 @@ class TestAXTreeObserver : public AXTreeObserver {
void OnNodeChanged(AXTree* tree, AXNode* node) override { void OnNodeChanged(AXTree* tree, AXNode* node) override {
changed_ids_.push_back(node->id()); changed_ids_.push_back(node->id());
if (call_posinset_and_setsize)
AssertPosinsetAndSetsizeNotSet(node);
} }
void OnAtomicUpdateFinished(AXTree* tree, void OnAtomicUpdateFinished(AXTree* tree,
...@@ -236,12 +237,6 @@ class TestAXTreeObserver : public AXTreeObserver { ...@@ -236,12 +237,6 @@ class TestAXTreeObserver : public AXTreeObserver {
return attribute_change_log_; return attribute_change_log_;
} }
bool call_posinset_and_setsize = false;
void AssertPosinsetAndSetsizeNotSet(AXNode* node) {
ASSERT_FALSE(node->GetPosInSet());
ASSERT_FALSE(node->GetSetSize());
}
private: private:
AXTree* tree_; AXTree* tree_;
bool tree_data_changed_; bool tree_data_changed_;
...@@ -2992,20 +2987,78 @@ TEST(AXTreeTest, TestSetSizePosInSetSubtreeDeleted) { ...@@ -2992,20 +2987,78 @@ TEST(AXTreeTest, TestSetSizePosInSetSubtreeDeleted) {
initial_state.nodes[2].role = ax::mojom::Role::kTreeItem; initial_state.nodes[2].role = ax::mojom::Role::kTreeItem;
AXTree tree(initial_state); AXTree tree(initial_state);
// This should work normally. AXNode* tree_node = tree.GetFromId(1);
AXNode* item = tree.GetFromId(3); AXNode* item = tree.GetFromId(3);
// This should work normally.
EXPECT_OPTIONAL_EQ(2, item->GetPosInSet()); EXPECT_OPTIONAL_EQ(2, item->GetPosInSet());
EXPECT_OPTIONAL_EQ(2, item->GetSetSize()); EXPECT_OPTIONAL_EQ(2, item->GetSetSize());
// Use test observer to assert posinset and setsize are 0.
TestAXTreeObserver test_observer(&tree);
test_observer.call_posinset_and_setsize = true;
// Remove item from tree. // Remove item from tree.
AXTreeUpdate tree_update = initial_state; AXTreeUpdate tree_update = initial_state;
tree_update.nodes.resize(1); tree_update.nodes.resize(1);
tree_update.nodes[0].child_ids = {2}; tree_update.nodes[0].child_ids = {2};
ASSERT_TRUE(tree.Unserialize(tree_update)); ASSERT_TRUE(tree.Unserialize(tree_update));
// These values are lazily created, so to test that they fail when
// called in the middle of a tree update, fake the update state.
tree.SetTreeUpdateInProgressState(true);
ASSERT_FALSE(tree_node->GetPosInSet());
ASSERT_FALSE(tree_node->GetSetSize());
// Then reset the state to make sure we have the expected values
// after |Unserialize|.
tree.SetTreeUpdateInProgressState(false);
ASSERT_FALSE(tree_node->GetPosInSet());
EXPECT_OPTIONAL_EQ(1, tree_node->GetSetSize());
}
// Tests that GetPosInSet and GetSetSize work when there are ignored nodes.
TEST(AXTreeTest, TestSetSizePosInSetIgnoredItem) {
AXTreeUpdate initial_state;
initial_state.root_id = 1;
initial_state.nodes.resize(3);
initial_state.nodes[0].id = 1;
initial_state.nodes[0].role = ax::mojom::Role::kTree;
initial_state.nodes[0].child_ids = {2, 3};
initial_state.nodes[1].id = 2;
initial_state.nodes[1].role = ax::mojom::Role::kTreeItem;
initial_state.nodes[2].id = 3;
initial_state.nodes[2].role = ax::mojom::Role::kTreeItem;
AXTree tree(initial_state);
AXNode* tree_node = tree.GetFromId(1);
AXNode* item1 = tree.GetFromId(2);
AXNode* item2 = tree.GetFromId(3);
// This should work normally.
ASSERT_FALSE(tree_node->GetPosInSet());
EXPECT_OPTIONAL_EQ(2, tree_node->GetSetSize());
EXPECT_OPTIONAL_EQ(1, item1->GetPosInSet());
EXPECT_OPTIONAL_EQ(2, item1->GetSetSize());
EXPECT_OPTIONAL_EQ(2, item2->GetPosInSet());
EXPECT_OPTIONAL_EQ(2, item2->GetSetSize());
// Remove item from tree.
AXTreeUpdate tree_update;
tree_update.nodes.resize(1);
tree_update.nodes[0] = initial_state.nodes[1];
tree_update.nodes[0].AddState(ax::mojom::State::kIgnored);
ASSERT_TRUE(tree.Unserialize(tree_update));
ASSERT_FALSE(tree_node->GetPosInSet());
EXPECT_OPTIONAL_EQ(1, tree_node->GetSetSize());
// Ignored nodes are not part of ordered sets.
EXPECT_FALSE(item1->GetPosInSet());
EXPECT_FALSE(item1->GetSetSize());
EXPECT_OPTIONAL_EQ(1, item2->GetPosInSet());
EXPECT_OPTIONAL_EQ(1, item2->GetSetSize());
} }
// Tests that kPopUpButtons are assigned the SetSize of the wrapped // Tests that kPopUpButtons are assigned the SetSize of the wrapped
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment