| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/accessibility/ax_event_generator.h" |
| |
| #include "base/stl_util.h" |
| #include "ui/accessibility/ax_node.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| |
| namespace ui { |
| |
| AXEventGenerator::TargetedEvent::TargetedEvent(ui::AXNode* node, Event event) |
| : node(node), event(event) {} |
| |
| AXEventGenerator::Iterator::Iterator( |
| const std::map<AXNode*, std::set<Event>>& map, |
| const std::map<AXNode*, std::set<Event>>::const_iterator& head) |
| : map_(map), map_iter_(head) { |
| if (map_iter_ != map.end()) |
| set_iter_ = map_iter_->second.begin(); |
| } |
| |
| AXEventGenerator::Iterator::Iterator(const AXEventGenerator::Iterator& other) = |
| default; |
| |
| AXEventGenerator::Iterator::~Iterator() = default; |
| |
| bool AXEventGenerator::Iterator::operator!=( |
| const AXEventGenerator::Iterator& rhs) const { |
| return map_iter_ != rhs.map_iter_ || |
| (map_iter_ != map_.end() && set_iter_ != rhs.set_iter_); |
| } |
| |
| AXEventGenerator::Iterator& AXEventGenerator::Iterator::operator++() { |
| if (map_iter_ == map_.end()) |
| return *this; |
| |
| set_iter_++; |
| while (map_iter_ != map_.end() && set_iter_ == map_iter_->second.end()) { |
| map_iter_++; |
| if (map_iter_ != map_.end()) |
| set_iter_ = map_iter_->second.begin(); |
| } |
| |
| return *this; |
| } |
| |
| AXEventGenerator::TargetedEvent AXEventGenerator::Iterator::operator*() const { |
| DCHECK(map_iter_ != map_.end() && set_iter_ != map_iter_->second.end()); |
| return AXEventGenerator::TargetedEvent(map_iter_->first, *set_iter_); |
| } |
| |
| AXEventGenerator::AXEventGenerator() = default; |
| |
| AXEventGenerator::AXEventGenerator(AXTree* tree) : tree_(tree) { |
| if (tree_) |
| tree_->SetDelegate(this); |
| } |
| |
| AXEventGenerator::~AXEventGenerator() { |
| if (tree_) |
| tree_->SetDelegate(nullptr); |
| } |
| |
| void AXEventGenerator::SetTree(AXTree* new_tree) { |
| if (tree_) |
| tree_->SetDelegate(nullptr); |
| tree_ = new_tree; |
| if (tree_) |
| tree_->SetDelegate(this); |
| } |
| |
| void AXEventGenerator::ReleaseTree() { |
| tree_ = nullptr; |
| } |
| |
| void AXEventGenerator::ClearEvents() { |
| tree_events_.clear(); |
| } |
| |
| void AXEventGenerator::AddEvent(ui::AXNode* node, |
| AXEventGenerator::Event event) { |
| if (node->data().role == AX_ROLE_INLINE_TEXT_BOX) |
| return; |
| |
| // A newly created live region or alert should not *also* fire a |
| // live region changed event. |
| if (event == Event::LIVE_REGION_CHANGED && |
| (base::ContainsKey(tree_events_[node], Event::ALERT) || |
| base::ContainsKey(tree_events_[node], Event::LIVE_REGION_CREATED))) { |
| return; |
| } |
| |
| tree_events_[node].insert(event); |
| } |
| |
| void AXEventGenerator::OnNodeDataWillChange(AXTree* tree, |
| const AXNodeData& old_node_data, |
| const AXNodeData& new_node_data) { |
| DCHECK_EQ(tree_, tree); |
| // Fire CHILDREN_CHANGED events when the list of children updates. |
| // Internally we store inline text box nodes as children of a static text |
| // node, which enables us to determine character bounds and line layout. |
| // We don't expose those to platform APIs, though, so suppress |
| // CHILDREN_CHANGED events on static text nodes. |
| if (new_node_data.child_ids != old_node_data.child_ids && |
| new_node_data.role != ui::AX_ROLE_STATIC_TEXT) { |
| AXNode* node = tree_->GetFromId(new_node_data.id); |
| tree_events_[node].insert(Event::CHILDREN_CHANGED); |
| } |
| } |
| |
| void AXEventGenerator::OnRoleChanged(AXTree* tree, |
| AXNode* node, |
| AXRole old_role, |
| AXRole new_role) { |
| DCHECK_EQ(tree_, tree); |
| AddEvent(node, Event::ROLE_CHANGED); |
| } |
| |
| void AXEventGenerator::OnStateChanged(AXTree* tree, |
| AXNode* node, |
| AXState state, |
| bool new_value) { |
| DCHECK_EQ(tree_, tree); |
| |
| AddEvent(node, Event::STATE_CHANGED); |
| if (state == ui::AX_STATE_EXPANDED) { |
| AddEvent(node, new_value ? Event::EXPANDED : Event::COLLAPSED); |
| if (node->data().role == ui::AX_ROLE_ROW || |
| node->data().role == ui::AX_ROLE_TREE_ITEM) { |
| ui::AXNode* container = node; |
| while (container && !ui::IsRowContainer(container->data().role)) |
| container = container->parent(); |
| if (container) |
| AddEvent(container, Event::ROW_COUNT_CHANGED); |
| } |
| } |
| if (state == ui::AX_STATE_SELECTED) { |
| AddEvent(node, Event::SELECTED_CHANGED); |
| ui::AXNode* container = node; |
| while (container && |
| !ui::IsContainerWithSelectableChildrenRole(container->data().role)) |
| container = container->parent(); |
| if (container) |
| AddEvent(container, Event::SELECTED_CHILDREN_CHANGED); |
| } |
| } |
| |
| void AXEventGenerator::OnStringAttributeChanged(AXTree* tree, |
| AXNode* node, |
| AXStringAttribute attr, |
| const std::string& old_value, |
| const std::string& new_value) { |
| DCHECK_EQ(tree_, tree); |
| |
| switch (attr) { |
| case ui::AX_ATTR_NAME: |
| AddEvent(node, Event::NAME_CHANGED); |
| if (node->data().HasStringAttribute(ui::AX_ATTR_CONTAINER_LIVE_STATUS)) { |
| FireLiveRegionEvents(node); |
| } |
| break; |
| case ui::AX_ATTR_DESCRIPTION: |
| AddEvent(node, Event::DESCRIPTION_CHANGED); |
| break; |
| case ui::AX_ATTR_VALUE: |
| AddEvent(node, Event::VALUE_CHANGED); |
| break; |
| case ui::AX_ATTR_ARIA_INVALID_VALUE: |
| AddEvent(node, Event::INVALID_STATUS_CHANGED); |
| break; |
| case ui::AX_ATTR_LIVE_STATUS: |
| if (node->data().role != ui::AX_ROLE_ALERT) |
| AddEvent(node, Event::LIVE_REGION_CREATED); |
| break; |
| default: |
| AddEvent(node, Event::OTHER_ATTRIBUTE_CHANGED); |
| break; |
| } |
| } |
| |
| void AXEventGenerator::OnIntAttributeChanged(AXTree* tree, |
| AXNode* node, |
| AXIntAttribute attr, |
| int32_t old_value, |
| int32_t new_value) { |
| DCHECK_EQ(tree_, tree); |
| |
| switch (attr) { |
| case ui::AX_ATTR_ACTIVEDESCENDANT_ID: |
| AddEvent(node, Event::ACTIVE_DESCENDANT_CHANGED); |
| active_descendant_changed_.push_back(node); |
| break; |
| case ui::AX_ATTR_CHECKED_STATE: |
| AddEvent(node, Event::CHECKED_STATE_CHANGED); |
| break; |
| case ui::AX_ATTR_INVALID_STATE: |
| AddEvent(node, Event::INVALID_STATUS_CHANGED); |
| break; |
| case ui::AX_ATTR_RESTRICTION: |
| AddEvent(node, Event::STATE_CHANGED); |
| break; |
| case ui::AX_ATTR_SCROLL_X: |
| case ui::AX_ATTR_SCROLL_Y: |
| AddEvent(node, Event::SCROLL_POSITION_CHANGED); |
| break; |
| default: |
| AddEvent(node, Event::OTHER_ATTRIBUTE_CHANGED); |
| break; |
| } |
| } |
| |
| void AXEventGenerator::OnFloatAttributeChanged(AXTree* tree, |
| AXNode* node, |
| AXFloatAttribute attr, |
| float old_value, |
| float new_value) { |
| DCHECK_EQ(tree_, tree); |
| |
| if (attr == ui::AX_ATTR_VALUE_FOR_RANGE) |
| AddEvent(node, Event::VALUE_CHANGED); |
| else |
| AddEvent(node, Event::OTHER_ATTRIBUTE_CHANGED); |
| } |
| |
| void AXEventGenerator::OnBoolAttributeChanged(AXTree* tree, |
| AXNode* node, |
| AXBoolAttribute attr, |
| bool new_value) { |
| DCHECK_EQ(tree_, tree); |
| |
| AddEvent(node, Event::OTHER_ATTRIBUTE_CHANGED); |
| } |
| |
| void AXEventGenerator::OnIntListAttributeChanged( |
| AXTree* tree, |
| AXNode* node, |
| AXIntListAttribute attr, |
| const std::vector<int32_t>& old_value, |
| const std::vector<int32_t>& new_value) { |
| DCHECK_EQ(tree_, tree); |
| AddEvent(node, Event::OTHER_ATTRIBUTE_CHANGED); |
| } |
| |
| void AXEventGenerator::OnStringListAttributeChanged( |
| AXTree* tree, |
| AXNode* node, |
| AXStringListAttribute attr, |
| const std::vector<std::string>& old_value, |
| const std::vector<std::string>& new_value) { |
| DCHECK_EQ(tree_, tree); |
| } |
| |
| void AXEventGenerator::OnTreeDataChanged(AXTree* tree, |
| const ui::AXTreeData& old_tree_data, |
| const ui::AXTreeData& new_tree_data) { |
| DCHECK_EQ(tree_, tree); |
| |
| if (new_tree_data.loaded && !old_tree_data.loaded) |
| AddEvent(tree->root(), Event::LOAD_COMPLETE); |
| if (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); |
| } |
| if (new_tree_data.title != old_tree_data.title) |
| AddEvent(tree->root(), Event::DOCUMENT_TITLE_CHANGED); |
| } |
| |
| void AXEventGenerator::OnNodeWillBeDeleted(AXTree* tree, AXNode* node) { |
| DCHECK_EQ(tree_, tree); |
| tree_events_.erase(node); |
| } |
| |
| void AXEventGenerator::OnSubtreeWillBeDeleted(AXTree* tree, AXNode* node) { |
| DCHECK_EQ(tree_, tree); |
| } |
| |
| void AXEventGenerator::OnNodeWillBeReparented(AXTree* tree, AXNode* node) { |
| DCHECK_EQ(tree_, tree); |
| tree_events_.erase(node); |
| } |
| |
| void AXEventGenerator::OnSubtreeWillBeReparented(AXTree* tree, AXNode* node) { |
| DCHECK_EQ(tree_, tree); |
| } |
| |
| void AXEventGenerator::OnNodeCreated(AXTree* tree, AXNode* node) { |
| DCHECK_EQ(tree_, tree); |
| } |
| |
| void AXEventGenerator::OnNodeReparented(AXTree* tree, AXNode* node) { |
| DCHECK_EQ(tree_, tree); |
| } |
| |
| void AXEventGenerator::OnNodeChanged(AXTree* tree, AXNode* node) { |
| DCHECK_EQ(tree_, tree); |
| } |
| |
| void AXEventGenerator::OnAtomicUpdateFinished( |
| AXTree* tree, |
| bool root_changed, |
| const std::vector<Change>& changes) { |
| DCHECK_EQ(tree_, tree); |
| |
| if (root_changed && tree->data().loaded) |
| AddEvent(tree->root(), Event::LOAD_COMPLETE); |
| |
| for (const auto& change : changes) { |
| if ((change.type == NODE_CREATED || change.type == SUBTREE_CREATED)) { |
| if (change.node->data().HasStringAttribute(ui::AX_ATTR_LIVE_STATUS)) { |
| if (change.node->data().role == ui::AX_ROLE_ALERT) |
| AddEvent(change.node, Event::ALERT); |
| else |
| AddEvent(change.node, Event::LIVE_REGION_CREATED); |
| } else if (change.node->data().HasStringAttribute( |
| ui::AX_ATTR_CONTAINER_LIVE_STATUS) && |
| change.node->data().HasStringAttribute(ui::AX_ATTR_NAME)) { |
| FireLiveRegionEvents(change.node); |
| } |
| } |
| } |
| |
| FireActiveDescendantEvents(); |
| } |
| |
| void AXEventGenerator::FireLiveRegionEvents(AXNode* node) { |
| ui::AXNode* live_root = node; |
| while (live_root && |
| !live_root->data().HasStringAttribute(ui::AX_ATTR_LIVE_STATUS)) |
| live_root = live_root->parent(); |
| |
| if (live_root && !live_root->data().GetBoolAttribute(ui::AX_ATTR_BUSY)) { |
| // Fire LIVE_REGION_NODE_CHANGED on each node that changed. |
| if (!node->data().GetStringAttribute(ui::AX_ATTR_NAME).empty()) |
| AddEvent(node, Event::LIVE_REGION_NODE_CHANGED); |
| // Fire LIVE_REGION_NODE_CHANGED on the root of the live region. |
| AddEvent(live_root, Event::LIVE_REGION_CHANGED); |
| } |
| } |
| |
| void AXEventGenerator::FireActiveDescendantEvents() { |
| for (AXNode* node : active_descendant_changed_) { |
| AXNode* descendant = tree_->GetFromId( |
| node->data().GetIntAttribute(ui::AX_ATTR_ACTIVEDESCENDANT_ID)); |
| if (!descendant) |
| continue; |
| switch (descendant->data().role) { |
| case ui::AX_ROLE_MENU_ITEM: |
| case ui::AX_ROLE_MENU_ITEM_CHECK_BOX: |
| case ui::AX_ROLE_MENU_ITEM_RADIO: |
| case ui::AX_ROLE_MENU_LIST_OPTION: |
| AddEvent(descendant, Event::MENU_ITEM_SELECTED); |
| break; |
| default: |
| break; |
| } |
| } |
| active_descendant_changed_.clear(); |
| } |
| |
| } // namespace ui |