| // Copyright 2014 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. |
| |
| // Custom bindings for the automation API. |
| var AutomationNode = require('automationNode').AutomationNode; |
| var AutomationRootNode = require('automationNode').AutomationRootNode; |
| var automation = require('binding').Binding.create('automation'); |
| var automationInternal = |
| require('binding').Binding.create('automationInternal').generate(); |
| var eventBindings = require('event_bindings'); |
| var Event = eventBindings.Event; |
| var exceptionHandler = require('uncaught_exception_handler'); |
| var forEach = require('utils').forEach; |
| var lastError = require('lastError'); |
| var logging = requireNative('logging'); |
| var nativeAutomationInternal = requireNative('automationInternal'); |
| var GetRoutingID = nativeAutomationInternal.GetRoutingID; |
| var GetSchemaAdditions = nativeAutomationInternal.GetSchemaAdditions; |
| var DestroyAccessibilityTree = |
| nativeAutomationInternal.DestroyAccessibilityTree; |
| var GetIntAttribute = nativeAutomationInternal.GetIntAttribute; |
| var StartCachingAccessibilityTrees = |
| nativeAutomationInternal.StartCachingAccessibilityTrees; |
| var AddTreeChangeObserver = nativeAutomationInternal.AddTreeChangeObserver; |
| var RemoveTreeChangeObserver = |
| nativeAutomationInternal.RemoveTreeChangeObserver; |
| var GetFocus = nativeAutomationInternal.GetFocus; |
| var schema = GetSchemaAdditions(); |
| |
| /** |
| * A namespace to export utility functions to other files in automation. |
| */ |
| window.automationUtil = function() {}; |
| |
| // TODO(aboxhall): Look into using WeakMap |
| var idToCallback = {}; |
| |
| var DESKTOP_TREE_ID = 0; |
| |
| automationUtil.storeTreeCallback = function(id, callback) { |
| if (!callback) |
| return; |
| |
| var targetTree = AutomationRootNode.get(id); |
| if (!targetTree) { |
| // If we haven't cached the tree, hold the callback until the tree is |
| // populated by the initial onAccessibilityEvent call. |
| if (id in idToCallback) |
| idToCallback[id].push(callback); |
| else |
| idToCallback[id] = [callback]; |
| } else { |
| callback(targetTree); |
| } |
| }; |
| |
| /** |
| * Global list of tree change observers. |
| * @type {Object<number, TreeChangeObserver>} |
| */ |
| automationUtil.treeChangeObserverMap = {}; |
| |
| /** |
| * The id of the next tree change observer. |
| * @type {number} |
| */ |
| automationUtil.nextTreeChangeObserverId = 1; |
| |
| /** |
| * @type {AutomationNode} The current focused node. This is only updated |
| * when calling automationUtil.updateFocusedNode. |
| */ |
| automationUtil.focusedNode = null; |
| |
| /** |
| * Update automationUtil.focusedNode to be the node that currently has focus. |
| */ |
| automationUtil.updateFocusedNode = function() { |
| automationUtil.focusedNode = null; |
| var focusedNodeInfo = GetFocus(DESKTOP_TREE_ID); |
| if (!focusedNodeInfo) |
| return; |
| var tree = AutomationRootNode.getOrCreate(focusedNodeInfo.treeId); |
| if (tree) { |
| automationUtil.focusedNode = |
| privates(tree).impl.get(focusedNodeInfo.nodeId); |
| } |
| }; |
| |
| automation.registerCustomHook(function(bindingsAPI) { |
| var apiFunctions = bindingsAPI.apiFunctions; |
| |
| // TODO(aboxhall, dtseng): Make this return the speced AutomationRootNode obj. |
| apiFunctions.setHandleRequest('getTree', function getTree(tabID, callback) { |
| var routingID = GetRoutingID(); |
| StartCachingAccessibilityTrees(); |
| |
| // enableTab() ensures the renderer for the active or specified tab has |
| // accessibility enabled, and fetches its ax tree id to use as |
| // a key in the idToAutomationRootNode map. The callback to |
| // enableTab is bound to the callback passed in to getTree(), so that once |
| // the tree is available (either due to having been cached earlier, or after |
| // an accessibility event occurs which causes the tree to be populated), the |
| // callback can be called. |
| var params = { routingID: routingID, tabID: tabID }; |
| automationInternal.enableTab(params, |
| function onEnable(id) { |
| if (lastError.hasError(chrome)) { |
| callback(); |
| return; |
| } |
| automationUtil.storeTreeCallback(id, callback); |
| }); |
| }); |
| |
| var desktopTree = null; |
| apiFunctions.setHandleRequest('getDesktop', function(callback) { |
| StartCachingAccessibilityTrees(); |
| desktopTree = AutomationRootNode.get(DESKTOP_TREE_ID); |
| if (!desktopTree) { |
| if (DESKTOP_TREE_ID in idToCallback) |
| idToCallback[DESKTOP_TREE_ID].push(callback); |
| else |
| idToCallback[DESKTOP_TREE_ID] = [callback]; |
| |
| var routingID = GetRoutingID(); |
| |
| // TODO(dtseng): Disable desktop tree once desktop object goes out of |
| // scope. |
| automationInternal.enableDesktop(routingID, function() { |
| if (lastError.hasError(chrome)) { |
| AutomationRootNode.destroy(DESKTOP_TREE_ID); |
| callback(); |
| return; |
| } |
| }); |
| } else { |
| callback(desktopTree); |
| } |
| }); |
| |
| apiFunctions.setHandleRequest('getFocus', function(callback) { |
| automationUtil.updateFocusedNode(); |
| callback(automationUtil.focusedNode); |
| }); |
| |
| function removeTreeChangeObserver(observer) { |
| for (var id in automationUtil.treeChangeObserverMap) { |
| if (automationUtil.treeChangeObserverMap[id] == observer) { |
| RemoveTreeChangeObserver(id); |
| delete automationUtil.treeChangeObserverMap[id]; |
| return; |
| } |
| } |
| } |
| apiFunctions.setHandleRequest('removeTreeChangeObserver', function(observer) { |
| removeTreeChangeObserver(observer); |
| }); |
| |
| function addTreeChangeObserver(filter, observer) { |
| removeTreeChangeObserver(observer); |
| var id = automationUtil.nextTreeChangeObserverId++; |
| AddTreeChangeObserver(id, filter); |
| automationUtil.treeChangeObserverMap[id] = observer; |
| } |
| apiFunctions.setHandleRequest('addTreeChangeObserver', |
| function(filter, observer) { |
| addTreeChangeObserver(filter, observer); |
| }); |
| |
| apiFunctions.setHandleRequest('setDocumentSelection', function(params) { |
| var anchorNodeImpl = privates(params.anchorObject).impl; |
| var focusNodeImpl = privates(params.focusObject).impl; |
| if (anchorNodeImpl.treeID !== focusNodeImpl.treeID) |
| throw new Error('Selection anchor and focus must be in the same tree.'); |
| if (anchorNodeImpl.treeID === DESKTOP_TREE_ID) { |
| throw new Error('Use AutomationNode.setSelection to set the selection ' + |
| 'in the desktop tree.'); |
| } |
| automationInternal.performAction({ treeID: anchorNodeImpl.treeID, |
| automationNodeID: anchorNodeImpl.id, |
| actionType: 'setSelection'}, |
| { focusNodeID: focusNodeImpl.id, |
| anchorOffset: params.anchorOffset, |
| focusOffset: params.focusOffset }); |
| }); |
| |
| }); |
| |
| automationInternal.onChildTreeID.addListener(function(treeID, |
| nodeID) { |
| var tree = AutomationRootNode.getOrCreate(treeID); |
| if (!tree) |
| return; |
| |
| var node = privates(tree).impl.get(nodeID); |
| if (!node) |
| return; |
| |
| // A WebView in the desktop tree has a different AX tree as its child. |
| // When we encounter a WebView with a child AX tree id that we don't |
| // currently have cached, explicitly request that AX tree from the |
| // browser process and set up a callback when it loads to attach that |
| // tree as a child of this node and fire appropriate events. |
| var childTreeID = GetIntAttribute(treeID, nodeID, 'childTreeId'); |
| if (!childTreeID) |
| return; |
| |
| var subroot = AutomationRootNode.get(childTreeID); |
| if (!subroot) { |
| automationUtil.storeTreeCallback(childTreeID, function(root) { |
| privates(root).impl.setHostNode(node); |
| |
| if (root.docLoaded) |
| privates(root).impl.dispatchEvent(schema.EventType.loadComplete); |
| |
| privates(node).impl.dispatchEvent(schema.EventType.childrenChanged); |
| }); |
| |
| automationInternal.enableFrame(childTreeID); |
| } else { |
| privates(subroot).impl.setHostNode(node); |
| } |
| }); |
| |
| automationInternal.onTreeChange.addListener(function(observerID, |
| treeID, |
| nodeID, |
| changeType) { |
| var tree = AutomationRootNode.getOrCreate(treeID); |
| if (!tree) |
| return; |
| |
| var node = privates(tree).impl.get(nodeID); |
| if (!node) |
| return; |
| |
| var observer = automationUtil.treeChangeObserverMap[observerID]; |
| if (!observer) |
| return; |
| |
| try { |
| observer({target: node, type: changeType}); |
| } catch (e) { |
| exceptionHandler.handle('Error in tree change observer for ' + |
| treeChange.type, e); |
| } |
| }); |
| |
| automationInternal.onNodesRemoved.addListener(function(treeID, nodeIDs) { |
| var tree = AutomationRootNode.getOrCreate(treeID); |
| if (!tree) |
| return; |
| |
| for (var i = 0; i < nodeIDs.length; i++) { |
| privates(tree).impl.remove(nodeIDs[i]); |
| } |
| }); |
| |
| /** |
| * Dispatch accessibility events fired on individual nodes to its |
| * corresponding AutomationNode. Handle focus events specially |
| * (see below). |
| */ |
| automationInternal.onAccessibilityEvent.addListener(function(eventParams) { |
| var id = eventParams.treeID; |
| var targetTree = AutomationRootNode.getOrCreate(id); |
| |
| // When we get a focus event, ignore the actual event target, and instead |
| // check what node has focus globally. If that represents a focus change, |
| // fire a focus event on the correct target. |
| if (eventParams.eventType == schema.EventType.focus) { |
| var previousFocusedNode = automationUtil.focusedNode; |
| automationUtil.updateFocusedNode(); |
| if (automationUtil.focusedNode && |
| automationUtil.focusedNode == previousFocusedNode) { |
| return; |
| } |
| |
| if (automationUtil.focusedNode) { |
| targetTree = automationUtil.focusedNode.root; |
| eventParams.treeID = privates(targetTree).impl.treeID; |
| eventParams.targetID = privates(automationUtil.focusedNode).impl.id; |
| } |
| } |
| |
| if (!privates(targetTree).impl.onAccessibilityEvent(eventParams)) |
| return; |
| |
| // If we're not waiting on a callback to getTree(), we can early out here. |
| if (!(id in idToCallback)) |
| return; |
| |
| // We usually get a 'placeholder' tree first, which doesn't have any url |
| // attribute or child nodes. If we've got that, wait for the full tree before |
| // calling the callback. |
| // TODO(dmazzoni): Don't send down placeholder (crbug.com/397553) |
| if (id != DESKTOP_TREE_ID && !targetTree.url && |
| targetTree.children.length == 0) |
| return; |
| |
| // If the tree wasn't available when getTree() was called, the callback will |
| // have been cached in idToCallback, so call and delete it now that we |
| // have the complete tree. |
| for (var i = 0; i < idToCallback[id].length; i++) { |
| var callback = idToCallback[id][i]; |
| callback(targetTree); |
| } |
| delete idToCallback[id]; |
| }); |
| |
| automationInternal.onAccessibilityTreeDestroyed.addListener(function(id) { |
| // Destroy the AutomationRootNode. |
| var targetTree = AutomationRootNode.get(id); |
| if (targetTree) { |
| privates(targetTree).impl.destroy(); |
| AutomationRootNode.destroy(id); |
| } else { |
| logging.WARNING('no targetTree to destroy'); |
| } |
| |
| // Destroy the native cache of the accessibility tree. |
| DestroyAccessibilityTree(id); |
| }); |
| |
| var binding = automation.generate(); |
| // Add additional accessibility bindings not specified in the automation IDL. |
| // Accessibility and automation share some APIs (see |
| // ui/accessibility/ax_enums.idl). |
| forEach(schema, function(k, v) { |
| binding[k] = v; |
| }); |
| |
| exports.$set('binding', binding); |
| |