Index: chrome/browser/resources/chromeos/switch_access/automation_manager.js |
diff --git a/chrome/browser/resources/chromeos/switch_access/automation_manager.js b/chrome/browser/resources/chromeos/switch_access/automation_manager.js |
index b23d3deed4e95f3cbc4c0fe4cd968840b32a0ce5..6c1d39f6cf7ebf68aa019123e2f0a66d846bb221 100644 |
--- a/chrome/browser/resources/chromeos/switch_access/automation_manager.js |
+++ b/chrome/browser/resources/chromeos/switch_access/automation_manager.js |
@@ -7,44 +7,45 @@ |
* to and selecting nodes. |
* |
* @constructor |
+ * @param {!chrome.automation.AutomationNode} desktop |
*/ |
-function AutomationManager() { |
+function AutomationManager(desktop) { |
/** |
* Currently highlighted node. |
* |
- * @private {chrome.automation.AutomationNode} |
+ * @private {!chrome.automation.AutomationNode} |
*/ |
- this.node_ = null; |
+ this.node_ = desktop; |
/** |
* The root of the subtree that the user is navigating through. |
* |
- * @private {chrome.automation.AutomationNode} |
+ * @private {!chrome.automation.AutomationNode} |
*/ |
- this.scope_ = null; |
+ this.scope_ = desktop; |
/** |
* The desktop node. |
* |
- * @private {chrome.automation.AutomationNode} |
+ * @private {!chrome.automation.AutomationNode} |
*/ |
- this.desktop_ = null; |
+ this.desktop_ = desktop; |
/** |
* A stack of past scopes. Allows user to traverse back to previous groups |
* after selecting one or more groups. The most recent group is at the end |
* of the array. |
* |
- * @private {Array<chrome.automation.AutomationNode>} |
+ * @private {Array<!chrome.automation.AutomationNode>} |
*/ |
this.scopeStack_ = []; |
/** |
* Moves to the appropriate node in the accessibility tree. |
* |
- * @private {AutomationTreeWalker} |
+ * @private {!AutomationTreeWalker} |
*/ |
- this.treeWalker_ = null; |
+ this.treeWalker_ = this.createTreeWalker_(desktop); |
this.init_(); |
}; |
@@ -69,14 +70,112 @@ AutomationManager.prototype = { |
* @private |
*/ |
init_: function() { |
- chrome.automation.getDesktop(function(desktop) { |
- this.node_ = desktop; |
- this.scope_ = desktop; |
- this.desktop_ = desktop; |
- this.treeWalker_ = this.createTreeWalker_(desktop); |
- console.log('AutomationNode for desktop is loaded'); |
+ console.log('AutomationNode for desktop is loaded'); |
+ this.printNode_(this.node_); |
+ |
+ this.desktop_.addEventListener( |
+ chrome.automation.EventType.FOCUS, |
+ this.handleFocusChange_.bind(this), |
+ false); |
+ |
+ // TODO(elichtenberg): Eventually use a more specific filter than |
+ // ALL_TREE_CHANGES. |
+ chrome.automation.addTreeChangeObserver( |
+ chrome.automation.TreeChangeObserverFilter.ALL_TREE_CHANGES, |
+ this.handleNodeRemoved_.bind(this)); |
+ }, |
+ |
+ /** |
+ * When an interesting element gains focus on the page, move to it. If an |
+ * element gains focus but is not interesting, move to the next interesting |
+ * node after it. |
+ * |
+ * @param {!chrome.automation.AutomationEvent} event |
+ * @private |
+ */ |
+ handleFocusChange_: function(event) { |
+ if (this.node_ === event.target) |
+ return; |
+ console.log('Focus changed'); |
+ |
+ // Rebuild scope stack and set scope for focused node. |
+ this.buildScopeStack_(event.target); |
+ |
+ // Move to focused node. |
+ this.node_ = event.target; |
+ this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); |
+ |
+ // In case the node that gained focus is not a subtreeLeaf. |
+ if (AutomationPredicate.isSubtreeLeaf(this.node_, this.scope_)) { |
this.printNode_(this.node_); |
- }.bind(this)); |
+ this.updateFocusRing_(); |
+ } else |
+ this.moveToNode(true); |
+ }, |
+ |
+ /** |
+ * Create a new scope stack and set the current scope for |node|. |
+ * |
+ * @param {!chrome.automation.AutomationNode} node |
+ * @private |
+ */ |
+ buildScopeStack_: function(node) { |
+ // Create list of |node|'s ancestors, with highest level ancestor at the |
+ // end. |
+ let ancestorList = []; |
+ while (node.parent) { |
+ ancestorList.push(node.parent); |
+ node = node.parent; |
+ } |
+ |
+ // Starting with desktop as the scope, if an ancestor is a group, set it to |
+ // the new scope and push the old scope onto the scope stack. |
+ this.scopeStack_ = []; |
+ this.scope_ = this.desktop_; |
+ while (ancestorList.length > 0) { |
+ let ancestor = ancestorList.pop(); |
+ if (ancestor.role === chrome.automation.RoleType.DESKTOP) |
+ continue; |
+ if (AutomationPredicate.isGroup(ancestor, this.scope_)) { |
+ this.scopeStack_.push(this.scope_); |
+ this.scope_ = ancestor; |
+ } |
+ } |
+ }, |
+ |
+ /** |
+ * When a node is removed from the page, move to a new valid node. |
+ * |
+ * @param {!chrome.automation.TreeChange} treeChange |
+ * @private |
+ */ |
+ handleNodeRemoved_: function(treeChange) { |
+ // TODO(elichtenberg): Only listen to NODE_REMOVED callbacks. Don't need |
+ // any others. |
+ if (treeChange.type !== chrome.automation.TreeChangeType.NODE_REMOVED) |
+ return; |
+ |
+ // TODO(elichtenberg): Currently not getting NODE_REMOVED event when whole |
+ // tree is deleted. Once fixed, can delete this. Should only need to check |
+ // if target is current node. |
+ let removedByRWA = |
+ treeChange.target.role === chrome.automation.RoleType.ROOT_WEB_AREA |
+ && !this.node_.role; |
+ |
+ if (!removedByRWA && treeChange.target !== this.node_) |
+ return; |
+ |
+ console.log('Node removed'); |
+ chrome.accessibilityPrivate.setFocusRing([]); |
+ |
+ // Current node not invalid until after treeChange callback, so move to |
+ // valid node after callback. Delay added to prevent moving to another |
+ // node about to be made invalid. If already at a valid node (e.g., user |
+ // moves to it or focus changes to it), won't need to move to a new node. |
+ window.setTimeout(function() { |
+ if (!this.node_.role) |
+ this.moveToNode(true); |
+ }.bind(this), 100); |
}, |
/** |
@@ -88,22 +187,14 @@ AutomationManager.prototype = { |
* @param {boolean} doNext |
*/ |
moveToNode: function(doNext) { |
- if (!this.treeWalker_) |
- return; |
+ // If node is invalid, set node to last valid scope. |
+ this.startAtValidNode_(); |
let node = this.treeWalker_.moveToNode(doNext); |
if (node) { |
this.node_ = node; |
this.printNode_(this.node_); |
- |
- let color; |
- if (this.node_ === this.scope_) |
- color = AutomationManager.Color.SCOPE; |
- else if (AutomationPredicate.isInteresting(this.node_)) |
- color = AutomationManager.Color.LEAF; |
- else |
- color = AutomationManager.Color.GROUP; |
- chrome.accessibilityPrivate.setFocusRing([this.node_.location], color); |
+ this.updateFocusRing_(); |
} |
}, |
@@ -116,7 +207,7 @@ AutomationManager.prototype = { |
* it. |
*/ |
selectCurrentNode: function() { |
- if (!this.node_ || !this.scope_ || !this.treeWalker_) |
+ if (!this.node_.role) |
return; |
if (this.node_ === this.scope_) { |
@@ -124,19 +215,16 @@ AutomationManager.prototype = { |
if (this.scopeStack_.length === 0) |
return; |
- // Find a previous scope that is still valid. |
- let oldScope; |
+ // Find a previous scope that is still valid. The stack here always has |
+ // at least one valid scope (i.e., the desktop node). |
do { |
- oldScope = this.scopeStack_.pop(); |
- } while (oldScope && !oldScope.role); |
- |
- // oldScope will always be valid here, so this will always be true. |
- if (oldScope) { |
- this.scope_ = oldScope; |
- this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); |
- chrome.accessibilityPrivate.setFocusRing( |
- [this.node_.location], AutomationManager.Color.GROUP); |
- } |
+ this.scope_ = this.scopeStack_.pop(); |
+ } while (!this.scope_.role && this.scopeStack_.length > 0); |
+ |
+ this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); |
+ this.updateFocusRing_(); |
+ console.log('Moved to previous scope'); |
+ this.printNode_(this.node_); |
return; |
} |
@@ -144,11 +232,57 @@ AutomationManager.prototype = { |
this.scopeStack_.push(this.scope_); |
this.scope_ = this.node_; |
this.treeWalker_ = this.createTreeWalker_(this.scope_); |
+ console.log('Entered scope'); |
this.moveToNode(true); |
return; |
} |
this.node_.doDefault(); |
+ console.log('Performed default action'); |
+ console.log('\n'); |
+ }, |
+ |
+ /** |
+ * Set the focus ring for the current node and determine the color for it. |
+ * |
+ * @private |
+ */ |
+ updateFocusRing_: function() { |
+ let color; |
+ if (this.node_ === this.scope_) |
+ color = AutomationManager.Color.SCOPE; |
+ else if (AutomationPredicate.isGroup(this.node_, this.scope_)) |
+ color = AutomationManager.Color.GROUP; |
+ else |
+ color = AutomationManager.Color.LEAF; |
+ chrome.accessibilityPrivate.setFocusRing([this.node_.location], color); |
+ }, |
+ |
+ /** |
+ * If this.node_ is invalid, set this.node_ to a valid scope. Will check the |
+ * current scope and past scopes until a valid scope is found. this.node_ |
+ * is set to that valid scope. |
+ * |
+ * @private |
+ */ |
+ startAtValidNode_: function() { |
+ if (this.node_.role) |
+ return; |
+ console.log('Finding new valid node'); |
+ |
+ // Current node is invalid, but current scope is still valid, so set node |
+ // to the current scope. |
+ if (this.scope_.role) |
+ this.node_ = this.scope_; |
+ |
+ // Current node and current scope are invalid, so set both to a valid scope |
+ // from the scope stack. The stack here always has at least one valid scope |
+ // (i.e., the desktop node). |
+ while (!this.node_.role && this.scopeStack_.length > 0) { |
+ this.node_ = this.scopeStack_.pop(); |
+ this.scope_ = this.node_; |
+ } |
+ this.treeWalker_ = this.createTreeWalker_(this.scope_); |
}, |
/** |
@@ -158,6 +292,7 @@ AutomationManager.prototype = { |
* |
* @param {!chrome.automation.AutomationNode} scope |
* @param {!chrome.automation.AutomationNode=} opt_start |
+ * @private |
* @return {!AutomationTreeWalker} |
*/ |
createTreeWalker_: function(scope, opt_start) { |