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.trackingMouse_ = false; | 56 this.down_ = 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(); | |
69 | 57 |
70 /** @private {{x: number, y: number}} */ | 58 /** @private {{x: number, y: number}} */ |
71 this.mouseStart_ = {x: 0, y: 0}; | 59 this.mouseStart_ = {x: 0, y: 0}; |
72 | 60 |
73 /** @private {{x: number, y: number}} */ | |
74 this.mouseEnd_ = {x: 0, y: 0}; | |
75 | |
76 /** @private {AutomationRootNode} */ | |
77 chrome.automation.getDesktop(function(desktop) { | 61 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. | |
83 desktop.addEventListener( | 62 desktop.addEventListener( |
84 EventType.MOUSE_RELEASED, this.onAutomationHitTest_.bind(this), | 63 EventType.MOUSE_PRESSED, this.onMousePressed_.bind(this), true); |
85 true); | 64 desktop.addEventListener( |
| 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); |
86 }.bind(this)); | 70 }.bind(this)); |
87 | 71 |
88 /** @private { ?string } */ | 72 /** @private { ?string } */ |
89 this.voiceNameFromPrefs_ = null; | 73 this.voiceNameFromPrefs_ = null; |
90 | 74 |
91 /** @private { ?string } */ | 75 /** @private { ?string } */ |
92 this.voiceNameFromLocale_ = null; | 76 this.voiceNameFromLocale_ = null; |
93 | 77 |
94 /** @private { Set<string> } */ | 78 /** @private { Set<string> } */ |
95 this.validVoiceNames_ = new Set(); | 79 this.validVoiceNames_ = new Set(); |
96 | 80 |
97 /** @private { number } */ | 81 /** @private { number } */ |
98 this.speechRate_ = 1.0; | 82 this.speechRate_ = 1.0; |
99 | 83 |
100 /** @const { string } */ | 84 /** @const { string } */ |
101 this.color_ = "#f73a98"; | 85 this.color_ = "#f73a98"; |
102 | 86 |
103 this.initPreferences_(); | 87 this.initPreferences_(); |
104 | |
105 this.setUpEventListeners_(); | |
106 }; | 88 }; |
107 | 89 |
108 /** @const {number} */ | |
109 SelectToSpeak.SEARCH_KEY_CODE = 91; | |
110 | |
111 /** @const {number} */ | |
112 SelectToSpeak.CONTROL_KEY_CODE = 17; | |
113 | |
114 SelectToSpeak.prototype = { | 90 SelectToSpeak.prototype = { |
115 /** | 91 /** |
116 * Called when the mouse is pressed and the user is in a mode where | 92 * Called when the mouse is pressed and the user is in a mode where |
117 * select-to-speak is capturing mouse events (for example holding down | 93 * select-to-speak is capturing mouse events (for example holding down |
118 * Search). | 94 * Search). |
119 * | 95 * |
120 * @param {!Event} evt The DOM event | 96 * @param {!AutomationEvent} evt |
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. | |
124 */ | 97 */ |
125 onMouseDown_: function(evt) { | 98 onMousePressed_: function(evt) { |
126 if (!this.isSearchKeyDown_) | 99 this.down_ = true; |
127 return false; | 100 this.mouseStart_ = {x: evt.mouseX, y: evt.mouseY}; |
128 | 101 this.startNode_ = evt.target; |
129 this.trackingMouse_ = true; | |
130 this.didTrackMouse_ = true; | |
131 this.mouseStart_ = {x: evt.screenX, y: evt.screenY}; | |
132 chrome.tts.stop(); | 102 chrome.tts.stop(); |
133 | 103 this.onMouseDragged_(evt); |
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; | |
139 }, | 104 }, |
140 | 105 |
141 /** | 106 /** |
142 * Called when the mouse is moved or dragged and the user is in a | 107 * Called when the mouse is moved or dragged and the user is in a |
143 * mode where select-to-speak is capturing mouse events (for example | 108 * mode where select-to-speak is capturing mouse events (for example |
144 * holding down Search). | 109 * holding down Search). |
145 * | 110 * |
146 * @param {!Event} evt The DOM event | 111 * @param {!AutomationEvent} evt |
147 * @return {boolean} True if the default action should be performed. | |
148 */ | 112 */ |
149 onMouseMove_: function(evt) { | 113 onMouseDragged_: function(evt) { |
150 if (!this.trackingMouse_) | 114 if (!this.down_) |
151 return false; | 115 return; |
152 | 116 |
153 var rect = rectFromPoints( | 117 var rect = rectFromPoints( |
154 this.mouseStart_.x, this.mouseStart_.y, | 118 this.mouseStart_.x, this.mouseStart_.y, |
155 evt.screenX, evt.screenY); | 119 evt.mouseX, evt.mouseY); |
156 chrome.accessibilityPrivate.setFocusRing([rect], this.color_); | 120 chrome.accessibilityPrivate.setFocusRing([rect], this.color_); |
157 return false; | |
158 }, | 121 }, |
159 | 122 |
160 /** | 123 /** |
161 * Called when the mouse is released and the user is in a | 124 * Called when the mouse is released and the user is in a |
162 * mode where select-to-speak is capturing mouse events (for example | 125 * mode where select-to-speak is capturing mouse events (for example |
163 * holding down Search). | 126 * holding down Search). |
164 * | 127 * |
165 * @param {!Event} evt | 128 * @param {!AutomationEvent} evt |
166 * @return {boolean} True if the default action should be performed. | |
167 */ | 129 */ |
168 onMouseUp_: function(evt) { | 130 onMouseReleased_: function(evt) { |
169 this.onMouseMove_(evt); | 131 this.onMouseDragged_(evt); |
170 this.trackingMouse_ = false; | 132 this.down_ = false; |
171 | 133 |
172 chrome.accessibilityPrivate.setFocusRing([]); | 134 chrome.accessibilityPrivate.setFocusRing([]); |
173 | 135 |
174 this.mouseEnd_ = {x: evt.screenX, y: evt.screenY}; | 136 // Walk up to the nearest window, web area, or dialog that the |
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 | |
196 // hit node is contained inside. Only speak objects within that | 137 // hit node is contained inside. Only speak objects within that |
197 // container. In the future we might include other container-like | 138 // container. In the future we might include other container-like |
198 // roles here. | 139 // roles here. |
199 var root = evt.target; | 140 var root = this.startNode_; |
200 while (root.parent && | 141 while (root.parent && |
201 root.role != RoleType.WINDOW && | 142 root.role != RoleType.WINDOW && |
202 root.role != RoleType.ROOT_WEB_AREA && | 143 root.role != RoleType.ROOT_WEB_AREA && |
203 root.role != RoleType.DESKTOP && | 144 root.role != RoleType.DESKTOP && |
204 root.role != RoleType.DIALOG && | 145 root.role != RoleType.DIALOG) { |
205 root.role != RoleType.ALERT_DIALOG && | |
206 root.role != RoleType.TOOLBAR) { | |
207 root = root.parent; | 146 root = root.parent; |
208 } | 147 } |
209 | 148 |
210 var rect = rectFromPoints( | 149 var rect = rectFromPoints( |
211 this.mouseStart_.x, this.mouseStart_.y, | 150 this.mouseStart_.x, this.mouseStart_.y, |
212 this.mouseEnd_.x, this.mouseEnd_.y); | 151 evt.mouseX, evt.mouseY); |
213 var nodes = []; | 152 var nodes = []; |
214 this.findAllMatching_(root, rect, nodes); | 153 this.findAllMatching_(root, rect, nodes); |
215 this.startSpeechQueue_(nodes); | 154 this.startSpeechQueue_(nodes); |
216 }, | 155 }, |
217 | 156 |
218 /** | 157 /** |
219 * @param {!Event} evt | 158 * Called when the user cancels select-to-speak's capturing of mouse |
| 159 * events (for example by releasing Search while the mouse is still down). |
| 160 * |
| 161 * @param {!AutomationEvent} evt |
220 */ | 162 */ |
221 onKeyDown_: function(evt) { | 163 onMouseCanceled_: function(evt) { |
222 if (this.keysPressedTogether_.size == 0 && | 164 this.down_ = false; |
223 evt.keyCode == SelectToSpeak.SEARCH_KEY_CODE) { | 165 chrome.accessibilityPrivate.setFocusRing([]); |
224 this.isSearchKeyDown_ = true; | 166 chrome.tts.stop(); |
225 } else if (!this.trackingMouse_) { | |
226 this.isSearchKeyDown_ = false; | |
227 } | |
228 | |
229 this.keysCurrentlyDown_.add(evt.keyCode); | |
230 this.keysPressedTogether_.add(evt.keyCode); | |
231 }, | 167 }, |
232 | 168 |
233 /** | 169 /** |
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 /** | |
282 * Finds all nodes within the subtree rooted at |node| that overlap | 170 * Finds all nodes within the subtree rooted at |node| that overlap |
283 * a given rectangle. | 171 * a given rectangle. |
284 * @param {AutomationNode} node The starting node. | 172 * @param {AutomationNode} node The starting node. |
285 * @param {{left: number, top: number, width: number, height: number}} rect | 173 * @param {{left: number, top: number, width: number, height: number}} rect |
286 * The bounding box to search. | 174 * The bounding box to search. |
287 * @param {Array<AutomationNode>} nodes The matching node array to be | 175 * @param {Array<AutomationNode>} nodes The matching node array to be |
288 * populated. | 176 * populated. |
289 * @return {boolean} True if any matches are found. | 177 * @return {boolean} True if any matches are found. |
290 */ | 178 */ |
291 findAllMatching_: function(node, rect, nodes) { | 179 findAllMatching_: function(node, rect, nodes) { |
(...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
388 this.updateDefaultVoice_(); | 276 this.updateDefaultVoice_(); |
389 window.speechSynthesis.onvoiceschanged = (function() { | 277 window.speechSynthesis.onvoiceschanged = (function() { |
390 this.updateDefaultVoice_(); | 278 this.updateDefaultVoice_(); |
391 }).bind(this); | 279 }).bind(this); |
392 }, | 280 }, |
393 | 281 |
394 /** | 282 /** |
395 * Get the list of TTS voices, and set the default voice if not already set. | 283 * Get the list of TTS voices, and set the default voice if not already set. |
396 */ | 284 */ |
397 updateDefaultVoice_: function() { | 285 updateDefaultVoice_: function() { |
| 286 console.log('updateDefaultVoice_ ' + this.down_); |
398 var uiLocale = chrome.i18n.getMessage('@@ui_locale'); | 287 var uiLocale = chrome.i18n.getMessage('@@ui_locale'); |
399 uiLocale = uiLocale.replace('_', '-').toLowerCase(); | 288 uiLocale = uiLocale.replace('_', '-').toLowerCase(); |
400 | 289 |
401 chrome.tts.getVoices((function(voices) { | 290 chrome.tts.getVoices((function(voices) { |
402 console.log('updateDefaultVoice_ voices: ' + voices.length); | 291 console.log('updateDefaultVoice_ voices: ' + voices.length); |
403 this.validVoiceNames_ = new Set(); | 292 this.validVoiceNames_ = new Set(); |
404 | 293 |
405 if (voices.length == 0) | 294 if (voices.length == 0) |
406 return; | 295 return; |
407 | 296 |
(...skipping 17 matching lines...) Expand all Loading... |
425 this.voiceNameFromLocale_ = voices[0].voiceName; | 314 this.voiceNameFromLocale_ = voices[0].voiceName; |
426 | 315 |
427 chrome.storage.sync.get(['voice'], (function(prefs) { | 316 chrome.storage.sync.get(['voice'], (function(prefs) { |
428 if (!prefs['voice']) { | 317 if (!prefs['voice']) { |
429 chrome.storage.sync.set({'voice': voices[0].voiceName}); | 318 chrome.storage.sync.set({'voice': voices[0].voiceName}); |
430 } | 319 } |
431 }).bind(this)); | 320 }).bind(this)); |
432 }).bind(this)); | 321 }).bind(this)); |
433 } | 322 } |
434 }; | 323 }; |
OLD | NEW |