Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. | 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * Class to manage interactions with the accessibility tree, including moving | 6 * Class to manage interactions with the accessibility tree, including moving |
| 7 * to and selecting nodes. | 7 * to and selecting nodes. |
| 8 * | 8 * |
| 9 * @constructor | 9 * @constructor |
| 10 * @param {!chrome.automation.AutomationNode} desktop | |
| 10 */ | 11 */ |
| 11 function AutomationManager() { | 12 function AutomationManager(desktop) { |
| 12 /** | 13 /** |
| 13 * Currently highlighted node. | 14 * Currently highlighted node. |
| 14 * | 15 * |
| 15 * @private {chrome.automation.AutomationNode} | 16 * @private {!chrome.automation.AutomationNode} |
| 16 */ | 17 */ |
| 17 this.node_ = null; | 18 this.node_ = desktop; |
| 18 | 19 |
| 19 /** | 20 /** |
| 20 * The root of the subtree that the user is navigating through. | 21 * The root of the subtree that the user is navigating through. |
| 21 * | 22 * |
| 22 * @private {chrome.automation.AutomationNode} | 23 * @private {!chrome.automation.AutomationNode} |
| 23 */ | 24 */ |
| 24 this.scope_ = null; | 25 this.scope_ = desktop; |
| 25 | 26 |
| 26 /** | 27 /** |
| 27 * The desktop node. | 28 * The desktop node. |
| 28 * | 29 * |
| 29 * @private {chrome.automation.AutomationNode} | 30 * @private {!chrome.automation.AutomationNode} |
| 30 */ | 31 */ |
| 31 this.desktop_ = null; | 32 this.desktop_ = desktop; |
| 32 | 33 |
| 33 /** | 34 /** |
| 34 * A stack of past scopes. Allows user to traverse back to previous groups | 35 * A stack of past scopes. Allows user to traverse back to previous groups |
| 35 * after selecting one or more groups. The most recent group is at the end | 36 * after selecting one or more groups. The most recent group is at the end |
| 36 * of the array. | 37 * of the array. |
| 37 * | 38 * |
| 38 * @private {Array<chrome.automation.AutomationNode>} | 39 * @private {Array<!chrome.automation.AutomationNode>} |
| 39 */ | 40 */ |
| 40 this.scopeStack_ = []; | 41 this.scopeStack_ = []; |
| 41 | 42 |
| 42 /** | 43 /** |
| 43 * Moves to the appropriate node in the accessibility tree. | 44 * Moves to the appropriate node in the accessibility tree. |
| 44 * | 45 * |
| 45 * @private {AutomationTreeWalker} | 46 * @private {!AutomationTreeWalker} |
| 46 */ | 47 */ |
| 47 this.treeWalker_ = null; | 48 this.treeWalker_ = this.createTreeWalker_(desktop); |
| 48 | 49 |
| 49 this.init_(); | 50 this.init_(); |
| 50 }; | 51 }; |
| 51 | 52 |
| 52 /** | 53 /** |
| 53 * Highlight colors for the focus ring to distinguish between different types | 54 * Highlight colors for the focus ring to distinguish between different types |
| 54 * of nodes. | 55 * of nodes. |
| 55 * | 56 * |
| 56 * @const | 57 * @const |
| 57 */ | 58 */ |
| 58 AutomationManager.Color = { | 59 AutomationManager.Color = { |
| 59 SCOPE: '#de742f', // dark orange | 60 SCOPE: '#de742f', // dark orange |
| 60 GROUP: '#ffbb33', // light orange | 61 GROUP: '#ffbb33', // light orange |
| 61 LEAF: '#78e428' //light green | 62 LEAF: '#78e428' //light green |
| 62 }; | 63 }; |
| 63 | 64 |
| 64 AutomationManager.prototype = { | 65 AutomationManager.prototype = { |
| 65 /** | 66 /** |
| 66 * Set this.node_, this.root_, and this.desktop_ to the desktop node, and | 67 * Set this.node_, this.root_, and this.desktop_ to the desktop node, and |
| 67 * creates an initial tree walker. | 68 * creates an initial tree walker. |
| 68 * | 69 * |
| 69 * @private | 70 * @private |
| 70 */ | 71 */ |
| 71 init_: function() { | 72 init_: function() { |
| 72 chrome.automation.getDesktop(function(desktop) { | 73 console.log('AutomationNode for desktop is loaded'); |
| 73 this.node_ = desktop; | 74 this.printNode_(this.node_); |
| 74 this.scope_ = desktop; | 75 |
| 75 this.desktop_ = desktop; | 76 this.desktop_.addEventListener( |
| 76 this.treeWalker_ = this.createTreeWalker_(desktop); | 77 chrome.automation.EventType.FOCUS, |
| 77 console.log('AutomationNode for desktop is loaded'); | 78 this.handleFocusChange_.bind(this), |
| 78 this.printNode_(this.node_); | 79 false); |
| 79 }.bind(this)); | 80 |
| 81 // TODO(elichtenberg): Eventually use a more specific filter than | |
| 82 // ALL_TREE_CHANGES. | |
| 83 chrome.automation.addTreeChangeObserver( | |
| 84 chrome.automation.TreeChangeObserverFilter.ALL_TREE_CHANGES, | |
| 85 this.handleNodeRemoved_.bind(this)); | |
| 80 }, | 86 }, |
| 81 | 87 |
| 82 /** | 88 /** |
| 89 * When an interesting element gains focus on the page, move to it. If an | |
| 90 * element gains focus but is not interesting, move to the next interesting | |
| 91 * node after it. | |
| 92 * | |
| 93 * @param {!chrome.automation.AutomationEvent} event | |
| 94 * @private | |
| 95 */ | |
| 96 handleFocusChange_: function(event) { | |
| 97 if (this.node_ === event.target) | |
| 98 return; | |
| 99 console.log('Focus changed'); | |
| 100 | |
| 101 // Create list of ancestors of node that encountered focus change. | |
| 102 let node = event.target; | |
| 103 let ancestorList = []; | |
| 104 while (node.parent) { | |
| 105 ancestorList.push(node.parent); | |
| 106 node = node.parent; | |
| 107 } | |
| 108 | |
| 109 // Rebuild scope stack. | |
|
dmazzoni
2017/05/18 16:25:52
Might be simpler to just do this in one loop rathe
elichtenberg
2017/05/18 18:31:01
Need separate loops because need the node's scope
| |
| 110 this.scopeStack_ = []; | |
| 111 this.scope_ = this.desktop_; | |
| 112 while (ancestorList.length > 0) { | |
| 113 let newScope = ancestorList.pop(); | |
| 114 if (newScope.role === chrome.automation.RoleType.DESKTOP) | |
| 115 continue; | |
| 116 if (AutomationPredicate.isGroup(newScope, this.scope_)) { | |
| 117 this.scopeStack_.push(this.scope_); | |
| 118 this.scope_ = newScope; | |
| 119 } | |
| 120 } | |
| 121 | |
| 122 // Move to focused node. | |
| 123 this.node_ = event.target; | |
| 124 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); | |
| 125 | |
| 126 // In case the node that gained focus is not a subtreeLeaf. | |
| 127 if (AutomationPredicate.isSubtreeLeaf(this.node_, this.scope_)) { | |
| 128 this.printNode_(this.node_); | |
| 129 this.setFocusRing_(); | |
| 130 } else | |
| 131 this.moveToNode(true); | |
| 132 }, | |
| 133 | |
| 134 /** | |
| 135 * When a node is removed from the page, move to a new valid node. | |
| 136 * | |
| 137 * @param {!chrome.automation.TreeChange} treeChange | |
| 138 * @private | |
| 139 */ | |
| 140 handleNodeRemoved_: function(treeChange) { | |
| 141 if (treeChange.type === chrome.automation.TreeChangeType.NODE_REMOVED | |
|
dmazzoni
2017/05/18 16:25:52
Please split this up into a few separate checks, a
elichtenberg
2017/05/18 18:31:01
Split it up and added some TODOs. Do you think thi
| |
| 142 && ((treeChange.target.role === 'rootWebArea' && !this.node_.role) | |
|
dmazzoni
2017/05/18 16:25:52
Use RoleType.ROOT_WEB_AREA here and elsewhere
elichtenberg
2017/05/18 18:31:01
Done.
| |
| 143 || treeChange.target === this.node_)) { | |
| 144 console.log('Node removed'); | |
| 145 chrome.accessibilityPrivate.setFocusRing([]); | |
| 146 | |
| 147 // Current node not invalid until after treeChange callback, so move to | |
| 148 // valid node after callback. Delay added to prevent moving to another | |
| 149 // node about to be made invalid. If already at a valid node (e.g., user | |
| 150 // moves to it or focus changes to it), won't need to move to a new node. | |
| 151 window.setTimeout(function() { | |
| 152 if (!this.node_.role) | |
| 153 this.moveToNode(true); | |
| 154 }.bind(this), 100); | |
| 155 } | |
| 156 }, | |
| 157 | |
| 158 /** | |
| 83 * Set this.node_ to the next/previous interesting node, and then highlight | 159 * Set this.node_ to the next/previous interesting node, and then highlight |
| 84 * it on the screen. If no interesting node is found, set this.node_ to the | 160 * it on the screen. If no interesting node is found, set this.node_ to the |
| 85 * first/last interesting node. If |doNext| is true, will search for next | 161 * first/last interesting node. If |doNext| is true, will search for next |
| 86 * node. Otherwise, will search for previous node. | 162 * node. Otherwise, will search for previous node. |
| 87 * | 163 * |
| 88 * @param {boolean} doNext | 164 * @param {boolean} doNext |
| 89 */ | 165 */ |
| 90 moveToNode: function(doNext) { | 166 moveToNode: function(doNext) { |
| 91 if (!this.treeWalker_) | 167 // If node is invalid, set node to last valid scope. |
| 92 return; | 168 this.startAtValidNode_(); |
| 93 | 169 |
| 94 let node = this.treeWalker_.moveToNode(doNext); | 170 let node = this.treeWalker_.moveToNode(doNext); |
| 95 if (node) { | 171 if (node) { |
| 96 this.node_ = node; | 172 this.node_ = node; |
| 97 this.printNode_(this.node_); | 173 this.printNode_(this.node_); |
| 98 | 174 this.setFocusRing_(); |
| 99 let color; | |
| 100 if (this.node_ === this.scope_) | |
| 101 color = AutomationManager.Color.SCOPE; | |
| 102 else if (AutomationPredicate.isInteresting(this.node_)) | |
| 103 color = AutomationManager.Color.LEAF; | |
| 104 else | |
| 105 color = AutomationManager.Color.GROUP; | |
| 106 chrome.accessibilityPrivate.setFocusRing([this.node_.location], color); | |
| 107 } | 175 } |
| 108 }, | 176 }, |
| 109 | 177 |
| 110 /** | 178 /** |
| 111 * Select the currently highlighted node. If the node is the current scope, | 179 * Select the currently highlighted node. If the node is the current scope, |
| 112 * go back to the previous scope (i.e., create a new tree walker rooted at | 180 * go back to the previous scope (i.e., create a new tree walker rooted at |
| 113 * the previous scope). If the node is a group other than the current scope, | 181 * the previous scope). If the node is a group other than the current scope, |
| 114 * create a new tree walker for the new subtree the user is scanning through. | 182 * create a new tree walker for the new subtree the user is scanning through. |
| 115 * Otherwise, meaning the node is interesting, perform the default action on | 183 * Otherwise, meaning the node is interesting, perform the default action on |
| 116 * it. | 184 * it. |
| 117 */ | 185 */ |
| 118 selectCurrentNode: function() { | 186 selectCurrentNode: function() { |
| 119 if (!this.node_ || !this.scope_ || !this.treeWalker_) | 187 if (!this.node_.role) |
| 120 return; | 188 return; |
| 121 | 189 |
| 122 if (this.node_ === this.scope_) { | 190 if (this.node_ === this.scope_) { |
| 123 // Don't let user select the top-level root node (i.e., the desktop node). | 191 // Don't let user select the top-level root node (i.e., the desktop node). |
| 124 if (this.scopeStack_.length === 0) | 192 if (this.scopeStack_.length === 0) |
| 125 return; | 193 return; |
| 126 | 194 |
| 127 // Find a previous scope that is still valid. | 195 // Find a previous scope that is still valid. The stack here always has |
| 128 let oldScope; | 196 // at least one valid scope (i.e., the desktop node). |
| 129 do { | 197 do { |
| 130 oldScope = this.scopeStack_.pop(); | 198 this.scope_ = this.scopeStack_.pop(); |
| 131 } while (oldScope && !oldScope.role); | 199 } while (!this.scope_.role && this.scopeStack_.length > 0); |
| 132 | 200 |
| 133 // oldScope will always be valid here, so this will always be true. | 201 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); |
| 134 if (oldScope) { | 202 chrome.accessibilityPrivate.setFocusRing( |
|
dmazzoni
2017/05/18 16:25:52
Can you call this.setFocusRing_() here?
elichtenberg
2017/05/18 18:31:01
Yep. Just changed it.
| |
| 135 this.scope_ = oldScope; | 203 [this.node_.location], AutomationManager.Color.GROUP); |
| 136 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); | 204 console.log('Moved to previous scope'); |
| 137 chrome.accessibilityPrivate.setFocusRing( | 205 this.printNode_(this.node_); |
| 138 [this.node_.location], AutomationManager.Color.GROUP); | |
| 139 } | |
| 140 return; | 206 return; |
| 141 } | 207 } |
| 142 | 208 |
| 143 if (AutomationPredicate.isGroup(this.node_, this.scope_)) { | 209 if (AutomationPredicate.isGroup(this.node_, this.scope_)) { |
| 144 this.scopeStack_.push(this.scope_); | 210 this.scopeStack_.push(this.scope_); |
| 145 this.scope_ = this.node_; | 211 this.scope_ = this.node_; |
| 146 this.treeWalker_ = this.createTreeWalker_(this.scope_); | 212 this.treeWalker_ = this.createTreeWalker_(this.scope_); |
| 213 console.log('Entered scope'); | |
| 147 this.moveToNode(true); | 214 this.moveToNode(true); |
| 148 return; | 215 return; |
| 149 } | 216 } |
| 150 | 217 |
| 151 this.node_.doDefault(); | 218 this.node_.doDefault(); |
| 219 console.log('Performed default action'); | |
| 220 console.log('\n'); | |
| 152 }, | 221 }, |
| 153 | 222 |
| 154 /** | 223 /** |
| 224 * Set the focus ring for the current node and determine the color for it. | |
| 225 * | |
| 226 * @private | |
| 227 */ | |
| 228 setFocusRing_: function() { | |
|
dmazzoni
2017/05/18 16:25:52
How about updateFocusRing(), since it sets the foc
elichtenberg
2017/05/18 18:31:01
Done.
| |
| 229 let color; | |
| 230 if (this.node_ === this.scope_) | |
| 231 color = AutomationManager.Color.SCOPE; | |
| 232 else if (AutomationPredicate.isGroup(this.node_, this.scope_)) | |
| 233 color = AutomationManager.Color.GROUP; | |
| 234 else | |
| 235 color = AutomationManager.Color.LEAF; | |
| 236 chrome.accessibilityPrivate.setFocusRing([this.node_.location], color); | |
| 237 }, | |
| 238 | |
| 239 /** | |
| 240 * If this.node_ is invalid, set this.node_ to a valid scope. Will check the | |
| 241 * current scope and past scopes until a valid scope is found. this.node_ | |
| 242 * is set to that valid scope. | |
| 243 * | |
| 244 * @private | |
| 245 */ | |
| 246 startAtValidNode_: function() { | |
| 247 if (this.node_.role) | |
| 248 return; | |
| 249 console.log('Finding new valid node'); | |
| 250 | |
| 251 // Current node is invalid, but current scope is still valid, so set node | |
| 252 // to the current scope. | |
| 253 if (this.scope_.role) | |
| 254 this.node_ = this.scope_; | |
| 255 | |
| 256 // Current node and current scope are invalid, so set both to a valid scope | |
| 257 // from the scope stack. The stack here always has at least one valid scope | |
| 258 // (i.e., the desktop node). | |
| 259 while (!this.node_.role && this.scopeStack_.length > 0) { | |
| 260 this.node_ = this.scopeStack_.pop(); | |
| 261 this.scope_ = this.node_; | |
| 262 } | |
| 263 this.treeWalker_ = this.createTreeWalker_(this.scope_); | |
| 264 }, | |
| 265 | |
| 266 /** | |
| 155 * Create an AutomationTreeWalker for the subtree with |scope| as its root. | 267 * Create an AutomationTreeWalker for the subtree with |scope| as its root. |
| 156 * If |opt_start| is defined, the tree walker will start walking the tree | 268 * If |opt_start| is defined, the tree walker will start walking the tree |
| 157 * from |opt_start|; otherwise, it will start from |scope|. | 269 * from |opt_start|; otherwise, it will start from |scope|. |
| 158 * | 270 * |
| 159 * @param {!chrome.automation.AutomationNode} scope | 271 * @param {!chrome.automation.AutomationNode} scope |
| 160 * @param {!chrome.automation.AutomationNode=} opt_start | 272 * @param {!chrome.automation.AutomationNode=} opt_start |
| 273 * @private | |
| 161 * @return {!AutomationTreeWalker} | 274 * @return {!AutomationTreeWalker} |
| 162 */ | 275 */ |
| 163 createTreeWalker_: function(scope, opt_start) { | 276 createTreeWalker_: function(scope, opt_start) { |
| 164 // If no explicit start node, start walking the tree from |scope|. | 277 // If no explicit start node, start walking the tree from |scope|. |
| 165 let start = opt_start || scope; | 278 let start = opt_start || scope; |
| 166 | 279 |
| 167 let leafPred = function(node) { | 280 let leafPred = function(node) { |
| 168 return (node !== scope && AutomationPredicate.isSubtreeLeaf(node, scope)) | 281 return (node !== scope && AutomationPredicate.isSubtreeLeaf(node, scope)) |
| 169 || !AutomationPredicate.isInterestingSubtree(node); | 282 || !AutomationPredicate.isInterestingSubtree(node); |
| 170 }; | 283 }; |
| (...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 251 */ | 364 */ |
| 252 debugMoveToParent: function() { | 365 debugMoveToParent: function() { |
| 253 let parent = this.treeWalker_.debugMoveToParent(this.node_); | 366 let parent = this.treeWalker_.debugMoveToParent(this.node_); |
| 254 if (parent) { | 367 if (parent) { |
| 255 this.node_ = parent; | 368 this.node_ = parent; |
| 256 this.printNode_(this.node_); | 369 this.printNode_(this.node_); |
| 257 chrome.accessibilityPrivate.setFocusRing([this.node_.location]); | 370 chrome.accessibilityPrivate.setFocusRing([this.node_.location]); |
| 258 } | 371 } |
| 259 } | 372 } |
| 260 }; | 373 }; |
| OLD | NEW |