#include "ui/accessibility/ax_event_generator.h"
#include "base/containers/contains.h"
#include "base/no_destructor.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_event.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h"
namespace ui {
namespace {
bool HasEvent(const std::set<AXEventGenerator::EventParams>& node_events,
AXEventGenerator::Event event) {
return node_events.count(AXEventGenerator::EventParams(event));
}
void RemoveEvent(std::set<AXEventGenerator::EventParams>* node_events,
AXEventGenerator::Event event) {
node_events->erase(AXEventGenerator::EventParams(event));
}
void RemoveEventsDueToIgnoredChanged(
std::set<AXEventGenerator::EventParams>* node_events) {
RemoveEvent(node_events,
AXEventGenerator::Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED);
RemoveEvent(node_events, AXEventGenerator::Event::CHILDREN_CHANGED);
RemoveEvent(node_events, AXEventGenerator::Event::DESCRIPTION_CHANGED);
RemoveEvent(node_events, AXEventGenerator::Event::NAME_CHANGED);
RemoveEvent(node_events, AXEventGenerator::Event::OBJECT_ATTRIBUTE_CHANGED);
RemoveEvent(node_events, AXEventGenerator::Event::PARENT_CHANGED);
RemoveEvent(node_events, AXEventGenerator::Event::SORT_CHANGED);
RemoveEvent(node_events, AXEventGenerator::Event::TEXT_ATTRIBUTE_CHANGED);
RemoveEvent(node_events,
AXEventGenerator::Event::WIN_IACCESSIBLE_STATE_CHANGED);
}
void AddIgnoredChangedState(
const AXEventGenerator::IgnoredChangedState& state,
AXEventGenerator::IgnoredChangedStatesBitset* ignored_changed_states) {
ignored_changed_states->set(static_cast<size_t>(state));
}
bool HasIgnoredChangedState(
const AXEventGenerator::IgnoredChangedStatesBitset& ignored_changed_states,
const AXEventGenerator::IgnoredChangedState& state) {
return ignored_changed_states[static_cast<size_t>(state)];
}
}
AXEventGenerator::EventParams::EventParams(const Event event) : event(event) {}
AXEventGenerator::EventParams::EventParams(
const Event event,
const ax::mojom::EventFrom event_from,
const ax::mojom::Action event_from_action,
const std::vector<AXEventIntent>& event_intents)
: event(event),
event_from(event_from),
event_from_action(event_from_action),
event_intents(event_intents) {}
AXEventGenerator::EventParams::EventParams(const EventParams& other) = default;
AXEventGenerator::EventParams::~EventParams() = default;
AXEventGenerator::EventParams& AXEventGenerator::EventParams::operator=(
const EventParams& other) = default;
bool AXEventGenerator::EventParams::operator==(const EventParams& rhs) const {
return rhs.event == event;
}
bool AXEventGenerator::EventParams::operator<(const EventParams& rhs) const {
return event < rhs.event;
}
AXEventGenerator::TargetedEvent::TargetedEvent(AXNodeID node_id,
const EventParams& event_params)
: node_id(node_id), event_params(event_params) {
DCHECK_NE(node_id, kInvalidAXNodeID);
}
AXEventGenerator::TargetedEvent::~TargetedEvent() = default;
AXEventGenerator::Iterator::Iterator(
std::map<AXNodeID, std::set<EventParams>>::const_iterator map_start_iter,
std::map<AXNodeID, std::set<EventParams>>::const_iterator map_end_iter)
: map_iter_(map_start_iter), map_end_iter_(map_end_iter) {
if (map_iter_ != map_end_iter_)
set_iter_ = map_iter_->second.begin();
}
AXEventGenerator::Iterator::Iterator(const AXEventGenerator::Iterator& other) =
default;
AXEventGenerator::Iterator::~Iterator() = default;
AXEventGenerator::Iterator& AXEventGenerator::Iterator::operator=(
const Iterator& other) = default;
AXEventGenerator::Iterator& AXEventGenerator::Iterator::operator++() {
if (map_iter_ == map_end_iter_)
return *this;
CHECK(set_iter_ != map_iter_->second.end());
set_iter_++;
while (map_iter_ != map_end_iter_ && set_iter_ == map_iter_->second.end()) {
map_iter_++;
if (map_iter_ != map_end_iter_)
set_iter_ = map_iter_->second.begin();
}
return *this;
}
AXEventGenerator::Iterator AXEventGenerator::Iterator::operator++(int) {
if (map_iter_ == map_end_iter_)
return *this;
Iterator iter = *this;
++(*this);
return iter;
}
AXEventGenerator::TargetedEvent AXEventGenerator::Iterator::operator*() const {
DCHECK(map_iter_ != map_end_iter_);
CHECK(set_iter_ != map_iter_->second.end());
return AXEventGenerator::TargetedEvent(map_iter_->first, *set_iter_);
}
bool operator==(const AXEventGenerator::Iterator& lhs,
const AXEventGenerator::Iterator& rhs) {
if (lhs.map_iter_ == lhs.map_end_iter_ && rhs.map_iter_ == rhs.map_end_iter_)
return true;
return lhs.map_iter_ == rhs.map_iter_ && lhs.set_iter_ == rhs.set_iter_;
}
void swap(AXEventGenerator::Iterator& lhs, AXEventGenerator::Iterator& rhs) {
if (lhs == rhs)
return;
std::map<AXNodeID, std::set<AXEventGenerator::EventParams>>::const_iterator
map_iter = lhs.map_iter_;
lhs.map_iter_ = rhs.map_iter_;
rhs.map_iter_ = map_iter;
std::map<AXNodeID, std::set<AXEventGenerator::EventParams>>::const_iterator
map_end_iter = lhs.map_end_iter_;
lhs.map_end_iter_ = rhs.map_end_iter_;
rhs.map_end_iter_ = map_end_iter;
std::set<AXEventGenerator::EventParams>::const_iterator set_iter =
lhs.set_iter_;
lhs.set_iter_ = rhs.set_iter_;
rhs.set_iter_ = set_iter;
}
AXEventGenerator::AXEventGenerator() = default;
AXEventGenerator::AXEventGenerator(AXTree* tree) : tree_(tree) {
if (tree)
tree_event_observation_.Observe(tree_.get());
}
AXEventGenerator::~AXEventGenerator() = default;
void AXEventGenerator::SetTree(AXTree* new_tree) {
if (tree_) {
DCHECK(tree_event_observation_.IsObservingSource(tree_.get()));
tree_event_observation_.Reset();
}
tree_ = new_tree;
if (tree_)
tree_event_observation_.Observe(tree_.get());
}
void AXEventGenerator::ReleaseTree() {
tree_event_observation_.Reset();
tree_ = nullptr;
}
bool AXEventGenerator::empty() const {
return tree_events_.empty();
}
size_t AXEventGenerator::size() const {
return tree_events_.size();
}
AXEventGenerator::Iterator AXEventGenerator::begin() const {
auto map_iter = tree_events_.begin();
if (map_iter != tree_events_.end()) {
auto set_iter = map_iter->second.begin();
while (map_iter != tree_events_.end() &&
set_iter == map_iter->second.end()) {
map_iter++;
if (map_iter != tree_events_.end())
set_iter = map_iter->second.begin();
}
}
return AXEventGenerator::Iterator(map_iter, tree_events_.end());
}
AXEventGenerator::Iterator AXEventGenerator::end() const {
return AXEventGenerator::Iterator(tree_events_.end(), tree_events_.end());
}
void AXEventGenerator::ClearEvents() {
tree_events_.clear();
}
void AXEventGenerator::AddEvent(AXNode* node, AXEventGenerator::Event event) {
DCHECK(node);
if (node->GetRole() == ax::mojom::Role::kInlineTextBox)
return;
if (!tree_->event_data())
return;
std::set<EventParams>& node_events = tree_events_[node->id()];
node_events.emplace(event, tree_->event_data()->event_from,
tree_->event_data()->event_from_action,
tree_->event_data()->event_intents);
}
void AXEventGenerator::RegisterEventOnNode(Event event_type, AXNodeID node_id) {
registered_event_to_node_ids_[event_type].insert(node_id);
}
void AXEventGenerator::UnregisterEventOnNode(Event event_type,
AXNodeID node_id) {
auto it = registered_event_to_node_ids_.find(event_type);
if (it == registered_event_to_node_ids_.end()) {
return;
}
registered_event_to_node_ids_[event_type].erase(node_id);
if (registered_event_to_node_ids_[event_type].empty()) {
registered_event_to_node_ids_.erase(event_type);
}
}
void AXEventGenerator::OnIgnoredWillChange(
AXTree* tree,
AXNode* node,
bool is_ignored_new_value,
bool is_changing_unignored_parents_children) {
if (!is_ignored_new_value && node->data().IsInvisible())
nodes_to_suppress_parent_changed_on_.insert(node->id());
}
void AXEventGenerator::OnNodeDataChanged(AXTree* tree,
const AXNodeData& old_node_data,
const AXNodeData& new_node_data) {
DCHECK_EQ(tree_, tree);
AXNode* node = tree_->GetFromId(new_node_data.id);
if (!node)
return;
if (new_node_data.HasStringListAttribute(
ax::mojom::StringListAttribute::kAriaNotificationAnnouncements)) {
AddEvent(node, Event::ARIA_NOTIFICATIONS_POSTED);
}
if (node->IsText()) {
return;
}
bool was_ignored = old_node_data.IsIgnored();
bool is_ignored = new_node_data.IsIgnored();
if (was_ignored != is_ignored) {
return;
}
if (new_node_data.child_ids == old_node_data.child_ids) {
return;
}
if (is_ignored) {
AXNode* unignored_parent = node->GetUnignoredParent();
DUMP_WILL_BE_CHECK(unignored_parent)
<< "The root cannot be ignored, so an unignored parent is always "
"found.";
AddEvent(unignored_parent, Event::CHILDREN_CHANGED);
} else {
AddEvent(node, Event::CHILDREN_CHANGED);
}
}
void AXEventGenerator::OnRoleChanged(AXTree* tree,
AXNode* node,
ax::mojom::Role old_role,
ax::mojom::Role new_role) {
DCHECK_EQ(tree_, tree);
AddEvent(node, ui::IsAlert(new_role) ? Event::ALERT : Event::ROLE_CHANGED);
}
void AXEventGenerator::OnIgnoredChanged(AXTree* tree,
AXNode* node,
bool is_ignored_new_value) {
DCHECK_EQ(tree_, tree);
AXNode* unignored_parent = node->GetUnignoredParent();
DUMP_WILL_BE_CHECK(unignored_parent)
<< "The root cannot be ignored, so an unignored parent is always "
"found.";
AddEvent(unignored_parent, Event::CHILDREN_CHANGED);
AddEvent(node, Event::IGNORED_CHANGED);
if (!is_ignored_new_value) {
AddEvent(node, Event::SUBTREE_CREATED);
if (IsAlert(node->GetRole())) {
AddEvent(node, Event::ALERT);
}
}
if (node->GetRole() == ax::mojom::Role::kMenu) {
if (is_ignored_new_value) {
AddEvent(node, Event::MENU_POPUP_END);
} else {
AddEvent(node, Event::MENU_POPUP_START);
}
}
const bool was_in_invisible_subtree =
!base::Contains(nodes_to_suppress_parent_changed_on_, node->id());
if (was_in_invisible_subtree) {
for (auto iter = node->UnignoredChildrenBegin(),
end = node->UnignoredChildrenEnd();
iter != end; ++iter) {
AddEvent(iter.get(), Event::PARENT_CHANGED);
}
}
}
void AXEventGenerator::OnStateChanged(AXTree* tree,
AXNode* node,
ax::mojom::State state,
bool new_value) {
DCHECK_EQ(tree_, tree);
DCHECK_NE(state, ax::mojom::State::kIgnored)
<< "The ignored state should be handled in "
"`AXEventGenerator::OnIgnoredChanged` and not in this method.";
if (node->IsIgnored())
return;
AddEvent(node, Event::STATE_CHANGED);
AddEvent(node, Event::WIN_IACCESSIBLE_STATE_CHANGED);
switch (state) {
case ax::mojom::State::kExpanded:
if (node->data().HasState(ax::mojom::State::kCollapsed) ||
node->data().HasState(ax::mojom::State::kExpanded)) {
AddEvent(node, new_value ? Event::EXPANDED : Event::COLLAPSED);
}
if (IsTableRow(node->GetRole()) ||
node->GetRole() == ax::mojom::Role::kTreeItem) {
AXNode* container = node;
while (container && !IsRowContainer(container->GetRole()))
container = container->parent();
if (container)
AddEvent(container, Event::ROW_COUNT_CHANGED);
}
break;
case ax::mojom::State::kMultiline:
AddEvent(node, Event::MULTILINE_STATE_CHANGED);
break;
case ax::mojom::State::kMultiselectable:
AddEvent(node, Event::MULTISELECTABLE_STATE_CHANGED);
break;
case ax::mojom::State::kRequired:
AddEvent(node, Event::REQUIRED_STATE_CHANGED);
break;
case ax::mojom::State::kAutofillAvailable:
AddEvent(node, Event::AUTOFILL_AVAILABILITY_CHANGED);
break;
case ax::mojom::State::kHorizontal:
case ax::mojom::State::kVertical:
AddEvent(node, Event::ORIENTATION_CHANGED);
break;
default:
break;
}
}
void AXEventGenerator::OnStringAttributeChanged(AXTree* tree,
AXNode* node,
ax::mojom::StringAttribute attr,
const std::string& old_value,
const std::string& new_value) {
DCHECK_EQ(tree_, tree);
switch (attr) {
case ax::mojom::StringAttribute::kAccessKey:
AddEvent(node, Event::ACCESS_KEY_CHANGED);
break;
case ax::mojom::StringAttribute::kAutoComplete:
AddEvent(node, Event::AUTO_COMPLETE_CHANGED);
break;
case ax::mojom::StringAttribute::kCheckedStateDescription:
AddEvent(node, Event::CHECKED_STATE_DESCRIPTION_CHANGED);
break;
case ax::mojom::StringAttribute::kClassName:
break;
case ax::mojom::StringAttribute::kDescription:
AddEvent(node, Event::DESCRIPTION_CHANGED);
break;
case ax::mojom::StringAttribute::kFontFamily:
if (node->HasState(ax::mojom::State::kRichlyEditable)) {
AddEvent(node, Event::TEXT_ATTRIBUTE_CHANGED);
}
break;
case ax::mojom::StringAttribute::kImageAnnotation:
AddEvent(node, Event::IMAGE_ANNOTATION_CHANGED);
break;
case ax::mojom::StringAttribute::kKeyShortcuts:
AddEvent(node, Event::KEY_SHORTCUTS_CHANGED);
break;
case ax::mojom::StringAttribute::kLanguage:
AddEvent(node, Event::LANGUAGE_CHANGED);
break;
case ax::mojom::StringAttribute::kLiveRelevant:
AddEvent(node, Event::LIVE_RELEVANT_CHANGED);
break;
case ax::mojom::StringAttribute::kLiveStatus:
AddEvent(node, Event::LIVE_STATUS_CHANGED);
if (!IsAlert(node->GetRole())) {
bool was_off = !old_value.empty()
? old_value == "off"
: !node->data().IsContainedInActiveLiveRegion();
bool is_off = !new_value.empty()
? new_value == "off"
: !node->data().IsContainedInActiveLiveRegion();
if (was_off && !is_off)
AddEvent(node, Event::LIVE_REGION_CREATED);
}
break;
case ax::mojom::StringAttribute::kName:
if (node != tree->root())
AddEvent(node, Event::NAME_CHANGED);
if (node->HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus)) {
FireLiveRegionEvents(node, false);
}
FireValueInTextFieldChangedEventIfNecessary(tree, node);
break;
case ax::mojom::StringAttribute::kPlaceholder:
AddEvent(node, Event::PLACEHOLDER_CHANGED);
break;
case ax::mojom::StringAttribute::kValue:
if (node->data().IsRangeValueSupported()) {
AddEvent(node, Event::RANGE_VALUE_CHANGED);
} else if (IsSelectElement(node->GetRole())) {
AddEvent(node, Event::SELECTED_VALUE_CHANGED);
} else if (node->data().IsTextField()) {
AddEvent(node, Event::VALUE_IN_TEXT_FIELD_CHANGED);
}
break;
default:
break;
}
}
void AXEventGenerator::OnIntAttributeChanged(AXTree* tree,
AXNode* node,
ax::mojom::IntAttribute attr,
int32_t old_value,
int32_t new_value) {
DCHECK_EQ(tree_, tree);
switch (attr) {
case ax::mojom::IntAttribute::kActivedescendantId:
if (!node->data().IsInvisible()) {
AddEvent(node, Event::ACTIVE_DESCENDANT_CHANGED);
active_descendant_changed_.push_back(node);
}
break;
case ax::mojom::IntAttribute::kCheckedState:
AddEvent(node, Event::CHECKED_STATE_CHANGED);
AddEvent(node, Event::WIN_IACCESSIBLE_STATE_CHANGED);
break;
case ax::mojom::IntAttribute::kAriaCurrentState:
AddEvent(node, Event::ARIA_CURRENT_CHANGED);
break;
case ax::mojom::IntAttribute::kHasPopup:
AddEvent(node, Event::HASPOPUP_CHANGED);
AddEvent(node, Event::WIN_IACCESSIBLE_STATE_CHANGED);
break;
case ax::mojom::IntAttribute::kHierarchicalLevel:
AddEvent(node, Event::HIERARCHICAL_LEVEL_CHANGED);
break;
case ax::mojom::IntAttribute::kInvalidState:
AddEvent(node, Event::INVALID_STATUS_CHANGED);
break;
case ax::mojom::IntAttribute::kPosInSet:
AddEvent(node, Event::POSITION_IN_SET_CHANGED);
break;
case ax::mojom::IntAttribute::kRestriction: {
bool was_enabled;
bool was_readonly;
GetRestrictionStates(static_cast<ax::mojom::Restriction>(old_value),
&was_enabled, &was_readonly);
bool is_enabled;
bool is_readonly;
GetRestrictionStates(static_cast<ax::mojom::Restriction>(new_value),
&is_enabled, &is_readonly);
if (was_enabled != is_enabled) {
AddEvent(node, Event::ENABLED_CHANGED);
AddEvent(node, Event::WIN_IACCESSIBLE_STATE_CHANGED);
}
if (was_readonly != is_readonly) {
AddEvent(node, Event::READONLY_CHANGED);
AddEvent(node, Event::WIN_IACCESSIBLE_STATE_CHANGED);
}
break;
}
case ax::mojom::IntAttribute::kScrollX:
AddEvent(node, Event::SCROLL_HORIZONTAL_POSITION_CHANGED);
break;
case ax::mojom::IntAttribute::kScrollY:
AddEvent(node, Event::SCROLL_VERTICAL_POSITION_CHANGED);
break;
case ax::mojom::IntAttribute::kSortDirection:
if (IsTableHeader(node->GetRole()))
AddEvent(node, Event::SORT_CHANGED);
break;
case ax::mojom::IntAttribute::kImageAnnotationStatus:
AddEvent(node, Event::IMAGE_ANNOTATION_CHANGED);
break;
case ax::mojom::IntAttribute::kSetSize:
AddEvent(node, Event::SET_SIZE_CHANGED);
break;
case ax::mojom::IntAttribute::kBackgroundColor:
case ax::mojom::IntAttribute::kColor:
case ax::mojom::IntAttribute::kTextDirection:
case ax::mojom::IntAttribute::kTextPosition:
case ax::mojom::IntAttribute::kTextStyle:
case ax::mojom::IntAttribute::kTextOverlineStyle:
case ax::mojom::IntAttribute::kTextStrikethroughStyle:
case ax::mojom::IntAttribute::kTextUnderlineStyle:
if (node->HasState(ax::mojom::State::kRichlyEditable)) {
AddEvent(node, Event::TEXT_ATTRIBUTE_CHANGED);
}
break;
case ax::mojom::IntAttribute::kTextAlign:
if (node->HasState(ax::mojom::State::kRichlyEditable)) {
AddEvent(node, Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED);
AddEvent(node, Event::OBJECT_ATTRIBUTE_CHANGED);
}
break;
case ax::mojom::IntAttribute::kDefaultActionVerb:
AddEvent(node, Event::DEFAULT_ACTION_VERB_CHANGED);
break;
default:
break;
}
}
void AXEventGenerator::OnFloatAttributeChanged(AXTree* tree,
AXNode* node,
ax::mojom::FloatAttribute attr,
float old_value,
float new_value) {
DCHECK_EQ(tree_, tree);
switch (attr) {
case ax::mojom::FloatAttribute::kMaxValueForRange:
AddEvent(node, Event::RANGE_VALUE_MAX_CHANGED);
break;
case ax::mojom::FloatAttribute::kMinValueForRange:
AddEvent(node, Event::RANGE_VALUE_MIN_CHANGED);
break;
case ax::mojom::FloatAttribute::kStepValueForRange:
AddEvent(node, Event::RANGE_VALUE_STEP_CHANGED);
break;
case ax::mojom::FloatAttribute::kValueForRange:
AddEvent(node, Event::RANGE_VALUE_CHANGED);
break;
case ax::mojom::FloatAttribute::kFontSize:
case ax::mojom::FloatAttribute::kFontWeight:
if (node->HasState(ax::mojom::State::kRichlyEditable)) {
AddEvent(node, Event::TEXT_ATTRIBUTE_CHANGED);
}
break;
case ax::mojom::FloatAttribute::kTextIndent:
if (node->HasState(ax::mojom::State::kRichlyEditable)) {
AddEvent(node, Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED);
AddEvent(node, Event::OBJECT_ATTRIBUTE_CHANGED);
}
break;
default:
break;
}
}
void AXEventGenerator::OnBoolAttributeChanged(AXTree* tree,
AXNode* node,
ax::mojom::BoolAttribute attr,
bool new_value) {
DCHECK_EQ(tree_, tree);
switch (attr) {
case ax::mojom::BoolAttribute::kBusy:
AddEvent(node, Event::BUSY_CHANGED);
AddEvent(node, Event::WIN_IACCESSIBLE_STATE_CHANGED);
if (!new_value)
AddEvent(node, Event::LAYOUT_INVALIDATED);
break;
case ax::mojom::BoolAttribute::kLiveAtomic:
AddEvent(node, Event::ATOMIC_CHANGED);
break;
case ax::mojom::BoolAttribute::kSelected: {
AddEvent(node, Event::SELECTED_CHANGED);
AddEvent(node, Event::WIN_IACCESSIBLE_STATE_CHANGED);
AXNode* container = node;
while (container &&
!IsContainerWithSelectableChildren(container->GetRole()))
container = container->parent();
if (container)
AddEvent(container, Event::SELECTED_CHILDREN_CHANGED);
break;
}
default:
break;
}
}
void AXEventGenerator::OnIntListAttributeChanged(
AXTree* tree,
AXNode* node,
ax::mojom::IntListAttribute attr,
const std::vector<int32_t>& old_value,
const std::vector<int32_t>& new_value) {
DCHECK_EQ(tree_, tree);
switch (attr) {
case ax::mojom::IntListAttribute::kControlsIds:
AddEvent(node, Event::CONTROLS_CHANGED);
break;
case ax::mojom::IntListAttribute::kDetailsIds:
AddEvent(node, Event::DETAILS_CHANGED);
break;
case ax::mojom::IntListAttribute::kDescribedbyIds:
AddEvent(node, Event::DESCRIBED_BY_CHANGED);
break;
case ax::mojom::IntListAttribute::kFlowtoIds: {
AddEvent(node, Event::FLOW_TO_CHANGED);
for (AXNodeID id : ComputeIntListDifference(old_value, new_value)) {
if (AXNode* target_node = tree->GetFromId(id))
AddEvent(target_node, Event::FLOW_FROM_CHANGED);
}
break;
}
case ax::mojom::IntListAttribute::kLabelledbyIds:
AddEvent(node, Event::LABELED_BY_CHANGED);
break;
case ax::mojom::IntListAttribute::kMarkerEnds:
case ax::mojom::IntListAttribute::kMarkerStarts:
case ax::mojom::IntListAttribute::kMarkerTypes:
if (AXNode* text_field = node->GetTextFieldAncestor()) {
AddEvent(text_field, Event::TEXT_ATTRIBUTE_CHANGED);
} else if (node->HasState(ax::mojom::State::kRichlyEditable)) {
AddEvent(node, Event::TEXT_ATTRIBUTE_CHANGED);
}
break;
case ax::mojom::IntListAttribute::kCaretBounds:
AddEvent(node, Event::CARET_BOUNDS_CHANGED);
break;
default:
break;
}
}
void AXEventGenerator::OnTreeDataChanged(AXTree* tree,
const AXTreeData& old_tree_data,
const AXTreeData& new_tree_data) {
DCHECK_EQ(tree_, tree);
DCHECK(tree->root());
if (new_tree_data.title != old_tree_data.title)
AddEvent(tree->root(), Event::DOCUMENT_TITLE_CHANGED);
if (new_tree_data.sel_is_backward != old_tree_data.sel_is_backward ||
new_tree_data.sel_anchor_object_id !=
old_tree_data.sel_anchor_object_id ||
new_tree_data.sel_anchor_offset != old_tree_data.sel_anchor_offset ||
new_tree_data.sel_anchor_affinity != old_tree_data.sel_anchor_affinity ||
new_tree_data.sel_focus_object_id != old_tree_data.sel_focus_object_id ||
new_tree_data.sel_focus_offset != old_tree_data.sel_focus_offset ||
new_tree_data.sel_focus_affinity != old_tree_data.sel_focus_affinity) {
AddEvent(tree->root(), Event::DOCUMENT_SELECTION_CHANGED);
const AXNode* selection_focus =
tree_->GetFromId(new_tree_data.sel_focus_object_id);
if (selection_focus) {
if (AXNode* text_field = selection_focus->GetTextFieldAncestor())
AddEvent(text_field, Event::TEXT_SELECTION_CHANGED);
}
}
}
void AXEventGenerator::OnSubtreeWillBeDeleted(AXTree* tree, AXNode* node) {
DCHECK_EQ(tree_, tree);
FireValueInTextFieldChangedEventIfNecessary(tree, node);
FireLiveRegionEvents(node, true);
}
void AXEventGenerator::OnNodeWillBeReparented(AXTree* tree, AXNode* node) {
DCHECK_EQ(tree_, tree);
}
void AXEventGenerator::OnSubtreeWillBeReparented(AXTree* tree, AXNode* node) {
DCHECK_EQ(tree_, tree);
}
void AXEventGenerator::OnNodeDeleted(AXTree* tree, AXNodeID node_id) {
DCHECK_EQ(tree_, tree);
tree_events_.erase(node_id);
}
void AXEventGenerator::OnNodeReparented(AXTree* tree, AXNode* node) {
DCHECK_EQ(tree_, tree);
AddEvent(node, Event::PARENT_CHANGED);
}
void AXEventGenerator::OnNodeCreated(AXTree* tree, AXNode* node) {
DCHECK_EQ(tree_, tree);
FireValueInTextFieldChangedEventIfNecessary(tree, node);
}
void AXEventGenerator::OnAtomicUpdateFinished(
AXTree* tree,
bool root_changed,
const std::vector<Change>& changes) {
DCHECK_EQ(tree_, tree);
DCHECK(tree->root());
for (const auto& change : changes) {
DCHECK(change.node);
if (change.type == SUBTREE_CREATED) {
AddEvent(change.node, Event::SUBTREE_CREATED);
} else if (change.type != NODE_CREATED) {
FireRelationSourceEvents(tree, change.node);
continue;
}
if (change.node->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected) &&
(change.type == SUBTREE_CREATED || change.type == NODE_CREATED)) {
OnBoolAttributeChanged(tree, change.node,
ax::mojom::BoolAttribute::kSelected,
true);
}
if (IsAlert(change.node->GetRole()))
AddEvent(change.node, Event::ALERT);
else if (change.node->data().IsActiveLiveRegionRoot())
AddEvent(change.node, Event::LIVE_REGION_CREATED);
else if (change.node->data().IsContainedInActiveLiveRegion())
FireLiveRegionEvents(change.node, false);
}
FireActiveDescendantEvents();
nodes_to_suppress_parent_changed_on_.clear();
PostprocessEvents();
}
void AXEventGenerator::AddEventsForTesting(
const AXNode& node,
const std::set<EventParams>& events) {
tree_events_[node.id()] = events;
}
bool AXEventGenerator::IsRemovalRelevantInLiveRegion(AXNode* node) {
const std::string& aria_relevant = node->GetStringAttribute(
ax::mojom::StringAttribute::kContainerLiveRelevant);
if (aria_relevant.empty())
return false;
std::vector<std::string> tokens = base::SplitString(
aria_relevant, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
return std::any_of(tokens.begin(), tokens.end(), [](std::string token) {
return token == "all" || token == "removals";
});
}
void AXEventGenerator::FireLiveRegionEvents(AXNode* node, bool is_removal) {
AXNode* live_root = node;
const std::string& container_live = node->GetStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus);
if (container_live.empty() || container_live == "off")
return;
if (is_removal && !IsRemovalRelevantInLiveRegion(node))
return;
while (live_root &&
live_root->GetStringAttribute(
ax::mojom::StringAttribute::kLiveStatus) != container_live)
live_root = live_root->parent();
if (live_root &&
!live_root->GetBoolAttribute(ax::mojom::BoolAttribute::kBusy)) {
if (!node->GetStringAttribute(ax::mojom::StringAttribute::kName).empty())
AddEvent(node, Event::LIVE_REGION_NODE_CHANGED);
AddEvent(live_root, Event::LIVE_REGION_CHANGED);
}
}
void AXEventGenerator::FireActiveDescendantEvents() {
for (AXNode* node : active_descendant_changed_) {
AXNode* descendant = tree_->GetFromId(
node->GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId));
if (!descendant)
continue;
switch (descendant->GetRole()) {
case ax::mojom::Role::kMenuItem:
case ax::mojom::Role::kMenuItemCheckBox:
case ax::mojom::Role::kMenuItemRadio:
case ax::mojom::Role::kMenuListOption:
AddEvent(descendant, Event::MENU_ITEM_SELECTED);
break;
default:
break;
}
}
active_descendant_changed_.clear();
}
bool CanContributeToValueOfTextfield(AXNode* target_node) {
if (target_node->GetRole() == ax::mojom::Role::kInlineTextBox) {
return false;
}
if (IsText(target_node->GetRole())) {
return true;
}
if (target_node->GetChildCount() == 0) {
return true;
}
return false;
}
void AXEventGenerator::FireValueInTextFieldChangedEventIfNecessary(
AXTree* tree,
AXNode* target_node) {
if (!CanContributeToValueOfTextfield(target_node))
return;
AXNode* text_field_ancestor = target_node->GetTextFieldAncestor();
if (!text_field_ancestor || text_field_ancestor == target_node)
return;
AddEvent(text_field_ancestor, Event::EDITABLE_TEXT_CHANGED);
AddEvent(text_field_ancestor, Event::VALUE_IN_TEXT_FIELD_CHANGED);
}
void AXEventGenerator::FireRelationSourceEvents(AXTree* tree,
AXNode* target_node) {
auto it = registered_event_to_node_ids_.find(Event::RELATED_NODE_CHANGED);
if (it == registered_event_to_node_ids_.end()) {
return;
}
AXNode* registered_node = target_node;
while (registered_node) {
if (it->second.contains(registered_node->id())) {
break;
}
registered_node = registered_node->parent();
}
if (!registered_node) {
return;
}
AXNodeID target_id = target_node->id();
std::set<AXNode*> source_nodes;
auto callback = [&](const auto& entry) {
const auto& target_to_sources = entry.second;
auto sources_it = target_to_sources.find(target_id);
if (sources_it == target_to_sources.end())
return;
std::ranges::for_each(sources_it->second, [&](AXNodeID source_id) {
AXNode* source_node = tree->GetFromId(source_id);
if (!source_node || source_nodes.count(source_node) > 0)
return;
source_nodes.insert(source_node);
this->AddEvent(source_node, Event::RELATED_NODE_CHANGED);
});
};
std::ranges::for_each(tree->int_reverse_relations(), callback);
std::ranges::for_each(tree->intlist_reverse_relations(), [&](auto& entry) {
if (entry.first != ax::mojom::IntListAttribute::kRadioGroupIds)
callback(entry);
});
}
void AXEventGenerator::TrimEventsDueToAncestorIgnoredChanged(
AXNode* node,
std::map<AXNode*, IgnoredChangedStatesBitset>&
ancestor_ignored_changed_map) {
DCHECK(node);
if (node->parent() &&
!base::Contains(ancestor_ignored_changed_map, node->parent())) {
TrimEventsDueToAncestorIgnoredChanged(node->parent(),
ancestor_ignored_changed_map);
}
const auto& parent_map_iter =
ancestor_ignored_changed_map.find(node->parent());
const auto& curr_events_iter = tree_events_.find(node->id());
IgnoredChangedStatesBitset& ancestor_ignored_changed_states =
ancestor_ignored_changed_map[node];
if (parent_map_iter != ancestor_ignored_changed_map.end()) {
if (HasIgnoredChangedState(parent_map_iter->second,
IgnoredChangedState::kHide)) {
AddIgnoredChangedState(IgnoredChangedState::kHide,
&ancestor_ignored_changed_states);
}
if (HasIgnoredChangedState(parent_map_iter->second,
IgnoredChangedState::kShow)) {
AddIgnoredChangedState(IgnoredChangedState::kShow,
&ancestor_ignored_changed_states);
}
if (curr_events_iter != tree_events_.end() &&
HasEvent(curr_events_iter->second, Event::IGNORED_CHANGED)) {
if ((HasIgnoredChangedState(parent_map_iter->second,
IgnoredChangedState::kHide) &&
node->IsIgnored()) ||
(HasIgnoredChangedState(parent_map_iter->second,
IgnoredChangedState::kShow) &&
!node->IsIgnored())) {
RemoveEvent(&(curr_events_iter->second), Event::IGNORED_CHANGED);
RemoveEventsDueToIgnoredChanged(&(curr_events_iter->second));
}
if (node->IsIgnored()) {
AddIgnoredChangedState(IgnoredChangedState::kHide,
&ancestor_ignored_changed_states);
} else {
AddIgnoredChangedState(IgnoredChangedState::kShow,
&ancestor_ignored_changed_states);
}
}
return;
}
if (curr_events_iter != tree_events_.end() &&
HasEvent(curr_events_iter->second, Event::IGNORED_CHANGED)) {
if (node->IsIgnored()) {
AddIgnoredChangedState(IgnoredChangedState::kHide,
&ancestor_ignored_changed_states);
} else {
AddIgnoredChangedState(IgnoredChangedState::kShow,
&ancestor_ignored_changed_states);
}
return;
}
}
void AXEventGenerator::PostprocessEvents() {
std::map<AXNode*, IgnoredChangedStatesBitset> ancestor_ignored_changed_map;
std::set<AXNode*> removed_subtree_created_nodes;
std::set<AXNode*> removed_parent_changed_nodes;
for (auto& iter : tree_events_) {
AXNodeID node_id = iter.first;
AXNode* node = tree_->GetFromId(node_id);
DCHECK(node);
if (!node)
continue;
std::set<EventParams>& node_events = iter.second;
if (HasEvent(node_events, Event::ALERT) ||
HasEvent(node_events, Event::LIVE_REGION_CREATED)) {
RemoveEvent(&node_events, Event::LIVE_REGION_CHANGED);
}
if (HasEvent(node_events, Event::IGNORED_CHANGED)) {
TrimEventsDueToAncestorIgnoredChanged(node, ancestor_ignored_changed_map);
RemoveEventsDueToIgnoredChanged(&node_events);
}
AXNode* parent = node->GetUnignoredParent();
if (parent && HasEvent(node_events, Event::TEXT_ATTRIBUTE_CHANGED) &&
tree_events_.find(parent->id()) != tree_events_.end() &&
HasEvent(tree_events_[parent->id()], Event::TEXT_ATTRIBUTE_CHANGED)) {
RemoveEvent(&node_events, Event::TEXT_ATTRIBUTE_CHANGED);
}
if (HasEvent(node_events, Event::PARENT_CHANGED)) {
while (parent && (tree_events_.find(parent->id()) != tree_events_.end() ||
base::Contains(removed_parent_changed_nodes, parent))) {
if ((base::Contains(removed_parent_changed_nodes, parent) ||
HasEvent(tree_events_[parent->id()], Event::PARENT_CHANGED)) &&
!HasEvent(tree_events_[parent->id()], Event::SUBTREE_CREATED)) {
RemoveEvent(&node_events, Event::PARENT_CHANGED);
removed_parent_changed_nodes.insert(node);
break;
}
parent = parent->GetUnignoredParent();
}
}
if (node->IsIgnored())
RemoveEvent(&node_events, Event::PARENT_CHANGED);
parent = node->GetUnignoredParent();
if (HasEvent(node_events, Event::SUBTREE_CREATED)) {
while (parent &&
(tree_events_.find(parent->id()) != tree_events_.end() ||
base::Contains(removed_subtree_created_nodes, parent))) {
if (base::Contains(removed_subtree_created_nodes, parent) ||
HasEvent(tree_events_[parent->id()], Event::SUBTREE_CREATED)) {
RemoveEvent(&node_events, Event::SUBTREE_CREATED);
removed_subtree_created_nodes.insert(node);
break;
}
parent = parent->GetUnignoredParent();
}
}
}
auto iter = tree_events_.begin();
while (iter != tree_events_.end()) {
std::set<EventParams>& node_events = iter->second;
if (node_events.empty())
iter = tree_events_.erase(iter);
else
++iter;
}
}
void AXEventGenerator::GetRestrictionStates(ax::mojom::Restriction restriction,
bool* is_enabled,
bool* is_readonly) {
switch (restriction) {
case ax::mojom::Restriction::kDisabled:
*is_enabled = false;
*is_readonly = true;
break;
case ax::mojom::Restriction::kReadOnly:
*is_enabled = true;
*is_readonly = true;
break;
case ax::mojom::Restriction::kNone:
*is_enabled = true;
*is_readonly = false;
break;
}
}
std::vector<int32_t> AXEventGenerator::ComputeIntListDifference(
const std::vector<int32_t>& lhs,
const std::vector<int32_t>& rhs) {
std::set<int32_t> sorted_lhs(lhs.cbegin(), lhs.cend());
std::set<int32_t> sorted_rhs(rhs.cbegin(), rhs.cend());
std::vector<int32_t> result;
std::set_symmetric_difference(sorted_lhs.cbegin(), sorted_lhs.cend(),
sorted_rhs.cbegin(), sorted_rhs.cend(),
std::back_inserter(result));
return result;
}
std::ostream& operator<<(std::ostream& os, AXEventGenerator::Event event) {
return os << ToString(event);
}
const char* ToString(AXEventGenerator::Event event) {
switch (event) {
case AXEventGenerator::Event::NONE:
return "none";
case AXEventGenerator::Event::ACCESS_KEY_CHANGED:
return "accessKeyChanged";
case AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED:
return "activeDescendantChanged";
case AXEventGenerator::Event::ALERT:
return "alert";
case AXEventGenerator::Event::ARIA_CURRENT_CHANGED:
return "ariaCurrentChanged";
case AXEventGenerator::Event::ARIA_NOTIFICATIONS_POSTED:
return "ariaNotificationsPosted";
case AXEventGenerator::Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED:
return "atkTextObjectAttributeChanged";
case AXEventGenerator::Event::ATOMIC_CHANGED:
return "atomicChanged";
case AXEventGenerator::Event::AUTO_COMPLETE_CHANGED:
return "autoCompleteChanged";
case AXEventGenerator::Event::AUTOFILL_AVAILABILITY_CHANGED:
return "autofillAvailabilityChanged";
case AXEventGenerator::Event::BUSY_CHANGED:
return "busyChanged";
case AXEventGenerator::Event::CARET_BOUNDS_CHANGED:
return "caretBoundsChanged";
case AXEventGenerator::Event::CHECKED_STATE_CHANGED:
return "checkedStateChanged";
case AXEventGenerator::Event::CHECKED_STATE_DESCRIPTION_CHANGED:
return "checkedStateDescriptionChanged";
case AXEventGenerator::Event::CHILDREN_CHANGED:
return "childrenChanged";
case AXEventGenerator::Event::COLLAPSED:
return "collapsed";
case AXEventGenerator::Event::CONTROLS_CHANGED:
return "controlsChanged";
case AXEventGenerator::Event::DEFAULT_ACTION_VERB_CHANGED:
return "defaultActionVerbChanged";
case AXEventGenerator::Event::DETAILS_CHANGED:
return "detailsChanged";
case AXEventGenerator::Event::DESCRIBED_BY_CHANGED:
return "describedByChanged";
case AXEventGenerator::Event::DESCRIPTION_CHANGED:
return "descriptionChanged";
case AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED:
return "documentSelectionChanged";
case AXEventGenerator::Event::DOCUMENT_TITLE_CHANGED:
return "documentTitleChanged";
case AXEventGenerator::Event::EDITABLE_TEXT_CHANGED:
return "editableTextChanged";
case AXEventGenerator::Event::ENABLED_CHANGED:
return "enabledChanged";
case AXEventGenerator::Event::EXPANDED:
return "expanded";
case AXEventGenerator::Event::FOCUS_CHANGED:
return "focusChanged";
case AXEventGenerator::Event::FLOW_FROM_CHANGED:
return "flowFromChanged";
case AXEventGenerator::Event::FLOW_TO_CHANGED:
return "flowToChanged";
case AXEventGenerator::Event::HASPOPUP_CHANGED:
return "haspopupChanged";
case AXEventGenerator::Event::HIERARCHICAL_LEVEL_CHANGED:
return "hierarchicalLevelChanged";
case AXEventGenerator::Event::IGNORED_CHANGED:
return "ignoredChanged";
case AXEventGenerator::Event::IMAGE_ANNOTATION_CHANGED:
return "imageAnnotationChanged";
case AXEventGenerator::Event::INVALID_STATUS_CHANGED:
return "invalidStatusChanged";
case AXEventGenerator::Event::KEY_SHORTCUTS_CHANGED:
return "keyShortcutsChanged";
case AXEventGenerator::Event::LABELED_BY_CHANGED:
return "labeledByChanged";
case AXEventGenerator::Event::LANGUAGE_CHANGED:
return "languageChanged";
case AXEventGenerator::Event::LAYOUT_INVALIDATED:
return "layoutInvalidated";
case AXEventGenerator::Event::LIVE_REGION_CHANGED:
return "liveRegionChanged";
case AXEventGenerator::Event::LIVE_REGION_CREATED:
return "liveRegionCreated";
case AXEventGenerator::Event::LIVE_REGION_NODE_CHANGED:
return "liveRegionNodeChanged";
case AXEventGenerator::Event::LIVE_RELEVANT_CHANGED:
return "liveRelevantChanged";
case AXEventGenerator::Event::LIVE_STATUS_CHANGED:
return "liveStatusChanged";
case AXEventGenerator::Event::MENU_ITEM_SELECTED:
return "menuItemSelected";
case AXEventGenerator::Event::MENU_POPUP_END:
return "menuPopupEnd";
case AXEventGenerator::Event::MENU_POPUP_START:
return "menuPopupStart";
case AXEventGenerator::Event::MULTILINE_STATE_CHANGED:
return "multilineStateChanged";
case AXEventGenerator::Event::MULTISELECTABLE_STATE_CHANGED:
return "multiselectableStateChanged";
case AXEventGenerator::Event::NAME_CHANGED:
return "nameChanged";
case AXEventGenerator::Event::OBJECT_ATTRIBUTE_CHANGED:
return "objectAttributeChanged";
case AXEventGenerator::Event::ORIENTATION_CHANGED:
return "orientationChanged";
case AXEventGenerator::Event::PARENT_CHANGED:
return "parentChanged";
case AXEventGenerator::Event::PLACEHOLDER_CHANGED:
return "placeholderChanged";
case AXEventGenerator::Event::POSITION_IN_SET_CHANGED:
return "positionInSetChanged";
case AXEventGenerator::Event::RANGE_VALUE_CHANGED:
return "rangeValueChanged";
case AXEventGenerator::Event::RANGE_VALUE_MAX_CHANGED:
return "rangeValueMaxChanged";
case AXEventGenerator::Event::RANGE_VALUE_MIN_CHANGED:
return "rangeValueMinChanged";
case AXEventGenerator::Event::RANGE_VALUE_STEP_CHANGED:
return "rangeValueStepChanged";
case AXEventGenerator::Event::READONLY_CHANGED:
return "readonlyChanged";
case AXEventGenerator::Event::RELATED_NODE_CHANGED:
return "relatedNodeChanged";
case AXEventGenerator::Event::REQUIRED_STATE_CHANGED:
return "requiredStateChanged";
case AXEventGenerator::Event::ROLE_CHANGED:
return "roleChanged";
case AXEventGenerator::Event::ROW_COUNT_CHANGED:
return "rowCountChanged";
case AXEventGenerator::Event::SCROLL_HORIZONTAL_POSITION_CHANGED:
return "scrollHorizontalPositionChanged";
case AXEventGenerator::Event::SCROLL_VERTICAL_POSITION_CHANGED:
return "scrollVerticalPositionChanged";
case AXEventGenerator::Event::SELECTED_CHANGED:
return "selectedChanged";
case AXEventGenerator::Event::SELECTED_CHILDREN_CHANGED:
return "selectedChildrenChanged";
case AXEventGenerator::Event::SELECTED_VALUE_CHANGED:
return "selectedValueChanged";
case AXEventGenerator::Event::TEXT_SELECTION_CHANGED:
return "textSelectionChanged";
case AXEventGenerator::Event::SET_SIZE_CHANGED:
return "setSizeChanged";
case AXEventGenerator::Event::SORT_CHANGED:
return "sortChanged";
case AXEventGenerator::Event::STATE_CHANGED:
return "stateChanged";
case AXEventGenerator::Event::SUBTREE_CREATED:
return "subtreeCreated";
case AXEventGenerator::Event::TEXT_ATTRIBUTE_CHANGED:
return "textAttributeChanged";
case AXEventGenerator::Event::VALUE_IN_TEXT_FIELD_CHANGED:
return "valueInTextFieldChanged";
case AXEventGenerator::Event::WIN_IACCESSIBLE_STATE_CHANGED:
return "winIaccessibleStateChanged";
}
}
bool MaybeParseGeneratedEvent(const char* attribute,
AXEventGenerator::Event* result) {
static base::NoDestructor<std::map<std::string, AXEventGenerator::Event>>
attr_map;
if (attr_map->empty()) {
(*attr_map)[""] = AXEventGenerator::Event::NONE;
for (int i = static_cast<int>(AXEventGenerator::Event::NONE);
i <= static_cast<int>(AXEventGenerator::Event::MAX_VALUE); i++) {
auto attr = static_cast<AXEventGenerator::Event>(i);
std::string str = ToString(attr);
if (!base::Contains(*attr_map, str))
(*attr_map)[str] = attr;
}
}
auto iter = attr_map->find(attribute);
if (iter != attr_map->end()) {
*result = iter->second;
return true;
}
return false;
}
AXEventGenerator::Event ParseGeneratedEvent(const char* attribute) {
AXEventGenerator::Event event;
if (MaybeParseGeneratedEvent(attribute, &event))
return event;
NOTREACHED() << "Could not parse: " << attribute;
}
}