| // 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 <algorithm> |
| |
| #include "base/containers/contains.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| |
| namespace ui { |
| namespace { |
| |
| bool IsActiveLiveRegion(const AXTreeObserver::Change& change) { |
| return change.node->data().HasStringAttribute( |
| ax::mojom::StringAttribute::kLiveStatus) && |
| change.node->data().GetStringAttribute( |
| ax::mojom::StringAttribute::kLiveStatus) != "off"; |
| } |
| |
| bool IsContainedInLiveRegion(const AXTreeObserver::Change& change) { |
| return change.node->data().HasStringAttribute( |
| ax::mojom::StringAttribute::kContainerLiveStatus) && |
| change.node->data().HasStringAttribute( |
| ax::mojom::StringAttribute::kName); |
| } |
| |
| bool HasEvent(const std::set<AXEventGenerator::EventParams>& node_events, |
| AXEventGenerator::Event event) { |
| for (auto& iter : node_events) { |
| if (iter.event == event) |
| return true; |
| } |
| return false; |
| } |
| |
| void RemoveEvent(std::set<AXEventGenerator::EventParams>* node_events, |
| AXEventGenerator::Event event) { |
| for (auto& iter : *node_events) { |
| if (iter.event == event) { |
| node_events->erase(iter); |
| return; |
| } |
| } |
| } |
| |
| // If a node toggled its ignored state, don't also fire children-changed because |
| // platforms likely will do that in response to ignored-changed. Also do not |
| // fire parent-changed on ignored nodes because functionally the parent did not |
| // change as far as platform assistive technologies are concerned. |
| // Suppress name- and description-changed because those can be emitted as a side |
| // effect of calculating alternative text values for a newly-displayed object. |
| // Ditto for text attributes such as foreground and background colors, or |
| // display changing from "none" to "block." |
| 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); |
| } |
| |
| // Add a particular AXEventGenerator::IgnoredChangedState to |
| // |ignored_changed_states|. |
| void AddIgnoredChangedState( |
| AXEventGenerator::IgnoredChangedStatesBitset& ignored_changed_states, |
| AXEventGenerator::IgnoredChangedState state) { |
| ignored_changed_states.set(static_cast<size_t>(state)); |
| } |
| |
| // Returns true if |ignored_changed_states| contains a particular |
| // AXEventGenerator::IgnoredChangedState. |
| bool HasIgnoredChangedState( |
| AXEventGenerator::IgnoredChangedStatesBitset& ignored_changed_states, |
| AXEventGenerator::IgnoredChangedState state) { |
| return ignored_changed_states[static_cast<size_t>(state)]; |
| } |
| |
| } // namespace |
| |
| AXEventGenerator::EventParams::EventParams( |
| const Event event, |
| const ax::mojom::EventFrom event_from, |
| const std::vector<AXEventIntent>& event_intents) |
| : event(event), event_from(event_from), event_intents(event_intents) {} |
| |
| AXEventGenerator::EventParams::EventParams(const EventParams& other) = default; |
| |
| AXEventGenerator::EventParams::~EventParams() = 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(AXNode* node, |
| const EventParams& event_params) |
| : node(node), event_params(event_params) { |
| DCHECK(node); |
| } |
| |
| AXEventGenerator::Iterator::Iterator( |
| const std::map<AXNode*, std::set<EventParams>>& map, |
| const std::map<AXNode*, std::set<EventParams>>::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; |
| |
| DCHECK(set_iter_ != map_iter_->second.end()); |
| set_iter_++; |
| |
| // |map_| may contain empty sets of events in its entries (i.e. |set_iter_| is |
| // at the iterator's end). In this case, we want to increment |map_iter_| to |
| // point to the next entry of |map_| that contains non-empty set of events. |
| 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_event_observer_.Add(tree_); |
| } |
| |
| AXEventGenerator::~AXEventGenerator() = default; |
| |
| void AXEventGenerator::SetTree(AXTree* new_tree) { |
| if (tree_) |
| tree_event_observer_.Remove(tree_); |
| tree_ = new_tree; |
| if (tree_) |
| tree_event_observer_.Add(tree_); |
| } |
| |
| void AXEventGenerator::ReleaseTree() { |
| tree_event_observer_.RemoveAll(); |
| 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(); |
| |
| // |tree_events_| may contain empty sets of events in its first entry |
| // (i.e. |set_iter| is at the iterator's end). In this case, we want to |
| // increment |map_iter| to point to the next entry of |tree_events_| that |
| // contains a non-empty set of events. |
| 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(tree_events_, map_iter); |
| } |
| |
| AXEventGenerator::Iterator AXEventGenerator::end() const { |
| return AXEventGenerator::Iterator(tree_events_, tree_events_.end()); |
| } |
| |
| void AXEventGenerator::ClearEvents() { |
| tree_events_.clear(); |
| } |
| |
| void AXEventGenerator::AddEvent(AXNode* node, AXEventGenerator::Event event) { |
| DCHECK(node); |
| |
| if (node->data().role == ax::mojom::Role::kInlineTextBox) |
| return; |
| |
| std::set<EventParams>& node_events = tree_events_[node]; |
| node_events.emplace(event, ax::mojom::EventFrom::kNone, |
| tree_->event_intents()); |
| } |
| |
| void AXEventGenerator::OnNodeDataChanged(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 or a line break 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 && |
| !ui::IsText(new_node_data.role)) { |
| AXNode* node = tree_->GetFromId(new_node_data.id); |
| if (node) |
| AddEvent(node, Event::CHILDREN_CHANGED); |
| } |
| |
| // If the ignored state of a node has changed, the inclusion/exclusion of that |
| // node in platform accessibility trees will change. Fire PARENT_CHANGED on |
| // the children of a node whose ignored state changed in order to notify ATs |
| // that existing children may have been reparented. |
| // |
| // We don't fire parent-changed if the invisible state of the node has changed |
| // because when invisibility changes, the entire subtree is being inserted / |
| // removed. For example if the 'hidden' property is changed on list item, we |
| // should not fire parent-changed on the list marker or static text. |
| if (old_node_data.IsIgnored() != new_node_data.IsIgnored() && |
| !old_node_data.IsInvisible() && !new_node_data.IsInvisible()) { |
| AXNode* node = tree_->GetFromId(new_node_data.id); |
| for (size_t i = 0; i < node->GetUnignoredChildCount(); ++i) { |
| AXNode* child = node->GetUnignoredChildAtIndex(i); |
| AddEvent(child, Event::PARENT_CHANGED); |
| } |
| } |
| } |
| |
| void AXEventGenerator::OnRoleChanged(AXTree* tree, |
| AXNode* node, |
| ax::mojom::Role old_role, |
| ax::mojom::Role new_role) { |
| DCHECK_EQ(tree_, tree); |
| AddEvent(node, Event::ROLE_CHANGED); |
| } |
| |
| void AXEventGenerator::OnStateChanged(AXTree* tree, |
| AXNode* node, |
| ax::mojom::State state, |
| bool new_value) { |
| DCHECK_EQ(tree_, tree); |
| |
| if (state != ax::mojom::State::kIgnored) { |
| AddEvent(node, Event::STATE_CHANGED); |
| AddEvent(node, Event::WIN_IACCESSIBLE_STATE_CHANGED); |
| } |
| |
| switch (state) { |
| case ax::mojom::State::kExpanded: |
| AddEvent(node, new_value ? Event::EXPANDED : Event::COLLAPSED); |
| |
| if (node->data().role == ax::mojom::Role::kRow || |
| node->data().role == ax::mojom::Role::kTreeItem) { |
| AXNode* container = node; |
| while (container && !IsRowContainer(container->data().role)) |
| container = container->parent(); |
| if (container) |
| AddEvent(container, Event::ROW_COUNT_CHANGED); |
| } |
| break; |
| case ax::mojom::State::kIgnored: { |
| AXNode* unignored_parent = node->GetUnignoredParent(); |
| if (unignored_parent) |
| AddEvent(unignored_parent, Event::CHILDREN_CHANGED); |
| AddEvent(node, Event::IGNORED_CHANGED); |
| if (!new_value) |
| AddEvent(node, Event::SUBTREE_CREATED); |
| 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; |
| 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::kAriaInvalidValue: |
| AddEvent(node, Event::INVALID_STATUS_CHANGED); |
| break; |
| case ax::mojom::StringAttribute::kAutoComplete: |
| AddEvent(node, Event::AUTO_COMPLETE_CHANGED); |
| break; |
| case ax::mojom::StringAttribute::kClassName: |
| AddEvent(node, Event::CLASS_NAME_CHANGED); |
| break; |
| case ax::mojom::StringAttribute::kDescription: |
| AddEvent(node, Event::DESCRIPTION_CHANGED); |
| break; |
| case ax::mojom::StringAttribute::kFontFamily: |
| AddEvent(node, Event::TEXT_ATTRIBUTE_CHANGED); |
| break; |
| case ax::mojom::StringAttribute::kImageAnnotation: |
| // The image annotation is reported as part of the accessible name. |
| 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); |
| |
| // Fire a LIVE_REGION_CREATED if the previous value was off, and the new |
| // value is not-off. |
| if (!IsAlert(node->data().role)) { |
| bool old_state = !old_value.empty() && old_value != "off"; |
| bool new_state = !new_value.empty() && new_value != "off"; |
| if (!old_state && new_state) |
| AddEvent(node, Event::LIVE_REGION_CREATED); |
| } |
| break; |
| case ax::mojom::StringAttribute::kName: |
| // If the name of the root node changes, we expect OnTreeDataChanged to |
| // add a DOCUMENT_TITLE_CHANGED event instead. |
| if (node != tree->root()) |
| AddEvent(node, Event::NAME_CHANGED); |
| |
| // If it's in a live region, fire live region events. |
| if (node->data().HasStringAttribute( |
| ax::mojom::StringAttribute::kContainerLiveStatus)) { |
| FireLiveRegionEvents(node); |
| } |
| |
| // If it's a change to static text, and it's in an editable text field, |
| // fire an event on the editable root. |
| if (IsText(node->data().role)) { |
| AXNode* text_field = node->GetTextFieldAncestor(); |
| if (text_field) |
| AddEvent(text_field, Event::EDITABLE_TEXT_CHANGED); |
| } |
| 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->data().role)) { |
| AddEvent(node, Event::SELECTED_VALUE_CHANGED); |
| } else if (node->data().IsTextField()) { |
| AddEvent(node, Event::VALUE_IN_TEXT_FIELD_CHANGED); |
| } |
| break; |
| default: |
| AddEvent(node, Event::OTHER_ATTRIBUTE_CHANGED); |
| 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: |
| // Don't fire on invisible containers, as it confuses some screen readers, |
| // such as NVDA. |
| if (!node->data().HasState(ax::mojom::State::kInvisible)) { |
| 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::kDropeffect: |
| AddEvent(node, Event::DROPEFFECT_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: |
| // Ignore sort direction changes on roles other than table headers and |
| // grid headers. |
| if (IsTableHeader(node->data().role)) |
| AddEvent(node, Event::SORT_CHANGED); |
| break; |
| case ax::mojom::IntAttribute::kImageAnnotationStatus: |
| // The image annotation is reported as part of the accessible name. |
| 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: |
| AddEvent(node, Event::TEXT_ATTRIBUTE_CHANGED); |
| break; |
| case ax::mojom::IntAttribute::kTextAlign: |
| // Alignment is exposed as an object attribute because it cannot apply to |
| // a substring. However, for some platforms (e.g. ATK), alignment is a |
| // text attribute. Therefore fire both events to ensure platforms get the |
| // expected notifications. |
| AddEvent(node, Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED); |
| AddEvent(node, Event::OBJECT_ATTRIBUTE_CHANGED); |
| break; |
| default: |
| AddEvent(node, Event::OTHER_ATTRIBUTE_CHANGED); |
| 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: |
| AddEvent(node, Event::TEXT_ATTRIBUTE_CHANGED); |
| break; |
| case ax::mojom::FloatAttribute::kTextIndent: |
| // Indentation is exposed as an object attribute because it cannot apply |
| // to a substring. However, for some platforms (e.g. ATK), alignment is a |
| // text attribute. Therefore fire both events to ensure platforms get the |
| // expected notifications. |
| AddEvent(node, Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED); |
| AddEvent(node, Event::OBJECT_ATTRIBUTE_CHANGED); |
| break; |
| default: |
| AddEvent(node, Event::OTHER_ATTRIBUTE_CHANGED); |
| 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); |
| // Fire an 'invalidated' event when aria-busy becomes false |
| if (!new_value) |
| AddEvent(node, Event::LAYOUT_INVALIDATED); |
| break; |
| case ax::mojom::BoolAttribute::kGrabbed: |
| AddEvent(node, Event::GRABBED_CHANGED); |
| 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->data().role)) |
| container = container->parent(); |
| if (container) |
| AddEvent(container, Event::SELECTED_CHILDREN_CHANGED); |
| break; |
| } |
| default: |
| AddEvent(node, Event::OTHER_ATTRIBUTE_CHANGED); |
| 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::kDescribedbyIds: |
| AddEvent(node, Event::DESCRIBED_BY_CHANGED); |
| break; |
| case ax::mojom::IntListAttribute::kFlowtoIds: { |
| AddEvent(node, Event::FLOW_TO_CHANGED); |
| |
| // Fire FLOW_FROM_CHANGED for all nodes added or removed |
| for (int32_t id : ComputeIntListDifference(old_value, new_value)) { |
| if (auto* 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: |
| // On a native text field, the spelling- and grammar-error markers are |
| // associated with children not exposed on any platform. Therefore, we |
| // adjust the node we fire that event on here. |
| if (AXNode* text_field = node->GetTextFieldAncestor()) { |
| AddEvent(text_field, Event::TEXT_ATTRIBUTE_CHANGED); |
| } else { |
| AddEvent(node, Event::TEXT_ATTRIBUTE_CHANGED); |
| } |
| break; |
| default: |
| AddEvent(node, Event::OTHER_ATTRIBUTE_CHANGED); |
| 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.loaded && !old_tree_data.loaded && |
| ShouldFireLoadEvents(tree->root())) { |
| AddEvent(tree->root(), Event::LOAD_COMPLETE); |
| } |
| |
| 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); |
| |
| // A special event is also fired internally for selection changes in text |
| // fields. The reasons are both historical and in order to have a unified |
| // way of handling selection changes between Web and Views. Views don't have |
| // the concept of a document selection but some individual Views controls |
| // have the ability for the user to select text inside them. |
| const AXNode* selection_focus = |
| tree_->GetFromId(new_tree_data.sel_focus_object_id); |
| if (selection_focus) { |
| // Even if it is possible for the document selection to span multiple text |
| // fields, an event should still fire on the field where the selection |
| // ends. |
| if (AXNode* text_field = selection_focus->GetTextFieldAncestor()) |
| AddEvent(text_field, Event::SELECTION_IN_TEXT_FIELD_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::OnNodeReparented(AXTree* tree, AXNode* node) { |
| DCHECK_EQ(tree_, tree); |
| AddEvent(node, Event::PARENT_CHANGED); |
| } |
| |
| void AXEventGenerator::OnAtomicUpdateFinished( |
| AXTree* tree, |
| bool root_changed, |
| const std::vector<Change>& changes) { |
| DCHECK_EQ(tree_, tree); |
| DCHECK(tree->root()); |
| |
| if (root_changed && ShouldFireLoadEvents(tree->root())) { |
| if (tree->data().loaded) |
| AddEvent(tree->root(), Event::LOAD_COMPLETE); |
| else |
| AddEvent(tree->root(), Event::LOAD_START); |
| } |
| |
| 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) { |
| FireValueInTextFieldChangedEvent(tree, change.node); |
| FireRelationSourceEvents(tree, change.node); |
| continue; |
| } |
| |
| if (IsAlert(change.node->data().role)) |
| AddEvent(change.node, Event::ALERT); |
| else if (IsActiveLiveRegion(change)) |
| AddEvent(change.node, Event::LIVE_REGION_CREATED); |
| else if (IsContainedInLiveRegion(change)) |
| FireLiveRegionEvents(change.node); |
| } |
| |
| FireActiveDescendantEvents(); |
| |
| PostprocessEvents(); |
| } |
| |
| void AXEventGenerator::AddEventsForTesting( |
| AXNode* node, |
| const std::set<EventParams>& events) { |
| DCHECK(node); |
| tree_events_[node] = events; |
| } |
| |
| void AXEventGenerator::FireLiveRegionEvents(AXNode* node) { |
| AXNode* live_root = node; |
| while (live_root && !live_root->data().HasStringAttribute( |
| ax::mojom::StringAttribute::kLiveStatus)) |
| live_root = live_root->parent(); |
| |
| if (live_root && |
| !live_root->data().GetBoolAttribute(ax::mojom::BoolAttribute::kBusy) && |
| live_root->data().GetStringAttribute( |
| ax::mojom::StringAttribute::kLiveStatus) != "off") { |
| // Fire LIVE_REGION_NODE_CHANGED on each node that changed. |
| if (!node->data() |
| .GetStringAttribute(ax::mojom::StringAttribute::kName) |
| .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( |
| ax::mojom::IntAttribute::kActivedescendantId)); |
| if (!descendant) |
| continue; |
| switch (descendant->data().role) { |
| 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(); |
| } |
| |
| void AXEventGenerator::FireValueInTextFieldChangedEvent(AXTree* tree, |
| AXNode* target_node) { |
| if (!target_node->IsText()) |
| return; |
| AXNode* text_field_ancestor = target_node->GetTextFieldAncestor(); |
| if (!text_field_ancestor) |
| return; |
| AddEvent(text_field_ancestor, Event::VALUE_IN_TEXT_FIELD_CHANGED); |
| } |
| |
| void AXEventGenerator::FireRelationSourceEvents(AXTree* tree, |
| AXNode* target_node) { |
| int32_t 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; |
| |
| auto sources = sources_it->second; |
| std::for_each(sources.begin(), sources.end(), [&](int32_t source_id) { |
| AXNode* source_node = tree->GetFromId(source_id); |
| |
| if (!source_node || source_nodes.count(source_node) > 0) |
| return; |
| |
| source_nodes.insert(source_node); |
| |
| // GCC < 6.4 requires this pointer when calling a member |
| // function in anonymous function |
| this->AddEvent(source_node, Event::RELATED_NODE_CHANGED); |
| }); |
| }; |
| |
| std::for_each(tree->int_reverse_relations().begin(), |
| tree->int_reverse_relations().end(), callback); |
| std::for_each( |
| tree->intlist_reverse_relations().begin(), |
| tree->intlist_reverse_relations().end(), [&](auto& entry) { |
| // Explicitly exclude relationships for which an additional event on the |
| // source node would cause extra noise. For example, kRadioGroupIds |
| // forms relations among all radio buttons and serves little value for |
| // AT to get events on the previous radio button in the group. |
| if (entry.first != ax::mojom::IntListAttribute::kRadioGroupIds) |
| callback(entry); |
| }); |
| } |
| |
| // Attempts to suppress load-related events that we presume no AT will be |
| // interested in under any circumstances, such as pages which have no size. |
| bool AXEventGenerator::ShouldFireLoadEvents(AXNode* node) { |
| if (always_fire_load_complete_) |
| return true; |
| |
| const AXNodeData& data = node->data(); |
| return data.relative_bounds.bounds.width() || |
| data.relative_bounds.bounds.height(); |
| } |
| |
| void AXEventGenerator::TrimEventsDueToAncestorIgnoredChanged( |
| AXNode* node, |
| std::map<AXNode*, IgnoredChangedStatesBitset>& |
| ancestor_ignored_changed_map) { |
| DCHECK(node); |
| |
| // Recursively compute and cache ancestor ignored changed results in |
| // |ancestor_ignored_changed_map|, if |node|'s ancestors have become ignored |
| // and the ancestor's ignored changed results have not been cached. |
| if (node->parent() && |
| !base::Contains(ancestor_ignored_changed_map, node->parent())) { |
| TrimEventsDueToAncestorIgnoredChanged(node->parent(), |
| ancestor_ignored_changed_map); |
| } |
| |
| // If an ancestor of |node| changed to ignored state (hide), append hide state |
| // to the corresponding entry in the map for |node|. Similarly, if an ancestor |
| // of |node| removed its ignored state (show), we append show state to the |
| // corresponding entry in map for |node| as well. If |node| flipped its |
| // ignored state as well, we want to remove various events related to |
| // IGNORED_CHANGED event. |
| const auto& parent_map_iter = |
| ancestor_ignored_changed_map.find(node->parent()); |
| const auto& curr_events_iter = tree_events_.find(node); |
| |
| // Initialize |ancestor_ignored_changed_map[node]| with an empty bitset, |
| // representing neither |node| nor its ancestor has IGNORED_CHANGED. |
| IgnoredChangedStatesBitset& ancestor_ignored_changed_states = |
| ancestor_ignored_changed_map[node]; |
| |
| // If |ancestor_ignored_changed_map| contains an entry for |node|'s |
| // ancestor's and the ancestor has either show/hide state, we want to populate |
| // |node|'s show/hide state in the map based on its cached ancestor result. |
| // An empty entry in |ancestor_ignored_changed_map| for |node| means that |
| // neither |node| nor its ancestor has IGNORED_CHANGED. |
| if (parent_map_iter != ancestor_ignored_changed_map.end()) { |
| // Propagate ancestor's show/hide states to |node|'s entry in the map. |
| if (HasIgnoredChangedState(parent_map_iter->second, |
| IgnoredChangedState::kHide)) { |
| AddIgnoredChangedState(ancestor_ignored_changed_states, |
| IgnoredChangedState::kHide); |
| } |
| if (HasIgnoredChangedState(parent_map_iter->second, |
| IgnoredChangedState::kShow)) { |
| AddIgnoredChangedState(ancestor_ignored_changed_states, |
| IgnoredChangedState::kShow); |
| } |
| |
| // If |node| has IGNORED changed with show/hide state that matches one of |
| // its ancestors' IGNORED changed show/hide states, we want to remove |
| // |node|'s IGNORED_CHANGED related events. |
| 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(ancestor_ignored_changed_states, |
| IgnoredChangedState::kHide); |
| } else { |
| AddIgnoredChangedState(ancestor_ignored_changed_states, |
| IgnoredChangedState::kShow); |
| } |
| } |
| |
| return; |
| } |
| |
| // If ignored changed results for ancestors are not cached, calculate the |
| // corresponding entry for |node| in the map using the ignored states and |
| // events of |node|. |
| if (curr_events_iter != tree_events_.end() && |
| HasEvent(curr_events_iter->second, Event::IGNORED_CHANGED)) { |
| if (node->IsIgnored()) { |
| AddIgnoredChangedState(ancestor_ignored_changed_states, |
| IgnoredChangedState::kHide); |
| } else { |
| AddIgnoredChangedState(ancestor_ignored_changed_states, |
| IgnoredChangedState::kShow); |
| } |
| |
| 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; |
| |
| // First pass through |tree_events_|, remove events that we do not need. |
| for (auto& iter : tree_events_) { |
| AXNode* node = iter.first; |
| |
| // TODO(http://crbug.com/2279799): remove all of the cases that could |
| // add a null node to |tree_events|. |
| DCHECK(node); |
| if (!node) |
| continue; |
| |
| std::set<EventParams>& node_events = iter.second; |
| |
| // A newly created live region or alert should not *also* fire a |
| // live region changed event. |
| 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)) { |
| // If a node toggled its ignored state, we only want to fire |
| // IGNORED_CHANGED event on the top most ancestor where this ignored state |
| // change takes place and suppress all the descendants's IGNORED_CHANGED |
| // events. |
| TrimEventsDueToAncestorIgnoredChanged(node, ancestor_ignored_changed_map); |
| RemoveEventsDueToIgnoredChanged(&node_events); |
| } |
| |
| // When the selected option in an expanded select element changes, the |
| // foreground and background colors change. But we don't want to treat |
| // those as text attribute changes. This can also happen when a widget |
| // such as a button becomes enabled/disabled. |
| if (HasEvent(node_events, Event::SELECTED_CHANGED) || |
| HasEvent(node_events, Event::ENABLED_CHANGED)) { |
| RemoveEvent(&node_events, Event::TEXT_ATTRIBUTE_CHANGED); |
| } |
| |
| AXNode* parent = node->GetUnignoredParent(); |
| |
| // Don't fire text attribute changed on this node if its immediate parent |
| // also has text attribute changed. |
| if (parent && HasEvent(node_events, Event::TEXT_ATTRIBUTE_CHANGED) && |
| tree_events_.find(parent) != tree_events_.end() && |
| HasEvent(tree_events_[parent], Event::TEXT_ATTRIBUTE_CHANGED)) { |
| RemoveEvent(&node_events, Event::TEXT_ATTRIBUTE_CHANGED); |
| } |
| |
| // Don't fire parent changed on this node if any of its ancestors also has |
| // parent changed. However, if the ancestor also has subtree created, it is |
| // possible that the created subtree is actually a newly unignored parent |
| // of an existing node. In that instance, we need to inform ATs that the |
| // existing node's parent has changed on the platform. |
| if (HasEvent(node_events, Event::PARENT_CHANGED)) { |
| while (parent && (tree_events_.find(parent) != tree_events_.end() || |
| base::Contains(removed_parent_changed_nodes, parent))) { |
| if ((base::Contains(removed_parent_changed_nodes, parent) || |
| HasEvent(tree_events_[parent], Event::PARENT_CHANGED)) && |
| !HasEvent(tree_events_[parent], Event::SUBTREE_CREATED)) { |
| RemoveEvent(&node_events, Event::PARENT_CHANGED); |
| removed_parent_changed_nodes.insert(node); |
| break; |
| } |
| parent = parent->GetUnignoredParent(); |
| } |
| } |
| |
| // Don't fire subtree created on this node if any of its ancestors also has |
| // subtree created. |
| parent = node->GetUnignoredParent(); |
| if (HasEvent(node_events, Event::SUBTREE_CREATED)) { |
| while (parent && |
| (tree_events_.find(parent) != tree_events_.end() || |
| base::Contains(removed_subtree_created_nodes, parent))) { |
| if (base::Contains(removed_subtree_created_nodes, parent) || |
| HasEvent(tree_events_[parent], Event::SUBTREE_CREATED)) { |
| RemoveEvent(&node_events, Event::SUBTREE_CREATED); |
| removed_subtree_created_nodes.insert(node); |
| break; |
| } |
| parent = parent->GetUnignoredParent(); |
| } |
| } |
| } |
| |
| // Second pass through |tree_events_|, remove nodes that do not have any |
| // events left. |
| 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; |
| } |
| } |
| |
| // static |
| 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; |
| } |
| } |
| |
| // static |
| 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::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::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::BUSY_CHANGED: |
| return "busyChanged"; |
| case AXEventGenerator::Event::CHECKED_STATE_CHANGED: |
| return "checkedStateChanged"; |
| case AXEventGenerator::Event::CHILDREN_CHANGED: |
| return "childrenChanged"; |
| case AXEventGenerator::Event::CLASS_NAME_CHANGED: |
| return "classNameChanged"; |
| case AXEventGenerator::Event::COLLAPSED: |
| return "collapsed"; |
| case AXEventGenerator::Event::CONTROLS_CHANGED: |
| return "controlsChanged"; |
| 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::DROPEFFECT_CHANGED: |
| return "dropeffectChanged"; |
| case ui::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::GRABBED_CHANGED: |
| return "grabbedChanged"; |
| case AXEventGenerator::Event::HASPOPUP_CHANGED: |
| return "haspopupChanged"; |
| case AXEventGenerator::Event::HIERARCHICAL_LEVEL_CHANGED: |
| return "hierarchicalLevelChanged"; |
| case ui::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::LOAD_COMPLETE: |
| return "loadComplete"; |
| case AXEventGenerator::Event::LOAD_START: |
| return "loadStart"; |
| case AXEventGenerator::Event::MENU_ITEM_SELECTED: |
| return "menuItemSelected"; |
| 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::OTHER_ATTRIBUTE_CHANGED: |
| return "otherAttributeChanged"; |
| case AXEventGenerator::Event::PARENT_CHANGED: |
| return "parentChanged"; |
| case AXEventGenerator::Event::PLACEHOLDER_CHANGED: |
| return "placeholderChanged"; |
| case AXEventGenerator::Event::PORTAL_ACTIVATED: |
| return "portalActivated"; |
| 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::SELECTION_IN_TEXT_FIELD_CHANGED: |
| return "selectionInTextFieldChanged"; |
| 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"; |
| } |
| } |
| |
| } // namespace ui |