Commit 35e54b73 authored by David Tseng's avatar David Tseng Committed by Commit Bot

Fixes performance of Google Calendar + ChromeVox/automation

In Google Calendar, the "rooms" tab of a new invite triggers a flood of events (location changes, and attribute changes).

The dynamic room dropdown appears to continually be updating itself resulting in thousands of events per second.

This change:
- minimizes the js <-> C++ calls by pushing event listener state to C++ from js
- if there's no event listener for a given event, don't incur the cost of sending it to js
- in ChromeVox, create a new class, RangeAutomationHandler, which installs event listeners scoped as much as possible, to ChromeVox's currentrange. This ignores events on other parts of the tree.

Test: manually on Google Calendar.
Change-Id: I4172f504915665dbe7d0a88df11aefceb36439b4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1947643
Commit-Queue: David Tseng <dtseng@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#721217}
parent 9e1e5d01
......@@ -101,6 +101,7 @@ chromevox_modules = [
"background/panel/panel_menu.js",
"background/panel/panel_menu_item.js",
"background/phonetic_data.js",
"background/range_automation_handler.js",
"background/recovery_strategy.js",
"background/logging/tree_dumper.js",
"background/tree_walker.js",
......
......@@ -31,6 +31,7 @@ goog.require('Output');
goog.require('Output.EventType');
goog.require('PanelCommand');
goog.require('PhoneticData');
goog.require('RangeAutomationHandler');
goog.require('constants');
goog.require('cursors.Cursor');
goog.require('BrailleKeyCommand');
......@@ -316,13 +317,6 @@ Background.prototype = {
if (!skipOutput) {
o.go();
if (range.start.node) {
// Update the DesktopAutomationHandler's state as well to ensure event
// handlers don't repeat this output.
DesktopAutomationHandler.instance.updateLastAttributeState(
range.start.node, o);
}
}
},
......
......@@ -15,12 +15,12 @@ var AutomationNode = chrome.automation.AutomationNode;
var EventType = chrome.automation.EventType;
/**
* @param {!AutomationNode} node
* @param {AutomationNode|undefined} node
* @constructor
*/
BaseAutomationHandler = function(node) {
/**
* @type {!AutomationNode}
* @type {AutomationNode|undefined}
*/
this.node_ = node;
......@@ -53,6 +53,8 @@ BaseAutomationHandler.prototype = {
this.node_.removeEventListener(
eventType, this.listeners_[eventType], true);
}
this.listeners_ = {};
},
/**
......
......@@ -48,12 +48,6 @@ DesktopAutomationHandler = function(node) {
/** @private {AutomationNode} */
this.lastValueTarget_ = null;
/** @private {AutomationNode} */
this.lastAttributeTarget_;
/** @private {Output} */
this.lastAttributeOutput_;
/** @private {string} */
this.lastRootUrl_ = '';
......@@ -63,28 +57,16 @@ DesktopAutomationHandler = function(node) {
/** @private {number?} */
this.delayedAttributeOutputId_;
this.addListener_(
EventType.ACTIVEDESCENDANTCHANGED, this.onActiveDescendantChanged);
this.addListener_(EventType.ALERT, this.onAlert);
this.addListener_(
EventType.ARIA_ATTRIBUTE_CHANGED, this.onAriaAttributeChanged);
this.addListener_(EventType.AUTOCORRECTION_OCCURED, this.onEventIfInRange);
this.addListener_(EventType.BLUR, this.onBlur);
this.addListener_(
EventType.CHECKED_STATE_CHANGED, this.onCheckedStateChanged);
this.addListener_(
EventType.DOCUMENT_SELECTION_CHANGED, this.onDocumentSelectionChanged);
this.addListener_(EventType.EXPANDED_CHANGED, this.onEventIfInRange);
this.addListener_(EventType.FOCUS, this.onFocus);
this.addListener_(EventType.HOVER, this.onHover);
this.addListener_(EventType.INVALID_STATUS_CHANGED, this.onEventIfInRange);
this.addListener_(EventType.LOAD_COMPLETE, this.onLoadComplete);
this.addListener_(EventType.LOCATION_CHANGED, this.onLocationChanged);
this.addListener_(EventType.MENU_END, this.onMenuEnd);
this.addListener_(EventType.MENU_LIST_ITEM_SELECTED, this.onEventIfSelected);
this.addListener_(EventType.MENU_START, this.onMenuStart);
this.addListener_(EventType.ROW_COLLAPSED, this.onEventIfInRange);
this.addListener_(EventType.ROW_EXPANDED, this.onEventIfInRange);
this.addListener_(
EventType.SCROLL_POSITION_CHANGED, this.onScrollPositionChanged);
this.addListener_(EventType.SELECTION, this.onSelection);
......@@ -166,59 +148,6 @@ DesktopAutomationHandler.prototype = {
output.withRichSpeechAndBraille(
ChromeVoxState.instance.currentRange, prevRange, evt.type);
output.go();
// Update some state here to ensure attribute changes don't duplicate
// output.
this.lastAttributeTarget_ = evt.target;
this.lastAttributeOutput_ = output;
},
/**
* @param {!AutomationEvent} evt
*/
onEventIfInRange: function(evt) {
if (!DesktopAutomationHandler.announceActions &&
evt.eventFrom == 'action') {
return;
}
var prev = ChromeVoxState.instance.currentRange;
if (!prev) {
return;
}
if (prev.contentEquals(cursors.Range.fromNode(evt.target)) ||
evt.target.state.focused) {
var prevTarget = this.lastAttributeTarget_;
// Re-target to active descendant if it exists.
var prevOutput = this.lastAttributeOutput_;
this.lastAttributeTarget_ = evt.target.activeDescendant || evt.target;
this.lastAttributeOutput_ = new Output().withRichSpeechAndBraille(
cursors.Range.fromNode(this.lastAttributeTarget_), prev,
Output.EventType.NAVIGATE);
if (this.lastAttributeTarget_ == prevTarget && prevOutput &&
prevOutput.equals(this.lastAttributeOutput_)) {
return;
}
// If the target or an ancestor is controlled by another control, we may
// want to delay the output.
var maybeControlledBy = evt.target;
while (maybeControlledBy) {
if (maybeControlledBy.controlledBy.length &&
maybeControlledBy.controlledBy.find((n) => !!n.autoComplete)) {
clearTimeout(this.delayedAttributeOutputId_);
this.delayedAttributeOutputId_ = setTimeout(() => {
this.lastAttributeOutput_.go();
}, DesktopAutomationHandler.ATTRIBUTE_DELAY_MS);
return;
}
maybeControlledBy = maybeControlledBy.parent;
}
this.lastAttributeOutput_.go();
}
},
/**
......@@ -230,26 +159,6 @@ DesktopAutomationHandler.prototype = {
}
},
/**
* @param {!AutomationEvent} evt
*/
onAriaAttributeChanged: function(evt) {
// Don't report changes on editable nodes since they interfere with text
// selection changes. Users can query via Search+k for the current state of
// the text field (which would also report the entire value).
if (evt.target.state[StateType.EDITABLE]) {
return;
}
// Only report attribute changes on some *Option roles if it is selected.
if ((evt.target.role == RoleType.MENU_LIST_OPTION ||
evt.target.role == RoleType.LIST_BOX_OPTION) &&
!evt.target.selected)
return;
this.onEventIfInRange(evt);
},
/**
* Handles the result of a hit test.
* @param {!AutomationNode} node The hit result.
......@@ -331,21 +240,6 @@ DesktopAutomationHandler.prototype = {
new CustomAutomationEvent(evt.type, target, evt.eventFrom));
},
/**
* Handles active descendant changes.
* @param {!AutomationEvent} evt
*/
onActiveDescendantChanged: function(evt) {
if (!evt.target.activeDescendant || !evt.target.state.focused) {
return;
}
// Various events might come before a key press (which forces flushed
// speech) and this handler. Force output to be at least category flushed.
Output.forceModeForNextSpeechUtterance(QueueMode.CATEGORY_FLUSH);
this.onEventIfInRange(evt);
},
/**
* Makes an announcement without changing focus.
* @param {!AutomationEvent} evt
......@@ -369,20 +263,6 @@ DesktopAutomationHandler.prototype = {
});
},
/**
* Provides all feedback once a checked state changed event fires.
* @param {!AutomationEvent} evt
*/
onCheckedStateChanged: function(evt) {
if (!AutomationPredicate.checkable(evt.target)) {
return;
}
var event = new CustomAutomationEvent(
EventType.CHECKED_STATE_CHANGED, evt.target, evt.eventFrom);
this.onEventIfInRange(event);
},
/**
* @param {!AutomationEvent} evt
*/
......@@ -463,6 +343,15 @@ DesktopAutomationHandler.prototype = {
* @param {!AutomationEvent} evt
*/
onLoadComplete: function(evt) {
// A load complete gets fired on the desktop node when display metrics
// change.
if (evt.target.role == RoleType.DESKTOP) {
var msg = evt.target.state[StateType.HORIZONTAL] ? 'device_landscape' :
'device_portrait';
new Output().format('@' + msg).go();
return;
}
// We are only interested in load completes on valid top level roots.
var top = AutomationUtil.getTopLevelRoot(evt.target);
if (!top || top != evt.target.root || !top.docUrl) {
......@@ -505,43 +394,6 @@ DesktopAutomationHandler.prototype = {
}.bind(this));
},
/**
* Updates the focus ring if the location of the current range, or
* an ancestor of the current range, changes.
* @param {!AutomationEvent} evt
*/
onLocationChanged: function(evt) {
if (evt.target.role == RoleType.DESKTOP) {
var msg = evt.target.state[StateType.HORIZONTAL] ? 'device_landscape' :
'device_portrait';
new Output().format('@' + msg).go();
return;
}
var cur = ChromeVoxState.instance.currentRange;
if (!cur || !cur.isValid()) {
if (ChromeVoxState.instance.getFocusBounds().length) {
ChromeVoxState.instance.setFocusBounds([]);
}
return;
}
// Rather than trying to figure out if the current range falls somewhere in
// |evt.target|, just update it if our cached bounds don't match.
var oldFocusBounds = ChromeVoxState.instance.getFocusBounds();
var startRect = cur.start.node.location;
var endRect = cur.end.node.location;
var found =
oldFocusBounds.some((rect) => this.areRectsEqual(rect, startRect)) &&
oldFocusBounds.some((rect) => this.areRectsEqual(rect, endRect));
if (found) {
return;
}
new Output().withLocation(cur, null, evt.type).go();
},
/**
* Sets whether document selections from actions should be ignored.
* @param {boolean} val
......@@ -550,17 +402,6 @@ DesktopAutomationHandler.prototype = {
this.shouldIgnoreDocumentSelectionFromAction_ = val;
},
/**
* Update the state for the last attribute change that occurred in ChromeVox
* output.
* @param {!AutomationNode} node
* @param {!Output} output
*/
updateLastAttributeState: function(node, output) {
this.lastAttributeTarget_ = node;
this.lastAttributeOutput_ = output;
},
/**
* Provides all feedback once a change event in a text field fires.
* @param {!AutomationEvent} evt
......@@ -812,17 +653,6 @@ DesktopAutomationHandler.prototype = {
o.withRichSpeechAndBraille(
ChromeVoxState.instance.currentRange, null, evt.type)
.go();
},
/**
* @param {!chrome.accessibilityPrivate.ScreenRect} rectA
* @param {!chrome.accessibilityPrivate.ScreenRect} rectB
* @return {boolean} Whether the rects are the same.
* @private
*/
areRectsEqual: function(rectA, rectB) {
return rectA.left == rectB.left && rectA.top == rectB.top &&
rectA.width == rectB.width && rectA.height == rectB.height;
}
};
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Handles automation from ChromeVox's current range.
*/
goog.provide('RangeAutomationHandler');
goog.require('BaseAutomationHandler');
goog.scope(function() {
var AutomationEvent = chrome.automation.AutomationEvent;
var AutomationNode = chrome.automation.AutomationNode;
var Dir = constants.Dir;
var EventType = chrome.automation.EventType;
var RoleType = chrome.automation.RoleType;
var StateType = chrome.automation.StateType;
/**
* @constructor
* @implements {ChromeVoxStateObserver}
* @extends {BaseAutomationHandler}
*/
RangeAutomationHandler = function() {
BaseAutomationHandler.call(this, undefined);
/** @private {AutomationNode} */
this.lastAttributeTarget_;
/** @private {Output} */
this.lastAttributeOutput_;
ChromeVoxState.addObserver(this);
};
RangeAutomationHandler.prototype = {
__proto__: BaseAutomationHandler.prototype,
/**
* @param {cursors.Range} newRange
*/
onCurrentRangeChanged: function(newRange) {
if (this.node_) {
this.removeAllListeners();
this.node_ = undefined;
}
if (!newRange || !newRange.start.node || !newRange.end.node) {
return;
}
this.node_ = AutomationUtil.getLeastCommonAncestor(
newRange.start.node, newRange.end.node) ||
newRange.start.node;
this.addListener_(
EventType.ACTIVEDESCENDANTCHANGED, this.onActiveDescendantChanged);
this.addListener_(
EventType.ARIA_ATTRIBUTE_CHANGED, this.onAriaAttributeChanged);
this.addListener_(EventType.AUTOCORRECTION_OCCURED, this.onEventIfInRange);
this.addListener_(
EventType.CHECKED_STATE_CHANGED, this.onCheckedStateChanged);
this.addListener_(EventType.EXPANDED_CHANGED, this.onEventIfInRange);
this.addListener_(EventType.INVALID_STATUS_CHANGED, this.onEventIfInRange);
this.addListener_(EventType.LOCATION_CHANGED, this.onLocationChanged);
this.addListener_(EventType.ROW_COLLAPSED, this.onEventIfInRange);
this.addListener_(EventType.ROW_EXPANDED, this.onEventIfInRange);
},
/**
* @param {!AutomationEvent} evt
*/
onEventIfInRange: function(evt) {
if (!DesktopAutomationHandler.announceActions &&
evt.eventFrom == 'action') {
return;
}
var prev = ChromeVoxState.instance.currentRange;
if (!prev) {
return;
}
var prevTarget = this.lastAttributeTarget_;
// Re-target to active descendant if it exists.
var prevOutput = this.lastAttributeOutput_;
this.lastAttributeTarget_ = evt.target.activeDescendant || evt.target;
this.lastAttributeOutput_ = new Output().withRichSpeechAndBraille(
cursors.Range.fromNode(this.lastAttributeTarget_), prev,
Output.EventType.NAVIGATE);
if (this.lastAttributeTarget_ == prevTarget && prevOutput &&
prevOutput.equals(this.lastAttributeOutput_)) {
return;
}
// If the target or an ancestor is controlled by another control, we may
// want to delay the output.
var maybeControlledBy = evt.target;
while (maybeControlledBy) {
if (maybeControlledBy.controlledBy.length &&
maybeControlledBy.controlledBy.find((n) => !!n.autoComplete)) {
clearTimeout(this.delayedAttributeOutputId_);
this.delayedAttributeOutputId_ = setTimeout(() => {
this.lastAttributeOutput_.go();
}, DesktopAutomationHandler.ATTRIBUTE_DELAY_MS);
return;
}
maybeControlledBy = maybeControlledBy.parent;
}
this.lastAttributeOutput_.go();
},
/**
* @param {!AutomationEvent} evt
*/
onAriaAttributeChanged: function(evt) {
// Don't report changes on editable nodes since they interfere with text
// selection changes. Users can query via Search+k for the current state of
// the text field (which would also report the entire value).
if (evt.target.state[StateType.EDITABLE]) {
return;
}
// Only report attribute changes on some *Option roles if it is selected.
if ((evt.target.role == RoleType.MENU_LIST_OPTION ||
evt.target.role == RoleType.LIST_BOX_OPTION) &&
!evt.target.selected)
return;
this.onEventIfInRange(evt);
},
/**
* Handles active descendant changes.
* @param {!AutomationEvent} evt
*/
onActiveDescendantChanged: function(evt) {
if (!evt.target.activeDescendant || !evt.target.state.focused) {
return;
}
// Various events might come before a key press (which forces flushed
// speech) and this handler. Force output to be at least category flushed.
Output.forceModeForNextSpeechUtterance(QueueMode.CATEGORY_FLUSH);
this.onEventIfInRange(evt);
},
/**
* Provides all feedback once a checked state changed event fires.
* @param {!AutomationEvent} evt
*/
onCheckedStateChanged: function(evt) {
if (!AutomationPredicate.checkable(evt.target)) {
return;
}
var event = new CustomAutomationEvent(
EventType.CHECKED_STATE_CHANGED, evt.target, evt.eventFrom);
this.onEventIfInRange(event);
},
/**
* Updates the focus ring if the location of the current range, or
* an descendant of the current range, changes.
* @param {!AutomationEvent} evt
*/
onLocationChanged: function(evt) {
var cur = ChromeVoxState.instance.currentRange;
if (!cur || !cur.isValid()) {
if (ChromeVoxState.instance.getFocusBounds().length) {
ChromeVoxState.instance.setFocusBounds([]);
}
return;
}
// Rather than trying to figure out if the current range falls somewhere in
// |evt.target|, just update it if our cached bounds don't match.
var oldFocusBounds = ChromeVoxState.instance.getFocusBounds();
var startRect = cur.start.node.location;
var endRect = cur.end.node.location;
var found =
oldFocusBounds.some((rect) => this.areRectsEqual_(rect, startRect)) &&
oldFocusBounds.some((rect) => this.areRectsEqual_(rect, endRect));
if (found) {
return;
}
new Output().withLocation(cur, null, evt.type).go();
},
/**
* @param {!chrome.accessibilityPrivate.ScreenRect} rectA
* @param {!chrome.accessibilityPrivate.ScreenRect} rectB
* @return {boolean} Whether the rects are the same.
* @private
*/
areRectsEqual_: function(rectA, rectB) {
return rectA.left == rectB.left && rectA.top == rectB.top &&
rectA.width == rectB.width && rectA.height == rectB.height;
}
};
}); // goog.scope
new RangeAutomationHandler();
......@@ -427,6 +427,27 @@ ui::AXNode* AutomationAXTreeWrapper::GetUnignoredNodeFromId(int32_t id) {
return (node && !node->IsIgnored()) ? node : nullptr;
}
void AutomationAXTreeWrapper::EventListenerAdded(ax::mojom::Event event_type,
ui::AXNode* node) {
node_id_to_events_[node->id()].insert(event_type);
}
void AutomationAXTreeWrapper::EventListenerRemoved(ax::mojom::Event event_type,
ui::AXNode* node) {
auto it = node_id_to_events_.find(node->id());
if (it != node_id_to_events_.end())
it->second.erase(event_type);
}
bool AutomationAXTreeWrapper::HasEventListener(ax::mojom::Event event_type,
ui::AXNode* node) {
auto it = node_id_to_events_.find(node->id());
if (it == node_id_to_events_.end())
return false;
return it->second.count(event_type);
}
// static
std::map<ui::AXTreeID, AutomationAXTreeWrapper*>&
AutomationAXTreeWrapper::GetChildTreeIDReverseMap() {
......@@ -449,6 +470,7 @@ void AutomationAXTreeWrapper::OnNodeWillBeDeleted(ui::AXTree* tree,
did_send_tree_change_during_unserialization_ |= owner_->SendTreeChangeEvent(
api::automation::TREE_CHANGE_TYPE_NODEREMOVED, tree, node);
deleted_node_ids_.push_back(node->id());
node_id_to_events_.erase(node->id());
}
void AutomationAXTreeWrapper::OnAtomicUpdateFinished(
......
......@@ -56,6 +56,11 @@ class AutomationAXTreeWrapper : public ui::AXTreeObserver {
// ignored.
ui::AXNode* GetUnignoredNodeFromId(int32_t id);
// Updates or gets this wrapper with the latest state of listeners in js.
void EventListenerAdded(ax::mojom::Event event_type, ui::AXNode* node);
void EventListenerRemoved(ax::mojom::Event event_type, ui::AXNode* node);
bool HasEventListener(ax::mojom::Event event_type, ui::AXNode* node);
private:
// AXTreeObserver overrides.
void OnNodeDataChanged(ui::AXTree* tree,
......@@ -83,6 +88,9 @@ class AutomationAXTreeWrapper : public ui::AXTreeObserver {
// reset after unserialization.
bool did_send_tree_change_during_unserialization_ = false;
// Maps a node to a set containing events for which the node has listeners.
std::map<int32_t, std::set<ax::mojom::Event>> node_id_to_events_;
DISALLOW_COPY_AND_ASSIGN(AutomationAXTreeWrapper);
};
......
......@@ -438,6 +438,53 @@ class NodeIDPlusDimensionsWrapper
AutomationInternalCustomBindings* automation_bindings_;
NodeIDPlusDimensionsFunction function_;
};
using NodeIDPlusEventFunction = void (*)(v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result,
AutomationAXTreeWrapper* tree_wrapper,
ui::AXNode* node,
ax::mojom::Event event_type);
class NodeIDPlusEventWrapper
: public base::RefCountedThreadSafe<NodeIDPlusEventWrapper> {
public:
NodeIDPlusEventWrapper(AutomationInternalCustomBindings* automation_bindings,
NodeIDPlusEventFunction function)
: automation_bindings_(automation_bindings), function_(function) {}
void Run(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = automation_bindings_->GetIsolate();
if (args.Length() < 3 || !args[0]->IsString() || !args[1]->IsInt32() ||
!args[2]->IsString()) {
ThrowInvalidArgumentsException(automation_bindings_);
}
ui::AXTreeID tree_id =
ui::AXTreeID::FromString(*v8::String::Utf8Value(isolate, args[0]));
int node_id = args[1].As<v8::Int32>()->Value();
ax::mojom::Event event_type =
ui::ParseEvent(*v8::String::Utf8Value(isolate, args[2]));
AutomationAXTreeWrapper* tree_wrapper =
automation_bindings_->GetAutomationAXTreeWrapperFromTreeID(tree_id);
if (!tree_wrapper)
return;
ui::AXNode* node = tree_wrapper->GetUnignoredNodeFromId(node_id);
if (!node)
return;
function_(isolate, args.GetReturnValue(), tree_wrapper, node, event_type);
}
private:
virtual ~NodeIDPlusEventWrapper() {}
friend class base::RefCountedThreadSafe<NodeIDPlusEventWrapper>;
AutomationInternalCustomBindings* automation_bindings_;
NodeIDPlusEventFunction function_;
};
} // namespace
class AutomationMessageFilter : public IPC::MessageFilter {
......@@ -1422,6 +1469,20 @@ void AutomationInternalCustomBindings::AddRoutes() {
if (node->GetTableCellAriaRowIndex())
result.Set(*node->GetTableCellAriaRowIndex());
});
RouteNodeIDPlusEventFunction(
"EventListenerAdded",
[](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
AutomationAXTreeWrapper* tree_wrapper, ui::AXNode* node,
ax::mojom::Event event_type) {
tree_wrapper->EventListenerAdded(event_type, node);
});
RouteNodeIDPlusEventFunction(
"EventListenerRemoved",
[](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
AutomationAXTreeWrapper* tree_wrapper, ui::AXNode* node,
ax::mojom::Event event_type) {
tree_wrapper->EventListenerRemoved(event_type, node);
});
}
void AutomationInternalCustomBindings::Invalidate() {
......@@ -1913,6 +1974,14 @@ void AutomationInternalCustomBindings::RouteNodeIDPlusDimensionsFunction(
name, base::BindRepeating(&NodeIDPlusDimensionsWrapper::Run, wrapper));
}
void AutomationInternalCustomBindings::RouteNodeIDPlusEventFunction(
const std::string& name,
NodeIDPlusEventFunction callback) {
auto wrapper = base::MakeRefCounted<NodeIDPlusEventWrapper>(this, callback);
RouteHandlerFunction(
name, base::BindRepeating(&NodeIDPlusEventWrapper::Run, wrapper));
}
void AutomationInternalCustomBindings::GetChildIDAtIndex(
const v8::FunctionCallbackInfo<v8::Value>& args) {
if (args.Length() < 3 || !args[2]->IsNumber()) {
......@@ -2088,6 +2157,27 @@ void AutomationInternalCustomBindings::SendAutomationEvent(
const gfx::Point& mouse_location,
const ui::AXEvent& event,
api::automation::EventType event_type) {
AutomationAXTreeWrapper* tree_wrapper =
GetAutomationAXTreeWrapperFromTreeID(tree_id);
if (!tree_wrapper)
return;
ui::AXNode* node = tree_wrapper->tree()->GetFromId(event.id);
if (!node)
return;
// Both the kNone| and |kHitTestResult| events get used internally to trigger
// other behaviors in js.
bool fire_event = event.event_type == ax::mojom::Event::kNone ||
event.event_type == ax::mojom::Event::kHitTestResult;
while (node && tree_wrapper && !fire_event) {
if (tree_wrapper->HasEventListener(event.event_type, node))
fire_event = true;
node = GetParent(node, &tree_wrapper);
}
if (!fire_event)
return;
auto event_params = std::make_unique<base::DictionaryValue>();
event_params->SetString("treeID", tree_id.ToString());
event_params->SetInteger("targetID", event.id);
......
......@@ -175,6 +175,13 @@ class AutomationInternalCustomBindings : public ObjectBackedNativeHandler {
int end,
int width,
int height));
void RouteNodeIDPlusEventFunction(
const std::string& name,
void (*callback)(v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result,
AutomationAXTreeWrapper* tree_wrapper,
ui::AXNode* node,
ax::mojom::Event event_type));
//
// Access the cached accessibility trees and properties of their nodes.
......
......@@ -501,6 +501,20 @@ var GetWordStartOffsets = natives.GetWordStartOffsets;
*/
var GetWordEndOffsets = natives.GetWordEndOffsets;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} eventType
*/
var EventListenerAdded = natives.EventListenerAdded;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} eventType
*/
var EventListenerRemoved = natives.EventListenerRemoved;
var logging = requireNative('logging');
var utils = require('utils');
......@@ -924,6 +938,7 @@ AutomationNodeImpl.prototype = {
callback: callback,
capture: !!capture,
});
EventListenerAdded(this.treeID, this.id, eventType);
},
// TODO(dtseng/aboxhall): Check this impl against spec.
......@@ -934,6 +949,10 @@ AutomationNodeImpl.prototype = {
if (callback === listeners[i].callback)
$Array.splice(listeners, i, 1);
}
if (listeners.length == 0) {
EventListenerRemoved(this.treeID, this.id, eventType);
}
}
},
......
......@@ -77,5 +77,5 @@ int32_t AXRootObjWrapper::GetUniqueId() const {
void AXRootObjWrapper::OnDisplayMetricsChanged(const display::Display& display,
uint32_t changed_metrics) {
delegate_->OnEvent(this, ax::mojom::Event::kLocationChanged);
delegate_->OnEvent(this, ax::mojom::Event::kLoadComplete);
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment