| 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 /** |
| 11 * Return the rect that encloses two points. | 11 * Return the rect that encloses two points. |
| 12 * @param {number} x1 The first x coordinate. | 12 * @param {number} x1 The first x coordinate. |
| 13 * @param {number} y1 The first y coordinate. | 13 * @param {number} y1 The first y coordinate. |
| 14 * @param {number} x2 The second x coordinate. | 14 * @param {number} x2 The second x coordinate. |
| 15 * @param {number} y2 The second x coordinate. | 15 * @param {number} y2 The second x coordinate. |
| 16 * @return {{left: number, top: number, width: number, height: number}} | 16 * @return {{left: number, top: number, width: number, height: number}} |
| 17 */ | 17 */ |
| 18 function rectFromPoints(x1, y1, x2, y2) { | 18 function rectFromPoints(x1, y1, x2, y2) { |
| 19 var left = Math.min(x1, x2); | 19 var left = Math.min(x1, x2); |
| 20 var right = Math.max(x1, x2); | 20 var right = Math.max(x1, x2); |
| 21 var top = Math.min(y1, y2); | 21 var top = Math.min(y1, y2); |
| 22 var bottom = Math.max(y1, y2); | 22 var bottom = Math.max(y1, y2); |
| 23 return {left: left, | 23 return {left: left, top: top, width: right - left, height: bottom - top}; |
| 24 top: top, | |
| 25 width: right - left, | |
| 26 height: bottom - top}; | |
| 27 } | 24 } |
| 28 | 25 |
| 29 /** | 26 /** |
| 30 * Returns true if |rect1| and |rect2| overlap. The rects must define | 27 * Returns true if |rect1| and |rect2| overlap. The rects must define |
| 31 * left, top, width, and height. | 28 * left, top, width, and height. |
| 32 * @param {{left: number, top: number, width: number, height: number}} rect1 | 29 * @param {{left: number, top: number, width: number, height: number}} rect1 |
| 33 * @param {{left: number, top: number, width: number, height: number}} rect2 | 30 * @param {{left: number, top: number, width: number, height: number}} rect2 |
| 34 * @return {boolean} True if the rects overlap. | 31 * @return {boolean} True if the rects overlap. |
| 35 */ | 32 */ |
| 36 function overlaps(rect1, rect2) { | 33 function overlaps(rect1, rect2) { |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 74 this.mouseEnd_ = {x: 0, y: 0}; | 71 this.mouseEnd_ = {x: 0, y: 0}; |
| 75 | 72 |
| 76 /** @private {AutomationRootNode} */ | 73 /** @private {AutomationRootNode} */ |
| 77 chrome.automation.getDesktop(function(desktop) { | 74 chrome.automation.getDesktop(function(desktop) { |
| 78 this.desktop_ = desktop; | 75 this.desktop_ = desktop; |
| 79 | 76 |
| 80 // After the user selects a region of the screen, we do a hit test at | 77 // 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 | 78 // the center of that box using the automation API. The result of the |
| 82 // hit test is a MOUSE_RELEASED accessibility event. | 79 // hit test is a MOUSE_RELEASED accessibility event. |
| 83 desktop.addEventListener( | 80 desktop.addEventListener( |
| 84 EventType.MOUSE_RELEASED, this.onAutomationHitTest_.bind(this), | 81 EventType.MOUSE_RELEASED, this.onAutomationHitTest_.bind(this), true); |
| 85 true); | |
| 86 }.bind(this)); | 82 }.bind(this)); |
| 87 | 83 |
| 88 /** @private { ?string } */ | 84 /** @private { ?string } */ |
| 89 this.voiceNameFromPrefs_ = null; | 85 this.voiceNameFromPrefs_ = null; |
| 90 | 86 |
| 91 /** @private { ?string } */ | 87 /** @private { ?string } */ |
| 92 this.voiceNameFromLocale_ = null; | 88 this.voiceNameFromLocale_ = null; |
| 93 | 89 |
| 94 /** @private { Set<string> } */ | 90 /** @private { Set<string> } */ |
| 95 this.validVoiceNames_ = new Set(); | 91 this.validVoiceNames_ = new Set(); |
| 96 | 92 |
| 97 /** @private { number } */ | 93 /** @private { number } */ |
| 98 this.speechRate_ = 1.0; | 94 this.speechRate_ = 1.0; |
| 99 | 95 |
| 100 /** @const { string } */ | 96 /** @const { string } */ |
| 101 this.color_ = "#f73a98"; | 97 this.color_ = '#f73a98'; |
| 102 | 98 |
| 103 this.initPreferences_(); | 99 this.initPreferences_(); |
| 104 | 100 |
| 105 this.setUpEventListeners_(); | 101 this.setUpEventListeners_(); |
| 106 }; | 102 }; |
| 107 | 103 |
| 108 /** @const {number} */ | 104 /** @const {number} */ |
| 109 SelectToSpeak.SEARCH_KEY_CODE = 91; | 105 SelectToSpeak.SEARCH_KEY_CODE = 91; |
| 110 | 106 |
| 111 /** @const {number} */ | 107 /** @const {number} */ |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 144 * holding down Search). | 140 * holding down Search). |
| 145 * | 141 * |
| 146 * @param {!Event} evt The DOM event | 142 * @param {!Event} evt The DOM event |
| 147 * @return {boolean} True if the default action should be performed. | 143 * @return {boolean} True if the default action should be performed. |
| 148 */ | 144 */ |
| 149 onMouseMove_: function(evt) { | 145 onMouseMove_: function(evt) { |
| 150 if (!this.trackingMouse_) | 146 if (!this.trackingMouse_) |
| 151 return false; | 147 return false; |
| 152 | 148 |
| 153 var rect = rectFromPoints( | 149 var rect = rectFromPoints( |
| 154 this.mouseStart_.x, this.mouseStart_.y, | 150 this.mouseStart_.x, this.mouseStart_.y, evt.screenX, evt.screenY); |
| 155 evt.screenX, evt.screenY); | |
| 156 chrome.accessibilityPrivate.setFocusRing([rect], this.color_); | 151 chrome.accessibilityPrivate.setFocusRing([rect], this.color_); |
| 157 return false; | 152 return false; |
| 158 }, | 153 }, |
| 159 | 154 |
| 160 /** | 155 /** |
| 161 * Called when the mouse is released and the user is in a | 156 * Called when the mouse is released and the user is in a |
| 162 * mode where select-to-speak is capturing mouse events (for example | 157 * mode where select-to-speak is capturing mouse events (for example |
| 163 * holding down Search). | 158 * holding down Search). |
| 164 * | 159 * |
| 165 * @param {!Event} evt | 160 * @param {!Event} evt |
| (...skipping 24 matching lines...) Expand all Loading... |
| 190 * mouse events (for example holding down Search). | 185 * mouse events (for example holding down Search). |
| 191 * | 186 * |
| 192 * @param {!AutomationEvent} evt The automation event. | 187 * @param {!AutomationEvent} evt The automation event. |
| 193 */ | 188 */ |
| 194 onAutomationHitTest_: function(evt) { | 189 onAutomationHitTest_: function(evt) { |
| 195 // Walk up to the nearest window, web area, toolbar, or dialog that the | 190 // Walk up to the nearest window, web area, toolbar, or dialog that the |
| 196 // hit node is contained inside. Only speak objects within that | 191 // hit node is contained inside. Only speak objects within that |
| 197 // container. In the future we might include other container-like | 192 // container. In the future we might include other container-like |
| 198 // roles here. | 193 // roles here. |
| 199 var root = evt.target; | 194 var root = evt.target; |
| 200 while (root.parent && | 195 while (root.parent && root.role != RoleType.WINDOW && |
| 201 root.role != RoleType.WINDOW && | 196 root.role != RoleType.ROOT_WEB_AREA && |
| 202 root.role != RoleType.ROOT_WEB_AREA && | 197 root.role != RoleType.DESKTOP && root.role != RoleType.DIALOG && |
| 203 root.role != RoleType.DESKTOP && | 198 root.role != RoleType.ALERT_DIALOG && |
| 204 root.role != RoleType.DIALOG && | 199 root.role != RoleType.TOOLBAR) { |
| 205 root.role != RoleType.ALERT_DIALOG && | |
| 206 root.role != RoleType.TOOLBAR) { | |
| 207 root = root.parent; | 200 root = root.parent; |
| 208 } | 201 } |
| 209 | 202 |
| 210 var rect = rectFromPoints( | 203 var rect = rectFromPoints( |
| 211 this.mouseStart_.x, this.mouseStart_.y, | 204 this.mouseStart_.x, this.mouseStart_.y, this.mouseEnd_.x, |
| 212 this.mouseEnd_.x, this.mouseEnd_.y); | 205 this.mouseEnd_.y); |
| 213 var nodes = []; | 206 var nodes = []; |
| 214 this.findAllMatching_(root, rect, nodes); | 207 this.findAllMatching_(root, rect, nodes); |
| 215 this.startSpeechQueue_(nodes); | 208 this.startSpeechQueue_(nodes); |
| 216 }, | 209 }, |
| 217 | 210 |
| 218 /** | 211 /** |
| 219 * @param {!Event} evt | 212 * @param {!Event} evt |
| 220 */ | 213 */ |
| 221 onKeyDown_: function(evt) { | 214 onKeyDown_: function(evt) { |
| 222 if (this.keysPressedTogether_.size == 0 && | 215 if (this.keysPressedTogether_.size == 0 && |
| (...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 315 */ | 308 */ |
| 316 startSpeechQueue_: function(nodes) { | 309 startSpeechQueue_: function(nodes) { |
| 317 chrome.tts.stop(); | 310 chrome.tts.stop(); |
| 318 for (var i = 0; i < nodes.length; i++) { | 311 for (var i = 0; i < nodes.length; i++) { |
| 319 var node = nodes[i]; | 312 var node = nodes[i]; |
| 320 var isLast = (i == nodes.length - 1); | 313 var isLast = (i == nodes.length - 1); |
| 321 | 314 |
| 322 var options = { | 315 var options = { |
| 323 rate: this.rate_, | 316 rate: this.rate_, |
| 324 'enqueue': true, | 317 'enqueue': true, |
| 325 onEvent: (function(node, isLast, event) { | 318 onEvent: |
| 326 if (event.type == 'start') { | 319 (function(node, isLast, event) { |
| 327 chrome.accessibilityPrivate.setFocusRing( | 320 if (event.type == 'start') { |
| 328 [node.location], this.color_); | 321 chrome.accessibilityPrivate.setFocusRing( |
| 329 } else if (event.type == 'interrupted' || | 322 [node.location], this.color_); |
| 330 event.type == 'cancelled') { | 323 } else if ( |
| 331 chrome.accessibilityPrivate.setFocusRing([]); | 324 event.type == 'interrupted' || event.type == 'cancelled') { |
| 332 } else if (event.type == 'end') { | 325 chrome.accessibilityPrivate.setFocusRing([]); |
| 333 if (isLast) { | 326 } else if (event.type == 'end') { |
| 334 chrome.accessibilityPrivate.setFocusRing([]); | 327 if (isLast) { |
| 335 } | 328 chrome.accessibilityPrivate.setFocusRing([]); |
| 336 } | 329 } |
| 337 }).bind(this, node, isLast) | 330 } |
| 331 }).bind(this, node, isLast) |
| 338 }; | 332 }; |
| 339 | 333 |
| 340 // Pick the voice name from prefs first, or the one that matches | 334 // Pick the voice name from prefs first, or the one that matches |
| 341 // the locale next, but don't pick a voice that isn't currently | 335 // the locale next, but don't pick a voice that isn't currently |
| 342 // loaded. If no voices are found, leave the voiceName option | 336 // loaded. If no voices are found, leave the voiceName option |
| 343 // unset to let the browser try to route the speech request | 337 // unset to let the browser try to route the speech request |
| 344 // anyway if possible. | 338 // anyway if possible. |
| 345 console.log('Pref: ' + this.voiceNameFromPrefs_); | 339 console.log('Pref: ' + this.voiceNameFromPrefs_); |
| 346 console.log('Locale: ' + this.voiceNameFromLocale_); | 340 console.log('Locale: ' + this.voiceNameFromLocale_); |
| 347 var valid = ''; | 341 var valid = ''; |
| 348 this.validVoiceNames_.forEach(function(voiceName) { | 342 this.validVoiceNames_.forEach(function(voiceName) { |
| 349 if (valid) | 343 if (valid) |
| 350 valid += ','; | 344 valid += ','; |
| 351 valid += voiceName; | 345 valid += voiceName; |
| 352 }); | 346 }); |
| 353 console.log('Valid: ' + valid); | 347 console.log('Valid: ' + valid); |
| 354 if (this.voiceNameFromPrefs_ && | 348 if (this.voiceNameFromPrefs_ && |
| 355 this.validVoiceNames_.has(this.voiceNameFromPrefs_)) { | 349 this.validVoiceNames_.has(this.voiceNameFromPrefs_)) { |
| 356 options['voiceName'] = this.voiceNameFromPrefs_; | 350 options['voiceName'] = this.voiceNameFromPrefs_; |
| 357 } else if (this.voiceNameFromLocale_ && | 351 } else if ( |
| 352 this.voiceNameFromLocale_ && |
| 358 this.validVoiceNames_.has(this.voiceNameFromLocale_)) { | 353 this.validVoiceNames_.has(this.voiceNameFromLocale_)) { |
| 359 options['voiceName'] = this.voiceNameFromLocale_; | 354 options['voiceName'] = this.voiceNameFromLocale_; |
| 360 } | 355 } |
| 361 | 356 |
| 362 chrome.tts.speak(node.name || '', options); | 357 chrome.tts.speak(node.name || '', options); |
| 363 } | 358 } |
| 364 }, | 359 }, |
| 365 | 360 |
| 366 /** | 361 /** |
| 367 * Loads preferences from chrome.storage, sets default values if | 362 * Loads preferences from chrome.storage, sets default values if |
| 368 * necessary, and registers a listener to update prefs when they | 363 * necessary, and registers a listener to update prefs when they |
| 369 * change. | 364 * change. |
| 370 */ | 365 */ |
| 371 initPreferences_: function() { | 366 initPreferences_: function() { |
| 372 var updatePrefs = (function() { | 367 var updatePrefs = (function() { |
| 373 chrome.storage.sync.get(['voice', 'rate'], (function(prefs) { | 368 chrome.storage.sync.get( |
| 374 if (prefs['voice']) { | 369 ['voice', 'rate'], |
| 375 this.voiceNameFromPrefs_ = prefs['voice']; | 370 (function(prefs) { |
| 376 } | 371 if (prefs['voice']) { |
| 377 if (prefs['rate']) { | 372 this.voiceNameFromPrefs_ = prefs['voice']; |
| 378 this.rate_ = parseFloat(prefs['rate']); | 373 } |
| 379 } else { | 374 if (prefs['rate']) { |
| 380 chrome.storage.sync.set({'rate': this.rate_}); | 375 this.rate_ = parseFloat(prefs['rate']); |
| 381 } | 376 } else { |
| 382 }).bind(this)); | 377 chrome.storage.sync.set({'rate': this.rate_}); |
| 383 }).bind(this); | 378 } |
| 379 }).bind(this)); |
| 380 }).bind(this); |
| 384 | 381 |
| 385 updatePrefs(); | 382 updatePrefs(); |
| 386 chrome.storage.onChanged.addListener(updatePrefs); | 383 chrome.storage.onChanged.addListener(updatePrefs); |
| 387 | 384 |
| 388 this.updateDefaultVoice_(); | 385 this.updateDefaultVoice_(); |
| 389 window.speechSynthesis.onvoiceschanged = (function() { | 386 window.speechSynthesis.onvoiceschanged = (function() { |
| 390 this.updateDefaultVoice_(); | 387 this.updateDefaultVoice_(); |
| 391 }).bind(this); | 388 }).bind(this); |
| 392 }, | 389 }, |
| 393 | 390 |
| 394 /** | 391 /** |
| 395 * Get the list of TTS voices, and set the default voice if not already set. | 392 * Get the list of TTS voices, and set the default voice if not already set. |
| 396 */ | 393 */ |
| 397 updateDefaultVoice_: function() { | 394 updateDefaultVoice_: function() { |
| 398 var uiLocale = chrome.i18n.getMessage('@@ui_locale'); | 395 var uiLocale = chrome.i18n.getMessage('@@ui_locale'); |
| 399 uiLocale = uiLocale.replace('_', '-').toLowerCase(); | 396 uiLocale = uiLocale.replace('_', '-').toLowerCase(); |
| 400 | 397 |
| 401 chrome.tts.getVoices((function(voices) { | 398 chrome.tts.getVoices( |
| 402 console.log('updateDefaultVoice_ voices: ' + voices.length); | 399 (function(voices) { |
| 403 this.validVoiceNames_ = new Set(); | 400 console.log('updateDefaultVoice_ voices: ' + voices.length); |
| 401 this.validVoiceNames_ = new Set(); |
| 404 | 402 |
| 405 if (voices.length == 0) | 403 if (voices.length == 0) |
| 406 return; | 404 return; |
| 407 | 405 |
| 408 voices.forEach((function(voice) { | 406 voices.forEach((function(voice) { |
| 409 this.validVoiceNames_.add(voice.voiceName); | 407 this.validVoiceNames_.add(voice.voiceName); |
| 410 }).bind(this)); | 408 }).bind(this)); |
| 411 | 409 |
| 412 voices.sort(function(a, b) { | 410 voices.sort(function(a, b) { |
| 413 function score(voice) { | 411 function score(voice) { |
| 414 var lang = voice.lang.toLowerCase(); | 412 var lang = voice.lang.toLowerCase(); |
| 415 var s = 0; | 413 var s = 0; |
| 416 if (lang == uiLocale) | 414 if (lang == uiLocale) |
| 417 s += 2; | 415 s += 2; |
| 418 if (lang.substr(0, 2) == uiLocale.substr(0, 2)) | 416 if (lang.substr(0, 2) == uiLocale.substr(0, 2)) |
| 419 s += 1; | 417 s += 1; |
| 420 return s; | 418 return s; |
| 421 } | 419 } |
| 422 return score(b) - score(a); | 420 return score(b) - score(a); |
| 423 }); | 421 }); |
| 424 | 422 |
| 425 this.voiceNameFromLocale_ = voices[0].voiceName; | 423 this.voiceNameFromLocale_ = voices[0].voiceName; |
| 426 | 424 |
| 427 chrome.storage.sync.get(['voice'], (function(prefs) { | 425 chrome.storage.sync.get( |
| 428 if (!prefs['voice']) { | 426 ['voice'], |
| 429 chrome.storage.sync.set({'voice': voices[0].voiceName}); | 427 (function(prefs) { |
| 430 } | 428 if (!prefs['voice']) { |
| 431 }).bind(this)); | 429 chrome.storage.sync.set({'voice': voices[0].voiceName}); |
| 432 }).bind(this)); | 430 } |
| 431 }).bind(this)); |
| 432 }).bind(this)); |
| 433 } | 433 } |
| 434 }; | 434 }; |
| OLD | NEW |