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 // Rebuild scope stack and set scope for focused node. |
| 102 this.buildScopeStack_(event.target); |
| 103 |
| 104 // Move to focused node. |
| 105 this.node_ = event.target; |
| 106 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); |
| 107 |
| 108 // In case the node that gained focus is not a subtreeLeaf. |
| 109 if (AutomationPredicate.isSubtreeLeaf(this.node_, this.scope_)) { |
| 110 this.printNode_(this.node_); |
| 111 this.updateFocusRing_(); |
| 112 } else |
| 113 this.moveToNode(true); |
| 114 }, |
| 115 |
| 116 /** |
| 117 * Create a new scope stack and set the current scope for |node|. |
| 118 * |
| 119 * @param {!chrome.automation.AutomationNode} node |
| 120 * @private |
| 121 */ |
| 122 buildScopeStack_: function(node) { |
| 123 // Create list of |node|'s ancestors, with highest level ancestor at the |
| 124 // end. |
| 125 let ancestorList = []; |
| 126 while (node.parent) { |
| 127 ancestorList.push(node.parent); |
| 128 node = node.parent; |
| 129 } |
| 130 |
| 131 // Starting with desktop as the scope, if an ancestor is a group, set it to |
| 132 // the new scope and push the old scope onto the scope stack. |
| 133 this.scopeStack_ = []; |
| 134 this.scope_ = this.desktop_; |
| 135 while (ancestorList.length > 0) { |
| 136 let ancestor = ancestorList.pop(); |
| 137 if (ancestor.role === chrome.automation.RoleType.DESKTOP) |
| 138 continue; |
| 139 if (AutomationPredicate.isGroup(ancestor, this.scope_)) { |
| 140 this.scopeStack_.push(this.scope_); |
| 141 this.scope_ = ancestor; |
| 142 } |
| 143 } |
| 144 }, |
| 145 |
| 146 /** |
| 147 * When a node is removed from the page, move to a new valid node. |
| 148 * |
| 149 * @param {!chrome.automation.TreeChange} treeChange |
| 150 * @private |
| 151 */ |
| 152 handleNodeRemoved_: function(treeChange) { |
| 153 // TODO(elichtenberg): Only listen to NODE_REMOVED callbacks. Don't need |
| 154 // any others. |
| 155 if (treeChange.type !== chrome.automation.TreeChangeType.NODE_REMOVED) |
| 156 return; |
| 157 |
| 158 // TODO(elichtenberg): Currently not getting NODE_REMOVED event when whole |
| 159 // tree is deleted. Once fixed, can delete this. Should only need to check |
| 160 // if target is current node. |
| 161 let removedByRWA = |
| 162 treeChange.target.role === chrome.automation.RoleType.ROOT_WEB_AREA |
| 163 && !this.node_.role; |
| 164 |
| 165 if (!removedByRWA && treeChange.target !== this.node_) |
| 166 return; |
| 167 |
| 168 console.log('Node removed'); |
| 169 chrome.accessibilityPrivate.setFocusRing([]); |
| 170 |
| 171 // Current node not invalid until after treeChange callback, so move to |
| 172 // valid node after callback. Delay added to prevent moving to another |
| 173 // node about to be made invalid. If already at a valid node (e.g., user |
| 174 // moves to it or focus changes to it), won't need to move to a new node. |
| 175 window.setTimeout(function() { |
| 176 if (!this.node_.role) |
| 177 this.moveToNode(true); |
| 178 }.bind(this), 100); |
| 179 }, |
| 180 |
| 181 /** |
83 * Set this.node_ to the next/previous interesting node, and then highlight | 182 * 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 | 183 * 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 | 184 * first/last interesting node. If |doNext| is true, will search for next |
86 * node. Otherwise, will search for previous node. | 185 * node. Otherwise, will search for previous node. |
87 * | 186 * |
88 * @param {boolean} doNext | 187 * @param {boolean} doNext |
89 */ | 188 */ |
90 moveToNode: function(doNext) { | 189 moveToNode: function(doNext) { |
91 if (!this.treeWalker_) | 190 // If node is invalid, set node to last valid scope. |
92 return; | 191 this.startAtValidNode_(); |
93 | 192 |
94 let node = this.treeWalker_.moveToNode(doNext); | 193 let node = this.treeWalker_.moveToNode(doNext); |
95 if (node) { | 194 if (node) { |
96 this.node_ = node; | 195 this.node_ = node; |
97 this.printNode_(this.node_); | 196 this.printNode_(this.node_); |
98 | 197 this.updateFocusRing_(); |
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 } | 198 } |
108 }, | 199 }, |
109 | 200 |
110 /** | 201 /** |
111 * Select the currently highlighted node. If the node is the current scope, | 202 * 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 | 203 * 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, | 204 * 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. | 205 * 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 | 206 * Otherwise, meaning the node is interesting, perform the default action on |
116 * it. | 207 * it. |
117 */ | 208 */ |
118 selectCurrentNode: function() { | 209 selectCurrentNode: function() { |
119 if (!this.node_ || !this.scope_ || !this.treeWalker_) | 210 if (!this.node_.role) |
120 return; | 211 return; |
121 | 212 |
122 if (this.node_ === this.scope_) { | 213 if (this.node_ === this.scope_) { |
123 // Don't let user select the top-level root node (i.e., the desktop node). | 214 // Don't let user select the top-level root node (i.e., the desktop node). |
124 if (this.scopeStack_.length === 0) | 215 if (this.scopeStack_.length === 0) |
125 return; | 216 return; |
126 | 217 |
127 // Find a previous scope that is still valid. | 218 // Find a previous scope that is still valid. The stack here always has |
128 let oldScope; | 219 // at least one valid scope (i.e., the desktop node). |
129 do { | 220 do { |
130 oldScope = this.scopeStack_.pop(); | 221 this.scope_ = this.scopeStack_.pop(); |
131 } while (oldScope && !oldScope.role); | 222 } while (!this.scope_.role && this.scopeStack_.length > 0); |
132 | 223 |
133 // oldScope will always be valid here, so this will always be true. | 224 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); |
134 if (oldScope) { | 225 this.updateFocusRing_(); |
135 this.scope_ = oldScope; | 226 console.log('Moved to previous scope'); |
136 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); | 227 this.printNode_(this.node_); |
137 chrome.accessibilityPrivate.setFocusRing( | |
138 [this.node_.location], AutomationManager.Color.GROUP); | |
139 } | |
140 return; | 228 return; |
141 } | 229 } |
142 | 230 |
143 if (AutomationPredicate.isGroup(this.node_, this.scope_)) { | 231 if (AutomationPredicate.isGroup(this.node_, this.scope_)) { |
144 this.scopeStack_.push(this.scope_); | 232 this.scopeStack_.push(this.scope_); |
145 this.scope_ = this.node_; | 233 this.scope_ = this.node_; |
146 this.treeWalker_ = this.createTreeWalker_(this.scope_); | 234 this.treeWalker_ = this.createTreeWalker_(this.scope_); |
| 235 console.log('Entered scope'); |
147 this.moveToNode(true); | 236 this.moveToNode(true); |
148 return; | 237 return; |
149 } | 238 } |
150 | 239 |
151 this.node_.doDefault(); | 240 this.node_.doDefault(); |
| 241 console.log('Performed default action'); |
| 242 console.log('\n'); |
152 }, | 243 }, |
153 | 244 |
154 /** | 245 /** |
| 246 * Set the focus ring for the current node and determine the color for it. |
| 247 * |
| 248 * @private |
| 249 */ |
| 250 updateFocusRing_: function() { |
| 251 let color; |
| 252 if (this.node_ === this.scope_) |
| 253 color = AutomationManager.Color.SCOPE; |
| 254 else if (AutomationPredicate.isGroup(this.node_, this.scope_)) |
| 255 color = AutomationManager.Color.GROUP; |
| 256 else |
| 257 color = AutomationManager.Color.LEAF; |
| 258 chrome.accessibilityPrivate.setFocusRing([this.node_.location], color); |
| 259 }, |
| 260 |
| 261 /** |
| 262 * If this.node_ is invalid, set this.node_ to a valid scope. Will check the |
| 263 * current scope and past scopes until a valid scope is found. this.node_ |
| 264 * is set to that valid scope. |
| 265 * |
| 266 * @private |
| 267 */ |
| 268 startAtValidNode_: function() { |
| 269 if (this.node_.role) |
| 270 return; |
| 271 console.log('Finding new valid node'); |
| 272 |
| 273 // Current node is invalid, but current scope is still valid, so set node |
| 274 // to the current scope. |
| 275 if (this.scope_.role) |
| 276 this.node_ = this.scope_; |
| 277 |
| 278 // Current node and current scope are invalid, so set both to a valid scope |
| 279 // from the scope stack. The stack here always has at least one valid scope |
| 280 // (i.e., the desktop node). |
| 281 while (!this.node_.role && this.scopeStack_.length > 0) { |
| 282 this.node_ = this.scopeStack_.pop(); |
| 283 this.scope_ = this.node_; |
| 284 } |
| 285 this.treeWalker_ = this.createTreeWalker_(this.scope_); |
| 286 }, |
| 287 |
| 288 /** |
155 * Create an AutomationTreeWalker for the subtree with |scope| as its root. | 289 * 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 | 290 * If |opt_start| is defined, the tree walker will start walking the tree |
157 * from |opt_start|; otherwise, it will start from |scope|. | 291 * from |opt_start|; otherwise, it will start from |scope|. |
158 * | 292 * |
159 * @param {!chrome.automation.AutomationNode} scope | 293 * @param {!chrome.automation.AutomationNode} scope |
160 * @param {!chrome.automation.AutomationNode=} opt_start | 294 * @param {!chrome.automation.AutomationNode=} opt_start |
| 295 * @private |
161 * @return {!AutomationTreeWalker} | 296 * @return {!AutomationTreeWalker} |
162 */ | 297 */ |
163 createTreeWalker_: function(scope, opt_start) { | 298 createTreeWalker_: function(scope, opt_start) { |
164 // If no explicit start node, start walking the tree from |scope|. | 299 // If no explicit start node, start walking the tree from |scope|. |
165 let start = opt_start || scope; | 300 let start = opt_start || scope; |
166 | 301 |
167 let leafPred = function(node) { | 302 let leafPred = function(node) { |
168 return (node !== scope && AutomationPredicate.isSubtreeLeaf(node, scope)) | 303 return (node !== scope && AutomationPredicate.isSubtreeLeaf(node, scope)) |
169 || !AutomationPredicate.isInterestingSubtree(node); | 304 || !AutomationPredicate.isInterestingSubtree(node); |
170 }; | 305 }; |
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
251 */ | 386 */ |
252 debugMoveToParent: function() { | 387 debugMoveToParent: function() { |
253 let parent = this.treeWalker_.debugMoveToParent(this.node_); | 388 let parent = this.treeWalker_.debugMoveToParent(this.node_); |
254 if (parent) { | 389 if (parent) { |
255 this.node_ = parent; | 390 this.node_ = parent; |
256 this.printNode_(this.node_); | 391 this.printNode_(this.node_); |
257 chrome.accessibilityPrivate.setFocusRing([this.node_.location]); | 392 chrome.accessibilityPrivate.setFocusRing([this.node_.location]); |
258 } | 393 } |
259 } | 394 } |
260 }; | 395 }; |
OLD | NEW |