| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 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 var AutomationEvent = chrome.automation.AutomationEvent; | 5 var AutomationEvent = chrome.automation.AutomationEvent; |
| 6 var AutomationNode = chrome.automation.AutomationNode; | 6 var AutomationNode = chrome.automation.AutomationNode; |
| 7 var EventType = chrome.automation.EventType; | 7 var EventType = chrome.automation.EventType; |
| 8 var RoleType = chrome.automation.RoleType; | 8 var RoleType = chrome.automation.RoleType; |
| 9 | 9 |
| 10 /** | 10 /** |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 46 } | 46 } |
| 47 | 47 |
| 48 /** | 48 /** |
| 49 * @constructor | 49 * @constructor |
| 50 */ | 50 */ |
| 51 var SelectToSpeak = function() { | 51 var SelectToSpeak = function() { |
| 52 /** @private { AutomationNode } */ | 52 /** @private { AutomationNode } */ |
| 53 this.node_ = null; | 53 this.node_ = null; |
| 54 | 54 |
| 55 /** @private { boolean } */ | 55 /** @private { boolean } */ |
| 56 this.down_ = false; | 56 this.trackingMouse_ = false; |
| 57 |
| 58 /** @private { boolean } */ |
| 59 this.didTrackMouse_ = false; |
| 60 |
| 61 /** @private { boolean } */ |
| 62 this.isSearchKeyDown_ = false; |
| 63 |
| 64 /** @private { !Set<number> } */ |
| 65 this.keysCurrentlyDown_ = new Set(); |
| 66 |
| 67 /** @private { !Set<number> } */ |
| 68 this.keysPressedTogether_ = new Set(); |
| 57 | 69 |
| 58 /** @private {{x: number, y: number}} */ | 70 /** @private {{x: number, y: number}} */ |
| 59 this.mouseStart_ = {x: 0, y: 0}; | 71 this.mouseStart_ = {x: 0, y: 0}; |
| 60 | 72 |
| 73 /** @private {{x: number, y: number}} */ |
| 74 this.mouseEnd_ = {x: 0, y: 0}; |
| 75 |
| 76 /** @private {AutomationRootNode} */ |
| 61 chrome.automation.getDesktop(function(desktop) { | 77 chrome.automation.getDesktop(function(desktop) { |
| 78 this.desktop_ = desktop; |
| 79 |
| 80 // After the user selects a region of the screen, we do a hit test at |
| 81 // the center of that box using the automation API. The result of the |
| 82 // hit test is a MOUSE_RELEASED accessibility event. |
| 62 desktop.addEventListener( | 83 desktop.addEventListener( |
| 63 EventType.MOUSE_PRESSED, this.onMousePressed_.bind(this), true); | 84 EventType.MOUSE_RELEASED, this.onAutomationHitTest_.bind(this), |
| 64 desktop.addEventListener( | 85 true); |
| 65 EventType.MOUSE_DRAGGED, this.onMouseDragged_.bind(this), true); | |
| 66 desktop.addEventListener( | |
| 67 EventType.MOUSE_RELEASED, this.onMouseReleased_.bind(this), true); | |
| 68 desktop.addEventListener( | |
| 69 EventType.MOUSE_CANCELED, this.onMouseCanceled_.bind(this), true); | |
| 70 }.bind(this)); | 86 }.bind(this)); |
| 71 | 87 |
| 72 /** @private { ?string } */ | 88 /** @private { ?string } */ |
| 73 this.voiceNameFromPrefs_ = null; | 89 this.voiceNameFromPrefs_ = null; |
| 74 | 90 |
| 75 /** @private { ?string } */ | 91 /** @private { ?string } */ |
| 76 this.voiceNameFromLocale_ = null; | 92 this.voiceNameFromLocale_ = null; |
| 77 | 93 |
| 78 /** @private { Set<string> } */ | 94 /** @private { Set<string> } */ |
| 79 this.validVoiceNames_ = new Set(); | 95 this.validVoiceNames_ = new Set(); |
| 80 | 96 |
| 81 /** @private { number } */ | 97 /** @private { number } */ |
| 82 this.speechRate_ = 1.0; | 98 this.speechRate_ = 1.0; |
| 83 | 99 |
| 84 /** @const { string } */ | 100 /** @const { string } */ |
| 85 this.color_ = "#f73a98"; | 101 this.color_ = "#f73a98"; |
| 86 | 102 |
| 87 this.initPreferences_(); | 103 this.initPreferences_(); |
| 104 |
| 105 this.setUpEventListeners_(); |
| 88 }; | 106 }; |
| 89 | 107 |
| 108 /** @const {number} */ |
| 109 SelectToSpeak.SEARCH_KEY_CODE = 91; |
| 110 |
| 111 /** @const {number} */ |
| 112 SelectToSpeak.CONTROL_KEY_CODE = 17; |
| 113 |
| 90 SelectToSpeak.prototype = { | 114 SelectToSpeak.prototype = { |
| 91 /** | 115 /** |
| 92 * Called when the mouse is pressed and the user is in a mode where | 116 * Called when the mouse is pressed and the user is in a mode where |
| 93 * select-to-speak is capturing mouse events (for example holding down | 117 * select-to-speak is capturing mouse events (for example holding down |
| 94 * Search). | 118 * Search). |
| 95 * | 119 * |
| 96 * @param {!AutomationEvent} evt | 120 * @param {!Event} evt The DOM event |
| 121 * @return {boolean} True if the default action should be performed; |
| 122 * we always return false because we don't want any other event |
| 123 * handlers to run. |
| 97 */ | 124 */ |
| 98 onMousePressed_: function(evt) { | 125 onMouseDown_: function(evt) { |
| 99 this.down_ = true; | 126 if (!this.isSearchKeyDown_) |
| 100 this.mouseStart_ = {x: evt.mouseX, y: evt.mouseY}; | 127 return false; |
| 101 this.startNode_ = evt.target; | 128 |
| 129 this.trackingMouse_ = true; |
| 130 this.didTrackMouse_ = true; |
| 131 this.mouseStart_ = {x: evt.screenX, y: evt.screenY}; |
| 102 chrome.tts.stop(); | 132 chrome.tts.stop(); |
| 103 this.onMouseDragged_(evt); | 133 |
| 134 // Fire a hit test event on click to warm up the cache. |
| 135 this.desktop_.hitTest(evt.screenX, evt.screenY, EventType.MOUSE_PRESSED); |
| 136 |
| 137 this.onMouseMove_(evt); |
| 138 return false; |
| 104 }, | 139 }, |
| 105 | 140 |
| 106 /** | 141 /** |
| 107 * Called when the mouse is moved or dragged and the user is in a | 142 * Called when the mouse is moved or dragged and the user is in a |
| 108 * mode where select-to-speak is capturing mouse events (for example | 143 * mode where select-to-speak is capturing mouse events (for example |
| 109 * holding down Search). | 144 * holding down Search). |
| 110 * | 145 * |
| 111 * @param {!AutomationEvent} evt | 146 * @param {!Event} evt The DOM event |
| 147 * @return {boolean} True if the default action should be performed. |
| 112 */ | 148 */ |
| 113 onMouseDragged_: function(evt) { | 149 onMouseMove_: function(evt) { |
| 114 if (!this.down_) | 150 if (!this.trackingMouse_) |
| 115 return; | 151 return false; |
| 116 | 152 |
| 117 var rect = rectFromPoints( | 153 var rect = rectFromPoints( |
| 118 this.mouseStart_.x, this.mouseStart_.y, | 154 this.mouseStart_.x, this.mouseStart_.y, |
| 119 evt.mouseX, evt.mouseY); | 155 evt.screenX, evt.screenY); |
| 120 chrome.accessibilityPrivate.setFocusRing([rect], this.color_); | 156 chrome.accessibilityPrivate.setFocusRing([rect], this.color_); |
| 157 return false; |
| 121 }, | 158 }, |
| 122 | 159 |
| 123 /** | 160 /** |
| 124 * Called when the mouse is released and the user is in a | 161 * Called when the mouse is released and the user is in a |
| 125 * mode where select-to-speak is capturing mouse events (for example | 162 * mode where select-to-speak is capturing mouse events (for example |
| 126 * holding down Search). | 163 * holding down Search). |
| 127 * | 164 * |
| 128 * @param {!AutomationEvent} evt | 165 * @param {!Event} evt |
| 166 * @return {boolean} True if the default action should be performed. |
| 129 */ | 167 */ |
| 130 onMouseReleased_: function(evt) { | 168 onMouseUp_: function(evt) { |
| 131 this.onMouseDragged_(evt); | 169 this.onMouseMove_(evt); |
| 132 this.down_ = false; | 170 this.trackingMouse_ = false; |
| 133 | 171 |
| 134 chrome.accessibilityPrivate.setFocusRing([]); | 172 chrome.accessibilityPrivate.setFocusRing([]); |
| 135 | 173 |
| 136 // Walk up to the nearest window, web area, or dialog that the | 174 this.mouseEnd_ = {x: evt.screenX, y: evt.screenY}; |
| 175 var ctrX = Math.floor((this.mouseStart_.x + this.mouseEnd_.x) / 2); |
| 176 var ctrY = Math.floor((this.mouseStart_.y + this.mouseEnd_.y) / 2); |
| 177 |
| 178 // Do a hit test at the center of the area the user dragged over. |
| 179 // This will give us some context when searching the accessibility tree. |
| 180 // The hit test will result in a EventType.MOUSE_RELEASED event being |
| 181 // fired on the result of that hit test, which will trigger |
| 182 // onAutomationHitTest_. |
| 183 this.desktop_.hitTest(ctrX, ctrY, EventType.MOUSE_RELEASED); |
| 184 return false; |
| 185 }, |
| 186 |
| 187 /** |
| 188 * Called in response to our hit test after the mouse is released, |
| 189 * when the user is in a mode where select-to-speak is capturing |
| 190 * mouse events (for example holding down Search). |
| 191 * |
| 192 * @param {!AutomationEvent} evt The automation event. |
| 193 */ |
| 194 onAutomationHitTest_: function(evt) { |
| 195 // Walk up to the nearest window, web area, toolbar, or dialog that the |
| 137 // hit node is contained inside. Only speak objects within that | 196 // hit node is contained inside. Only speak objects within that |
| 138 // container. In the future we might include other container-like | 197 // container. In the future we might include other container-like |
| 139 // roles here. | 198 // roles here. |
| 140 var root = this.startNode_; | 199 var root = evt.target; |
| 141 while (root.parent && | 200 while (root.parent && |
| 142 root.role != RoleType.WINDOW && | 201 root.role != RoleType.WINDOW && |
| 143 root.role != RoleType.ROOT_WEB_AREA && | 202 root.role != RoleType.ROOT_WEB_AREA && |
| 144 root.role != RoleType.DESKTOP && | 203 root.role != RoleType.DESKTOP && |
| 145 root.role != RoleType.DIALOG) { | 204 root.role != RoleType.DIALOG && |
| 205 root.role != RoleType.ALERT_DIALOG && |
| 206 root.role != RoleType.TOOLBAR) { |
| 146 root = root.parent; | 207 root = root.parent; |
| 147 } | 208 } |
| 148 | 209 |
| 149 var rect = rectFromPoints( | 210 var rect = rectFromPoints( |
| 150 this.mouseStart_.x, this.mouseStart_.y, | 211 this.mouseStart_.x, this.mouseStart_.y, |
| 151 evt.mouseX, evt.mouseY); | 212 this.mouseEnd_.x, this.mouseEnd_.y); |
| 152 var nodes = []; | 213 var nodes = []; |
| 153 this.findAllMatching_(root, rect, nodes); | 214 this.findAllMatching_(root, rect, nodes); |
| 154 this.startSpeechQueue_(nodes); | 215 this.startSpeechQueue_(nodes); |
| 155 }, | 216 }, |
| 156 | 217 |
| 157 /** | 218 /** |
| 158 * Called when the user cancels select-to-speak's capturing of mouse | 219 * @param {!Event} evt |
| 159 * events (for example by releasing Search while the mouse is still down). | |
| 160 * | |
| 161 * @param {!AutomationEvent} evt | |
| 162 */ | 220 */ |
| 163 onMouseCanceled_: function(evt) { | 221 onKeyDown_: function(evt) { |
| 164 this.down_ = false; | 222 if (this.keysPressedTogether_.size == 0 && |
| 165 chrome.accessibilityPrivate.setFocusRing([]); | 223 evt.keyCode == SelectToSpeak.SEARCH_KEY_CODE) { |
| 166 chrome.tts.stop(); | 224 this.isSearchKeyDown_ = true; |
| 225 } else if (!this.trackingMouse_) { |
| 226 this.isSearchKeyDown_ = false; |
| 227 } |
| 228 |
| 229 this.keysCurrentlyDown_.add(evt.keyCode); |
| 230 this.keysPressedTogether_.add(evt.keyCode); |
| 167 }, | 231 }, |
| 168 | 232 |
| 169 /** | 233 /** |
| 234 * @param {!Event} evt |
| 235 */ |
| 236 onKeyUp_: function(evt) { |
| 237 if (evt.keyCode == SelectToSpeak.SEARCH_KEY_CODE) { |
| 238 this.isSearchKeyDown_ = false; |
| 239 |
| 240 // If we were in the middle of tracking the mouse, cancel it. |
| 241 if (this.trackingMouse_) { |
| 242 this.trackingMouse_ = false; |
| 243 chrome.accessibilityPrivate.setFocusRing([]); |
| 244 chrome.tts.stop(); |
| 245 } |
| 246 } |
| 247 |
| 248 // Stop speech when the user taps and releases Control or Search |
| 249 // without using the mouse or pressing any other keys along the way. |
| 250 if (!this.didTrackMouse_ && |
| 251 (evt.keyCode == SelectToSpeak.SEARCH_KEY_CODE || |
| 252 evt.keyCode == SelectToSpeak.CONTROL_KEY_CODE) && |
| 253 this.keysPressedTogether_.has(evt.keyCode) && |
| 254 this.keysPressedTogether_.size == 1) { |
| 255 this.trackingMouse_ = false; |
| 256 chrome.accessibilityPrivate.setFocusRing([]); |
| 257 chrome.tts.stop(); |
| 258 } |
| 259 |
| 260 this.keysCurrentlyDown_.delete(evt.keyCode); |
| 261 if (this.keysCurrentlyDown_.size == 0) { |
| 262 this.keysPressedTogether_.clear(); |
| 263 this.didTrackMouse_ = false; |
| 264 } |
| 265 }, |
| 266 |
| 267 /** |
| 268 * Set up event listeners for mouse and keyboard events. These are |
| 269 * forwarded to us from the SelectToSpeakEventHandler so they should |
| 270 * be interpreted as global events on the whole screen, not local to |
| 271 * any particular window. |
| 272 */ |
| 273 setUpEventListeners_: function() { |
| 274 document.addEventListener('keydown', this.onKeyDown_.bind(this)); |
| 275 document.addEventListener('keyup', this.onKeyUp_.bind(this)); |
| 276 document.addEventListener('mousedown', this.onMouseDown_.bind(this)); |
| 277 document.addEventListener('mousemove', this.onMouseMove_.bind(this)); |
| 278 document.addEventListener('mouseup', this.onMouseUp_.bind(this)); |
| 279 }, |
| 280 |
| 281 /** |
| 170 * Finds all nodes within the subtree rooted at |node| that overlap | 282 * Finds all nodes within the subtree rooted at |node| that overlap |
| 171 * a given rectangle. | 283 * a given rectangle. |
| 172 * @param {AutomationNode} node The starting node. | 284 * @param {AutomationNode} node The starting node. |
| 173 * @param {{left: number, top: number, width: number, height: number}} rect | 285 * @param {{left: number, top: number, width: number, height: number}} rect |
| 174 * The bounding box to search. | 286 * The bounding box to search. |
| 175 * @param {Array<AutomationNode>} nodes The matching node array to be | 287 * @param {Array<AutomationNode>} nodes The matching node array to be |
| 176 * populated. | 288 * populated. |
| 177 * @return {boolean} True if any matches are found. | 289 * @return {boolean} True if any matches are found. |
| 178 */ | 290 */ |
| 179 findAllMatching_: function(node, rect, nodes) { | 291 findAllMatching_: function(node, rect, nodes) { |
| (...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 276 this.updateDefaultVoice_(); | 388 this.updateDefaultVoice_(); |
| 277 window.speechSynthesis.onvoiceschanged = (function() { | 389 window.speechSynthesis.onvoiceschanged = (function() { |
| 278 this.updateDefaultVoice_(); | 390 this.updateDefaultVoice_(); |
| 279 }).bind(this); | 391 }).bind(this); |
| 280 }, | 392 }, |
| 281 | 393 |
| 282 /** | 394 /** |
| 283 * Get the list of TTS voices, and set the default voice if not already set. | 395 * Get the list of TTS voices, and set the default voice if not already set. |
| 284 */ | 396 */ |
| 285 updateDefaultVoice_: function() { | 397 updateDefaultVoice_: function() { |
| 286 console.log('updateDefaultVoice_ ' + this.down_); | |
| 287 var uiLocale = chrome.i18n.getMessage('@@ui_locale'); | 398 var uiLocale = chrome.i18n.getMessage('@@ui_locale'); |
| 288 uiLocale = uiLocale.replace('_', '-').toLowerCase(); | 399 uiLocale = uiLocale.replace('_', '-').toLowerCase(); |
| 289 | 400 |
| 290 chrome.tts.getVoices((function(voices) { | 401 chrome.tts.getVoices((function(voices) { |
| 291 console.log('updateDefaultVoice_ voices: ' + voices.length); | 402 console.log('updateDefaultVoice_ voices: ' + voices.length); |
| 292 this.validVoiceNames_ = new Set(); | 403 this.validVoiceNames_ = new Set(); |
| 293 | 404 |
| 294 if (voices.length == 0) | 405 if (voices.length == 0) |
| 295 return; | 406 return; |
| 296 | 407 |
| (...skipping 17 matching lines...) Expand all Loading... |
| 314 this.voiceNameFromLocale_ = voices[0].voiceName; | 425 this.voiceNameFromLocale_ = voices[0].voiceName; |
| 315 | 426 |
| 316 chrome.storage.sync.get(['voice'], (function(prefs) { | 427 chrome.storage.sync.get(['voice'], (function(prefs) { |
| 317 if (!prefs['voice']) { | 428 if (!prefs['voice']) { |
| 318 chrome.storage.sync.set({'voice': voices[0].voiceName}); | 429 chrome.storage.sync.set({'voice': voices[0].voiceName}); |
| 319 } | 430 } |
| 320 }).bind(this)); | 431 }).bind(this)); |
| 321 }).bind(this)); | 432 }).bind(this)); |
| 322 } | 433 } |
| 323 }; | 434 }; |
| OLD | NEW |