Commit 83388678 authored by Dominic Mazzoni's avatar Dominic Mazzoni Committed by Commit Bot

Fire live region events when a node is removed.

Adds an AXLiveRegionTracker class to keep track of live regions
in an AXTree. Uses it to fix a bug where we weren't firing the
LIVE_REGION_CHANGED event on the live root when a node was removed,
only when a node was added or changed.

I'm going to follow this up with code that optionally computes
the text of a live region change, that we can use on Android,
Chrome OS, and some older versions of macOS. So AXLiveRegionTracker
is simple now, but it will be a convenient place to put that
logic.

Bug: 560599, 930763
Change-Id: Iaff11e5adbdde533b8868226ed563567edc5589e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1464325
Commit-Queue: Dominic Mazzoni <dmazzoni@chromium.org>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#727589}
parent 4d53d204
...@@ -62,6 +62,8 @@ jumbo_component("accessibility") { ...@@ -62,6 +62,8 @@ jumbo_component("accessibility") {
"ax_export.h", "ax_export.h",
"ax_language_detection.cc", "ax_language_detection.cc",
"ax_language_detection.h", "ax_language_detection.h",
"ax_live_region_tracker.cc",
"ax_live_region_tracker.h",
"ax_mode.cc", "ax_mode.cc",
"ax_mode.h", "ax_mode.h",
"ax_mode_observer.h", "ax_mode_observer.h",
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include "base/stl_util.h" #include "base/stl_util.h"
#include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_live_region_tracker.h"
#include "ui/accessibility/ax_node.h" #include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h" #include "ui/accessibility/ax_role_properties.h"
...@@ -100,8 +101,10 @@ AXEventGenerator::TargetedEvent AXEventGenerator::Iterator::operator*() const { ...@@ -100,8 +101,10 @@ AXEventGenerator::TargetedEvent AXEventGenerator::Iterator::operator*() const {
AXEventGenerator::AXEventGenerator() = default; AXEventGenerator::AXEventGenerator() = default;
AXEventGenerator::AXEventGenerator(AXTree* tree) : tree_(tree) { AXEventGenerator::AXEventGenerator(AXTree* tree) : tree_(tree) {
if (tree_) if (tree_) {
tree_->AddObserver(this); tree_->AddObserver(this);
live_region_tracker_ = std::make_unique<AXLiveRegionTracker>(tree_);
}
} }
AXEventGenerator::~AXEventGenerator() { AXEventGenerator::~AXEventGenerator() {
...@@ -110,11 +113,15 @@ AXEventGenerator::~AXEventGenerator() { ...@@ -110,11 +113,15 @@ AXEventGenerator::~AXEventGenerator() {
} }
void AXEventGenerator::SetTree(AXTree* new_tree) { void AXEventGenerator::SetTree(AXTree* new_tree) {
if (tree_) if (tree_) {
tree_->RemoveObserver(this); tree_->RemoveObserver(this);
live_region_tracker_.reset();
}
tree_ = new_tree; tree_ = new_tree;
if (tree_) if (tree_) {
tree_->AddObserver(this); tree_->AddObserver(this);
live_region_tracker_ = std::make_unique<AXLiveRegionTracker>(tree_);
}
} }
void AXEventGenerator::ReleaseTree() { void AXEventGenerator::ReleaseTree() {
...@@ -475,6 +482,12 @@ void AXEventGenerator::OnTreeDataChanged(AXTree* tree, ...@@ -475,6 +482,12 @@ void AXEventGenerator::OnTreeDataChanged(AXTree* tree,
} }
void AXEventGenerator::OnNodeWillBeDeleted(AXTree* tree, AXNode* node) { void AXEventGenerator::OnNodeWillBeDeleted(AXTree* tree, AXNode* node) {
if (node->data().HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus)) {
FireLiveRegionEvents(node);
live_region_tracker_->OnNodeWillBeDeleted(node);
}
DCHECK_EQ(tree_, tree); DCHECK_EQ(tree_, tree);
tree_events_.erase(node); tree_events_.erase(node);
} }
...@@ -506,6 +519,14 @@ void AXEventGenerator::OnAtomicUpdateFinished( ...@@ -506,6 +519,14 @@ void AXEventGenerator::OnAtomicUpdateFinished(
} }
for (const auto& change : changes) { for (const auto& change : changes) {
if ((change.type == NODE_CREATED || change.type == SUBTREE_CREATED ||
change.type == NODE_REPARENTED || change.type == SUBTREE_REPARENTED)) {
if (change.node->data().HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus)) {
live_region_tracker_->TrackNode(change.node);
}
}
if (change.type == SUBTREE_CREATED) { if (change.type == SUBTREE_CREATED) {
AddEvent(change.node, Event::SUBTREE_CREATED); AddEvent(change.node, Event::SUBTREE_CREATED);
} else if (change.type != NODE_CREATED) { } else if (change.type != NODE_CREATED) {
...@@ -525,23 +546,29 @@ void AXEventGenerator::OnAtomicUpdateFinished( ...@@ -525,23 +546,29 @@ void AXEventGenerator::OnAtomicUpdateFinished(
} }
void AXEventGenerator::FireLiveRegionEvents(AXNode* node) { void AXEventGenerator::FireLiveRegionEvents(AXNode* node) {
AXNode* live_root = node; AXNode* live_root = live_region_tracker_->GetLiveRoot(node);
while (live_root && !live_root->data().HasStringAttribute(
ax::mojom::StringAttribute::kLiveStatus)) // Note that |live_root| might be nullptr if a live region was just added.
live_root = live_root->parent(); if (!live_root)
return;
if (live_root &&
!live_root->data().GetBoolAttribute(ax::mojom::BoolAttribute::kBusy) && if (live_root->data().GetBoolAttribute(ax::mojom::BoolAttribute::kBusy))
live_root->data().GetStringAttribute( return;
ax::mojom::StringAttribute::kLiveStatus) != "off") {
// Fire LIVE_REGION_NODE_CHANGED on each node that changed. std::string live_status = live_root->data().GetStringAttribute(
if (!node->data() ax::mojom::StringAttribute::kLiveStatus);
.GetStringAttribute(ax::mojom::StringAttribute::kName) if (live_status != "polite" && live_status != "assertive")
.empty()) return;
AddEvent(node, Event::LIVE_REGION_NODE_CHANGED);
// Fire LIVE_REGION_CHANGED on the root of the live region. // Fire LIVE_REGION_NODE_CHANGED on each node that changed.
AddEvent(live_root, Event::LIVE_REGION_CHANGED); if (!node->data()
.GetStringAttribute(ax::mojom::StringAttribute::kName)
.empty()) {
AddEvent(node, Event::LIVE_REGION_NODE_CHANGED);
} }
// Fire LIVE_REGION_CHANGED on the root of the live region.
AddEvent(live_root, Event::LIVE_REGION_CHANGED);
} }
void AXEventGenerator::FireActiveDescendantEvents() { void AXEventGenerator::FireActiveDescendantEvents() {
......
...@@ -6,8 +6,10 @@ ...@@ -6,8 +6,10 @@
#define UI_ACCESSIBILITY_AX_EVENT_GENERATOR_H_ #define UI_ACCESSIBILITY_AX_EVENT_GENERATOR_H_
#include <map> #include <map>
#include <memory>
#include <ostream> #include <ostream>
#include <set> #include <set>
#include <string>
#include <vector> #include <vector>
#include "ui/accessibility/ax_export.h" #include "ui/accessibility/ax_export.h"
...@@ -16,6 +18,8 @@ ...@@ -16,6 +18,8 @@
namespace ui { namespace ui {
class AXLiveRegionTracker;
// Subclass of AXTreeObserver that automatically generates AXEvents to fire // Subclass of AXTreeObserver that automatically generates AXEvents to fire
// based on changes to an accessibility tree. Every platform // based on changes to an accessibility tree. Every platform
// tends to want different events, so this class lets each platform // tends to want different events, so this class lets each platform
...@@ -232,6 +236,9 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver { ...@@ -232,6 +236,9 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver {
// Valid between the call to OnIntAttributeChanged and the call to // Valid between the call to OnIntAttributeChanged and the call to
// OnAtomicUpdateFinished. List of nodes whose active descendant changed. // OnAtomicUpdateFinished. List of nodes whose active descendant changed.
std::vector<AXNode*> active_descendant_changed_; std::vector<AXNode*> active_descendant_changed_;
// Helper that tracks live regions.
std::unique_ptr<AXLiveRegionTracker> live_region_tracker_;
}; };
AX_EXPORT std::ostream& operator<<(std::ostream& os, AX_EXPORT std::ostream& operator<<(std::ostream& os,
......
...@@ -1370,4 +1370,41 @@ TEST(AXEventGeneratorTest, MultilineStateChanged) { ...@@ -1370,4 +1370,41 @@ TEST(AXEventGeneratorTest, MultilineStateChanged) {
HasEventAtNode(AXEventGenerator::Event::STATE_CHANGED, 1))); HasEventAtNode(AXEventGenerator::Event::STATE_CHANGED, 1)));
} }
TEST(AXEventGeneratorTest, LiveRegionNodeRemoved) {
AXTreeUpdate initial_state;
initial_state.root_id = 1;
initial_state.nodes.resize(3);
initial_state.nodes[0].id = 1;
initial_state.nodes[0].AddStringAttribute(
ax::mojom::StringAttribute::kLiveStatus, "polite");
initial_state.nodes[0].AddStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus, "polite");
initial_state.nodes[0].child_ids = {2, 3};
initial_state.nodes[1].id = 2;
initial_state.nodes[1].role = ax::mojom::Role::kStaticText;
initial_state.nodes[1].AddStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus, "polite");
initial_state.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kName,
"Before 1");
initial_state.nodes[2].id = 3;
initial_state.nodes[2].role = ax::mojom::Role::kStaticText;
initial_state.nodes[2].AddStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus, "polite");
initial_state.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kName,
"Before 2");
AXTree tree(initial_state);
AXEventGenerator event_generator(&tree);
AXTreeUpdate update = initial_state;
update.nodes.resize(1);
update.nodes[0].child_ids = {2};
EXPECT_TRUE(tree.Unserialize(update));
EXPECT_THAT(
event_generator,
UnorderedElementsAre(
HasEventAtNode(AXEventGenerator::Event::CHILDREN_CHANGED, 1),
HasEventAtNode(AXEventGenerator::Event::LIVE_REGION_CHANGED, 1)));
}
} // namespace ui } // namespace ui
// Copyright 2019 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 "ui/accessibility/ax_live_region_tracker.h"
#include "base/stl_util.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h"
namespace ui {
AXLiveRegionTracker::AXLiveRegionTracker(AXTree* tree) : tree_(tree) {
InitializeLiveRegionNodeToRoot(tree_->root(), nullptr);
}
AXLiveRegionTracker::~AXLiveRegionTracker() {}
void AXLiveRegionTracker::TrackNode(AXNode* node) {
LOG(ERROR) << "TrackNode " << node->data().ToString();
AXNode* live_root = node;
while (live_root && !live_root->HasStringAttribute(
ax::mojom::StringAttribute::kLiveStatus))
live_root = live_root->parent();
if (live_root) {
LOG(ERROR) << " Live root: " << live_root->data().ToString();
live_region_node_to_root_[node] = live_root;
} else
LOG(ERROR) << " No live root";
}
void AXLiveRegionTracker::OnNodeWillBeDeleted(AXNode* node) {
LOG(ERROR) << "OnNodeWillBeDeleted " << node->data().ToString();
live_region_node_to_root_.erase(node);
}
AXNode* AXLiveRegionTracker::GetLiveRoot(AXNode* node) {
LOG(ERROR) << "GetLiveRoot for " << node->data().ToString();
auto iter = live_region_node_to_root_.find(node);
if (iter != live_region_node_to_root_.end())
return iter->second;
return nullptr;
}
void AXLiveRegionTracker::InitializeLiveRegionNodeToRoot(AXNode* node,
AXNode* current_root) {
if (!current_root &&
node->HasStringAttribute(ax::mojom::StringAttribute::kLiveStatus)) {
current_root = node;
}
if (current_root)
live_region_node_to_root_[node] = current_root;
for (size_t i = 0; i < node->children().size(); i++)
InitializeLiveRegionNodeToRoot(node->children()[i], current_root);
}
} // namespace ui
// Copyright 2019 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.
#ifndef UI_ACCESSIBILITY_AX_LIVE_REGION_TRACKER_H_
#define UI_ACCESSIBILITY_AX_LIVE_REGION_TRACKER_H_
#include <map>
#include <set>
#include <vector>
#include "ui/accessibility/ax_tree.h"
namespace ui {
// Class that works with AXEventGenerator to track live regions in
// an AXTree.
class AXLiveRegionTracker {
public:
explicit AXLiveRegionTracker(AXTree* tree);
~AXLiveRegionTracker();
void TrackNode(AXNode* node);
void OnNodeWillBeDeleted(AXNode* node);
AXNode* GetLiveRoot(AXNode* node);
private:
void InitializeLiveRegionNodeToRoot(AXNode* node, AXNode* current_root);
AXTree* tree_; // Not owned.
// Map from live region node to its live region root.
std::map<AXNode*, AXNode*> live_region_node_to_root_;
DISALLOW_COPY_AND_ASSIGN(AXLiveRegionTracker);
};
} // namespace ui
#endif // UI_ACCESSIBILITY_AX_LIVE_REGION_TRACKER_H_
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