OLD | NEW |
---|---|
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 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 * @fileoverview ChromeVox panel. | 6 * @fileoverview The ChromeVox panel and menus. |
7 * | |
8 */ | 7 */ |
9 | 8 |
10 goog.provide('Panel'); | 9 goog.provide('Panel'); |
11 | 10 |
12 goog.require('Msgs'); | 11 goog.require('Msgs'); |
13 goog.require('PanelCommand'); | 12 goog.require('PanelCommand'); |
14 | 13 goog.require('PanelMenu'); |
15 function $(id) { | 14 goog.require('PanelMenuItem'); |
16 return document.getElementById(id); | 15 goog.require('cvox.ChromeVoxKbHandler'); |
17 } | 16 goog.require('cvox.CommandStore'); |
18 | 17 |
19 /** | 18 /** |
20 * Class to manage the panel. | 19 * Class to manage the panel. |
21 * @constructor | 20 * @constructor |
22 */ | 21 */ |
23 Panel = function() { | 22 Panel = function() { |
24 }; | 23 }; |
25 | 24 |
26 /** | 25 /** |
27 * Initialize the panel. | 26 * Initialize the panel. |
28 */ | 27 */ |
29 Panel.init = function() { | 28 Panel.init = function() { |
30 /** @type {Element} @private */ | 29 /** @type {Element} @private */ |
31 this.speechContainer_ = $('speech-container'); | 30 this.speechContainer_ = $('speech-container'); |
32 | 31 |
33 /** @type {Element} @private */ | 32 /** @type {Element} @private */ |
34 this.speechElement_ = $('speech'); | 33 this.speechElement_ = $('speech'); |
35 | 34 |
36 /** @type {Element} @private */ | 35 /** @type {Element} @private */ |
37 this.brailleContainer_ = $('braille-container'); | 36 this.brailleContainer_ = $('braille-container'); |
38 | 37 |
39 /** @type {Element} @private */ | 38 /** @type {Element} @private */ |
40 this.brailleTextElement_ = $('braille-text'); | 39 this.brailleTextElement_ = $('braille-text'); |
41 | 40 |
42 /** @type {Element} @private */ | 41 /** @type {Element} @private */ |
43 this.brailleCellsElement_ = $('braille-cells'); | 42 this.brailleCellsElement_ = $('braille-cells'); |
44 | 43 |
44 /** | |
45 * The array of top-level menus. | |
46 * @type {!Array<PanelMenu>} | |
47 * @private | |
48 */ | |
49 this.menus_ = []; | |
50 | |
51 /** | |
52 * The currently active menu, if any. | |
53 * @type {?PanelMenu} | |
David Tseng
2016/01/08 21:39:33
Object types are nullable by default right?
dmazzoni
2016/01/11 22:04:06
Done.
| |
54 * @private | |
55 */ | |
56 this.activeMenu_ = null; | |
57 | |
58 /** | |
59 * If the menu button in the panel is enabled at all. It's disabled if | |
David Tseng
2016/01/08 21:39:33
True if ...?
dmazzoni
2016/01/11 22:04:06
Done.
| |
60 * ChromeVox Next is not active. | |
61 * @type {boolean} | |
62 * @private | |
63 */ | |
64 this.menusEnabled_ = false; | |
65 | |
66 /** | |
67 * A callback function to be executed to perform the action from selecting | |
68 * a menu item after the menu has been closed and focus has been restored | |
69 * to the page or wherever it was previously. | |
70 * @type {?Function} | |
71 * @private | |
72 */ | |
73 this.pendingCallback_ = null; | |
74 | |
45 Panel.updateFromPrefs(); | 75 Panel.updateFromPrefs(); |
76 | |
77 Msgs.addTranslatedMessagesToDom(document); | |
78 | |
46 window.addEventListener('storage', function(event) { | 79 window.addEventListener('storage', function(event) { |
47 if (event.key == 'brailleCaptions') { | 80 if (event.key == 'brailleCaptions') { |
48 Panel.updateFromPrefs(); | 81 Panel.updateFromPrefs(); |
49 } | 82 } |
50 }, false); | 83 }, false); |
51 | 84 |
52 window.addEventListener('message', function(message) { | 85 window.addEventListener('message', function(message) { |
53 var command = JSON.parse(message.data); | 86 var command = JSON.parse(message.data); |
54 Panel.exec(/** @type {PanelCommand} */(command)); | 87 Panel.exec(/** @type {PanelCommand} */(command)); |
55 }, false); | 88 }, false); |
56 | 89 |
90 $('menus_button').addEventListener('mousedown', Panel.onOpenMenus, false); | |
57 $('options').addEventListener('click', Panel.onOptions, false); | 91 $('options').addEventListener('click', Panel.onOptions, false); |
58 $('close').addEventListener('click', Panel.onClose, false); | 92 $('close').addEventListener('click', Panel.onClose, false); |
59 | 93 |
60 // The ChromeVox menu isn't fully implemented yet, disable it. | 94 document.addEventListener('keydown', Panel.onKeyDown, false); |
61 $('menu').disabled = true; | 95 document.addEventListener('mouseup', Panel.onMouseUp, false); |
62 $('triangle').style.display = 'none'; | |
63 | |
64 Msgs.addTranslatedMessagesToDom(document); | |
65 }; | 96 }; |
66 | 97 |
67 /** | 98 /** |
68 * Update the display based on prefs. | 99 * Update the display based on prefs. |
69 */ | 100 */ |
70 Panel.updateFromPrefs = function() { | 101 Panel.updateFromPrefs = function() { |
71 if (localStorage['brailleCaptions'] === String(true)) { | 102 if (localStorage['brailleCaptions'] === String(true)) { |
72 this.speechContainer_.style.visibility = 'hidden'; | 103 this.speechContainer_.style.visibility = 'hidden'; |
73 this.brailleContainer_.style.visibility = 'visible'; | 104 this.brailleContainer_.style.visibility = 'visible'; |
74 } else { | 105 } else { |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
113 case PanelCommandType.ADD_ANNOTATION_SPEECH: | 144 case PanelCommandType.ADD_ANNOTATION_SPEECH: |
114 if (this.speechElement_.innerHTML != '') { | 145 if (this.speechElement_.innerHTML != '') { |
115 this.speechElement_.innerHTML += ' '; | 146 this.speechElement_.innerHTML += ' '; |
116 } | 147 } |
117 this.speechElement_.innerHTML += escapeForHtml(command.data); | 148 this.speechElement_.innerHTML += escapeForHtml(command.data); |
118 break; | 149 break; |
119 case PanelCommandType.UPDATE_BRAILLE: | 150 case PanelCommandType.UPDATE_BRAILLE: |
120 this.brailleTextElement_.textContent = command.data.text; | 151 this.brailleTextElement_.textContent = command.data.text; |
121 this.brailleCellsElement_.textContent = command.data.braille; | 152 this.brailleCellsElement_.textContent = command.data.braille; |
122 break; | 153 break; |
123 } | 154 case PanelCommandType.ENABLE_MENUS: |
124 }; | 155 Panel.onEnableMenus(); |
125 | 156 break; |
126 /** | 157 case PanelCommandType.DISABLE_MENUS: |
158 Panel.onDisableMenus(); | |
159 break; | |
160 case PanelCommandType.OPEN_MENUS: | |
161 Panel.onOpenMenus(); | |
162 break; | |
163 } | |
164 }; | |
165 | |
166 /** | |
167 * Enable the ChromeVox Menus. | |
168 */ | |
169 Panel.onEnableMenus = function() { | |
170 Panel.menusEnabled_ = true; | |
171 $('menus_button').disabled = false; | |
172 $('triangle').style.display = ''; | |
173 }; | |
174 | |
175 /** | |
176 * Disable the ChromeVox Menus. | |
177 */ | |
178 Panel.onDisableMenus = function() { | |
179 Panel.menusEnabled_ = false; | |
180 $('menus_button').disabled = true; | |
181 $('triangle').style.display = 'none'; | |
182 }; | |
183 | |
184 /** | |
185 * Open / show the ChromeVox Menus. | |
186 * @param {Event=} opt_event An optional event that triggered this. | |
187 */ | |
188 Panel.onOpenMenus = function(opt_event) { | |
189 // Don't open the menu if it's not enabled, such as when ChromeVox Next | |
190 // is not active. | |
191 if (!Panel.menusEnabled_) | |
192 return; | |
193 | |
194 // Eat the event so that a mousedown isn't turned into a drag, allowing | |
195 // users to click-drag-release to select a menu item. | |
196 if (opt_event) { | |
197 opt_event.stopPropagation(); | |
198 opt_event.preventDefault(); | |
199 } | |
200 | |
201 // Change the url fragment to 'fullscreen', which signals the native | |
202 // host code to make the window fullscreen, revealing the menus. | |
203 window.location = '#fullscreen'; | |
204 | |
205 // Clear any existing menus and clear the callback. | |
206 Panel.clearMenus(); | |
207 Panel.pendingCallback_ = null; | |
208 | |
209 // Build the top-level menus. | |
210 var jumpMenu = Panel.addMenu('Jump'); | |
211 var speechMenu = Panel.addMenu('Speech'); | |
212 var tabsMenu = Panel.addMenu('Tabs'); | |
213 var chromevoxMenu = Panel.addMenu('ChromeVox'); | |
214 | |
215 // Create a mapping between categories from CommandStore, and our | |
216 // top-level menus. Some categories aren't mapped to any menu. | |
David Tseng
2016/01/08 21:39:33
Do the non-categorized options get placed in the t
dmazzoni
2016/01/11 22:04:06
No, the top-level menus are hard-coded in the code
| |
217 var categoryToMenu = { | |
David Tseng
2016/01/08 21:39:33
Could promote this to an assigned var of Panel.
dmazzoni
2016/01/11 22:04:06
Not easily because the mapping goes to PanelMenu o
| |
218 'navigation': jumpMenu, | |
219 'jump_commands': jumpMenu, | |
220 'controlling_speech': speechMenu, | |
221 'modifier_keys': chromevoxMenu, | |
222 'help_commands': chromevoxMenu, | |
223 | |
224 'information': null, // Get link URL, get page title, etc. | |
225 'overview': null, // Headings list, etc. | |
226 'tables': null, // Table navigation. | |
227 'braille': null, | |
228 'developer': null}; | |
229 | |
230 // Get the key map from the background page. | |
231 var bkgnd = chrome.extension.getBackgroundPage(); | |
232 var keymap = bkgnd['cvox']['ChromeVoxKbHandler']['handlerKeyMap']; | |
David Tseng
2016/01/08 21:39:32
You could also use cvox.KeyMap.fromCurrent.
dmazzoni
2016/01/11 22:04:06
Done.
| |
233 | |
234 // Make a copy of the key bindings, get the localized title of each | |
235 // command, and then sort them. | |
236 var sortedBindings = keymap.bindings().slice(); | |
237 sortedBindings.forEach(goog.bind(function(binding) { | |
238 var command = binding.command; | |
239 var keySeq = binding.sequence; | |
240 binding.keySeq = cvox.KeyUtil.keySequenceToString(keySeq, true); | |
241 var titleMsgId = cvox.CommandStore.messageForCommand(command); | |
242 if (!titleMsgId) { | |
243 console.log('No localization for: ' + command); | |
244 binding.title = ''; | |
245 return; | |
246 } | |
247 var title = Msgs.getMsg(titleMsgId); | |
248 // Convert to title case. | |
249 title = title.replace(/\w\S*/g, function(word) { | |
250 return word.charAt(0).toUpperCase() + word.substr(1); | |
251 }); | |
252 binding.title = title; | |
253 }, this)); | |
254 sortedBindings.sort(function(binding1, binding2) { | |
255 return binding1.title.localeCompare(binding2.title); | |
256 }); | |
257 | |
258 // Insert items from the bindings into the menus. | |
259 sortedBindings.forEach(goog.bind(function(binding) { | |
260 var category = cvox.CommandStore.categoryForCommand(binding.command); | |
261 var menu = category ? categoryToMenu[category] : null; | |
262 if (binding.title && menu) { | |
263 menu.addMenuItem( | |
264 binding.title, | |
265 binding.keySeq, | |
266 function() { | |
267 var bkgnd = | |
268 chrome.extension.getBackgroundPage()['global']['backgroundObj']; | |
269 bkgnd['onGotCommand'](binding.command); | |
270 }); | |
271 } | |
272 }, this)); | |
273 | |
274 // Add all open tabs to the Tabs menu. | |
275 bkgnd.chrome.windows.getAll({'populate': true}, function(windows) { | |
276 for (var i = 0; i < windows.length; i++) { | |
277 var tabs = windows[i].tabs; | |
278 for (var j = 0; j < tabs.length; j++) { | |
279 tabsMenu.addMenuItem(tabs[j].title, '', (function(win, tab) { | |
David Tseng
2016/01/08 21:39:33
It would be nice to include whether a tab was curr
dmazzoni
2016/01/11 22:04:06
Done.
| |
280 bkgnd.chrome.windows.update(win.id, {focused: true}, function() { | |
281 bkgnd.chrome.tabs.update(tab.id, {active: true}); | |
282 }); | |
283 }).bind(this, windows[i], tabs[j])); | |
284 } | |
285 } | |
286 }); | |
287 | |
288 // Add a menu item that disables / closes ChromeVox. | |
289 chromevoxMenu.addMenuItem( | |
290 Msgs.getMsg('disable_chromevox'), 'Ctrl+Alt+Z', function() { | |
291 Panel.onClose(); | |
292 }); | |
293 | |
294 // Activate the first menu. | |
295 Panel.activateMenu(Panel.menus_[0]); | |
296 }; | |
297 | |
298 /** | |
299 * Clear any previous menus. The menus are all regenerated each time the | |
300 * menus are opened. | |
301 */ | |
302 Panel.clearMenus = function() { | |
303 while (this.menus_.length) { | |
304 var menu = this.menus_.pop(); | |
305 $('menu-bar').removeChild(menu.menuBarItemElement); | |
306 $('menus_background').removeChild(menu.menuContainerElement); | |
307 } | |
308 this.activeMenu_ = null; | |
309 }; | |
310 | |
311 /** | |
312 * Create a new menu with the given name and add it to the menu bar. | |
313 * @param {string} menuTitle The title of the new menu to add. | |
314 * @return {PanelMenu} The menu just created. | |
315 */ | |
316 Panel.addMenu = function(menuTitle) { | |
317 var menu = new PanelMenu(menuTitle); | |
318 $('menu-bar').appendChild(menu.menuBarItemElement); | |
319 menu.menuBarItemElement.addEventListener('mouseover', function() { | |
320 Panel.activateMenu(menu); | |
321 }, false); | |
322 | |
323 $('menus_background').appendChild(menu.menuContainerElement); | |
324 this.menus_.push(menu); | |
325 return menu; | |
326 }; | |
327 | |
328 /** | |
329 * Activate a menu, which implies hiding the previous active menu. | |
330 * @param {PanelMenu} menu The new menu to activate. | |
331 */ | |
332 Panel.activateMenu = function(menu) { | |
333 if (menu == this.activeMenu_) | |
334 return; | |
335 | |
336 if (this.activeMenu_) { | |
337 this.activeMenu_.deactivate(); | |
338 this.activeMenu_ = null; | |
339 } | |
340 | |
341 this.activeMenu_ = menu; | |
342 this.pendingCallback_ = null; | |
343 | |
344 if (this.activeMenu_) { | |
345 this.activeMenu_.activate(); | |
346 } | |
347 }; | |
348 | |
349 /** | |
350 * Advance the index of the current active menu by |delta|. | |
351 * @param {number} delta The number to add to the active menu index. | |
352 */ | |
353 Panel.advanceActiveMenuBy = function(delta) { | |
354 var activeIndex = -1; | |
355 for (var i = 0; i < this.menus_.length; i++) { | |
356 if (this.activeMenu_ == this.menus_[i]) { | |
357 activeIndex = i; | |
358 break; | |
359 } | |
360 } | |
361 | |
362 if (activeIndex >= 0) { | |
363 activeIndex += delta; | |
364 activeIndex = (activeIndex + this.menus_.length) % this.menus_.length; | |
David Tseng
2016/01/08 21:39:32
Can't you just do:
activeIndex %= this.menus_.leng
dmazzoni
2016/01/11 22:04:06
That wouldn't work with negative numbers. We want
| |
365 } else { | |
366 if (delta >= 0) | |
367 activeIndex = 0; | |
368 else | |
369 activeIndex = this.menus_.length - 1; | |
370 } | |
371 Panel.activateMenu(this.menus_[activeIndex]); | |
372 }; | |
373 | |
374 /** | |
375 * Advance the index of the current active menu item by |delta|. | |
376 * @param {number} delta The number to add to the active menu item index. | |
377 */ | |
378 Panel.advanceItemBy = function(delta) { | |
379 if (this.activeMenu_) | |
380 this.activeMenu_.advanceItemBy(delta); | |
381 }; | |
382 | |
383 /** | |
384 * Called when the user releases the mouse button. If it's anywhere other | |
385 * than on the menus button, close the menus and return focus to the page, | |
386 * and if the mouse was released over a menu item, execute that item's | |
387 * callback. | |
388 * @param {Event} event The mouse event. | |
389 */ | |
390 Panel.onMouseUp = function(event) { | |
391 var target = event.target; | |
392 while (target && !target.classList.contains('menu-item')) { | |
393 // Allow the user to click and release on the menu button and leave | |
394 // the menu button. Otherwise releasing the mouse anywhere else will | |
395 // close the menu. | |
396 if (target.id == 'menus_button') | |
397 return; | |
398 | |
399 target = target.parentElement; | |
400 } | |
401 | |
402 if (target && Panel.activeMenu_) | |
403 Panel.pendingCallback_ = Panel.activeMenu_.getCallbackForElement(target); | |
404 Panel.closeMenusAndRestoreFocus(); | |
405 }; | |
406 | |
407 /** | |
408 * Called when a key is pressed. Handle arrow keys to navigate the menus, | |
409 * Esc to close, and Enter/Space to activate an item. | |
410 * @param {Event} event The key event. | |
411 */ | |
412 Panel.onKeyDown = function(event) { | |
413 if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) | |
414 return; | |
415 | |
416 switch (event.keyIdentifier) { | |
417 case 'Left': | |
418 Panel.advanceActiveMenuBy(-1); | |
419 break; | |
420 case 'Right': | |
421 Panel.advanceActiveMenuBy(1); | |
422 break; | |
423 case 'Up': | |
424 Panel.advanceItemBy(-1); | |
425 break; | |
426 case 'Down': | |
427 Panel.advanceItemBy(1); | |
428 break; | |
429 case 'U+001B': // Escape | |
430 Panel.closeMenusAndRestoreFocus(); | |
431 break; | |
432 case 'Enter': // Enter | |
433 case 'U+0020': // Space | |
434 Panel.pendingCallback_ = Panel.getCallbackForCurrentItem(); | |
435 Panel.closeMenusAndRestoreFocus(); | |
436 break; | |
437 default: | |
438 // Don't mark this event as handled. | |
439 return; | |
440 } | |
441 | |
442 event.preventDefault(); | |
443 event.stopPropagation(); | |
444 }; | |
445 | |
446 /** | |
127 * Open the ChromeVox Options. | 447 * Open the ChromeVox Options. |
128 */ | 448 */ |
129 Panel.onOptions = function() { | 449 Panel.onOptions = function() { |
130 var bkgnd = | 450 var bkgnd = |
131 chrome.extension.getBackgroundPage()['global']['backgroundObj']; | 451 chrome.extension.getBackgroundPage()['global']['backgroundObj']; |
132 bkgnd['showOptionsPage'](); | 452 bkgnd['showOptionsPage'](); |
133 window.location = '#'; | 453 window.location = '#'; |
134 }; | 454 }; |
135 | 455 |
136 /** | 456 /** |
137 * Exit ChromeVox. | 457 * Exit ChromeVox. |
138 */ | 458 */ |
139 Panel.onClose = function() { | 459 Panel.onClose = function() { |
140 window.location = '#close'; | 460 window.location = '#close'; |
141 }; | 461 }; |
142 | 462 |
463 /** | |
464 * Get the callback for whatever item is currently selected. | |
465 * @return {Function} The callback for the current item. | |
466 */ | |
467 Panel.getCallbackForCurrentItem = function() { | |
468 if (this.activeMenu_) | |
469 return this.activeMenu_.getCallbackForCurrentItem(); | |
470 return null; | |
471 }; | |
472 | |
473 /** | |
474 * Close the menus and restore focus to the page. If a menu item's callback | |
475 * was queued, execute it once focus is restored. | |
476 */ | |
477 Panel.closeMenusAndRestoreFocus = function() { | |
478 // Make sure we're not in full-screen mode. | |
479 window.location = '#'; | |
480 | |
481 var bkgnd = | |
482 chrome.extension.getBackgroundPage()['global']['backgroundObj']; | |
483 bkgnd['restoreCurrentRange'](); | |
484 if (Panel.pendingCallback_) | |
485 Panel.pendingCallback_(); | |
486 }; | |
487 | |
143 window.addEventListener('load', function() { | 488 window.addEventListener('load', function() { |
144 Panel.init(); | 489 Panel.init(); |
145 }, false); | 490 }, false); |
OLD | NEW |