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} | |
54 * @private | |
55 */ | |
56 this.activeMenu_ = null; | |
57 | |
58 /** | |
59 * True if the menu button in the panel is enabled at all. It's disabled if | |
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. | |
217 var categoryToMenu = { | |
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']['KeyMap']['fromCurrentKeyMap'](); | |
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); | |
David Tseng
2016/01/12 20:10:57
.error
dmazzoni
2016/01/13 23:26:40
Done.
| |
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.getLastFocused(function(lastFocusedWindow) { | |
276 bkgnd.chrome.windows.getAll({'populate': true}, function(windows) { | |
277 for (var i = 0; i < windows.length; i++) { | |
278 var tabs = windows[i].tabs; | |
279 for (var j = 0; j < tabs.length; j++) { | |
280 var title = tabs[j].title; | |
281 if (tabs[j].active && windows[i].id == lastFocusedWindow.id) | |
282 title += ' ' + Msgs.getMsg('active_tab'); | |
283 tabsMenu.addMenuItem(title, '', (function(win, tab) { | |
284 bkgnd.chrome.windows.update(win.id, {focused: true}, function() { | |
285 bkgnd.chrome.tabs.update(tab.id, {active: true}); | |
286 }); | |
287 }).bind(this, windows[i], tabs[j])); | |
288 } | |
289 } | |
290 }); | |
291 }); | |
292 | |
293 // Add a menu item that disables / closes ChromeVox. | |
294 chromevoxMenu.addMenuItem( | |
295 Msgs.getMsg('disable_chromevox'), 'Ctrl+Alt+Z', function() { | |
296 Panel.onClose(); | |
297 }); | |
298 | |
299 // Activate the first menu. | |
300 Panel.activateMenu(Panel.menus_[0]); | |
301 }; | |
302 | |
303 /** | |
304 * Clear any previous menus. The menus are all regenerated each time the | |
305 * menus are opened. | |
306 */ | |
307 Panel.clearMenus = function() { | |
308 while (this.menus_.length) { | |
309 var menu = this.menus_.pop(); | |
310 $('menu-bar').removeChild(menu.menuBarItemElement); | |
311 $('menus_background').removeChild(menu.menuContainerElement); | |
312 } | |
313 this.activeMenu_ = null; | |
314 }; | |
315 | |
316 /** | |
317 * Create a new menu with the given name and add it to the menu bar. | |
318 * @param {string} menuTitle The title of the new menu to add. | |
319 * @return {PanelMenu} The menu just created. | |
320 */ | |
321 Panel.addMenu = function(menuTitle) { | |
322 var menu = new PanelMenu(menuTitle); | |
323 $('menu-bar').appendChild(menu.menuBarItemElement); | |
324 menu.menuBarItemElement.addEventListener('mouseover', function() { | |
325 Panel.activateMenu(menu); | |
326 }, false); | |
327 | |
328 $('menus_background').appendChild(menu.menuContainerElement); | |
329 this.menus_.push(menu); | |
330 return menu; | |
331 }; | |
332 | |
333 /** | |
334 * Activate a menu, which implies hiding the previous active menu. | |
335 * @param {PanelMenu} menu The new menu to activate. | |
336 */ | |
337 Panel.activateMenu = function(menu) { | |
338 if (menu == this.activeMenu_) | |
339 return; | |
340 | |
341 if (this.activeMenu_) { | |
342 this.activeMenu_.deactivate(); | |
343 this.activeMenu_ = null; | |
344 } | |
345 | |
346 this.activeMenu_ = menu; | |
347 this.pendingCallback_ = null; | |
348 | |
349 if (this.activeMenu_) { | |
350 this.activeMenu_.activate(); | |
351 } | |
352 }; | |
353 | |
354 /** | |
355 * Advance the index of the current active menu by |delta|. | |
356 * @param {number} delta The number to add to the active menu index. | |
357 */ | |
358 Panel.advanceActiveMenuBy = function(delta) { | |
David Tseng
2016/01/12 20:10:57
Really optional but I was thinking this would be a
dmazzoni
2016/01/13 23:26:40
Sure, let's explore that as a follow-up.
| |
359 var activeIndex = -1; | |
360 for (var i = 0; i < this.menus_.length; i++) { | |
361 if (this.activeMenu_ == this.menus_[i]) { | |
362 activeIndex = i; | |
363 break; | |
364 } | |
365 } | |
366 | |
367 if (activeIndex >= 0) { | |
368 activeIndex += delta; | |
369 activeIndex = (activeIndex + this.menus_.length) % this.menus_.length; | |
370 } else { | |
371 if (delta >= 0) | |
372 activeIndex = 0; | |
373 else | |
374 activeIndex = this.menus_.length - 1; | |
375 } | |
376 Panel.activateMenu(this.menus_[activeIndex]); | |
377 }; | |
378 | |
379 /** | |
380 * Advance the index of the current active menu item by |delta|. | |
381 * @param {number} delta The number to add to the active menu item index. | |
382 */ | |
383 Panel.advanceItemBy = function(delta) { | |
384 if (this.activeMenu_) | |
385 this.activeMenu_.advanceItemBy(delta); | |
386 }; | |
387 | |
388 /** | |
389 * Called when the user releases the mouse button. If it's anywhere other | |
390 * than on the menus button, close the menus and return focus to the page, | |
391 * and if the mouse was released over a menu item, execute that item's | |
392 * callback. | |
393 * @param {Event} event The mouse event. | |
394 */ | |
395 Panel.onMouseUp = function(event) { | |
396 var target = event.target; | |
397 while (target && !target.classList.contains('menu-item')) { | |
398 // Allow the user to click and release on the menu button and leave | |
399 // the menu button. Otherwise releasing the mouse anywhere else will | |
400 // close the menu. | |
401 if (target.id == 'menus_button') | |
402 return; | |
403 | |
404 target = target.parentElement; | |
405 } | |
406 | |
407 if (target && Panel.activeMenu_) | |
408 Panel.pendingCallback_ = Panel.activeMenu_.getCallbackForElement(target); | |
409 Panel.closeMenusAndRestoreFocus(); | |
410 }; | |
411 | |
412 /** | |
413 * Called when a key is pressed. Handle arrow keys to navigate the menus, | |
414 * Esc to close, and Enter/Space to activate an item. | |
415 * @param {Event} event The key event. | |
416 */ | |
417 Panel.onKeyDown = function(event) { | |
418 if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) | |
419 return; | |
420 | |
421 switch (event.keyIdentifier) { | |
422 case 'Left': | |
423 Panel.advanceActiveMenuBy(-1); | |
424 break; | |
425 case 'Right': | |
David Tseng
2016/01/12 20:10:57
Are there submenus?
dmazzoni
2016/01/13 23:26:40
Not currently.
| |
426 Panel.advanceActiveMenuBy(1); | |
427 break; | |
428 case 'Up': | |
429 Panel.advanceItemBy(-1); | |
430 break; | |
431 case 'Down': | |
432 Panel.advanceItemBy(1); | |
433 break; | |
434 case 'U+001B': // Escape | |
435 Panel.closeMenusAndRestoreFocus(); | |
436 break; | |
437 case 'Enter': // Enter | |
438 case 'U+0020': // Space | |
439 Panel.pendingCallback_ = Panel.getCallbackForCurrentItem(); | |
440 Panel.closeMenusAndRestoreFocus(); | |
441 break; | |
David Tseng
2016/01/12 20:10:57
Are we thinking about supporting auto complete/typ
dmazzoni
2016/01/13 23:26:40
Yes, that'd be great. We should talk about what be
David Tseng
2016/01/14 03:13:59
Maybe something like the current keyboard help; ba
| |
442 default: | |
443 // Don't mark this event as handled. | |
444 return; | |
445 } | |
446 | |
447 event.preventDefault(); | |
448 event.stopPropagation(); | |
449 }; | |
450 | |
451 /** | |
127 * Open the ChromeVox Options. | 452 * Open the ChromeVox Options. |
128 */ | 453 */ |
129 Panel.onOptions = function() { | 454 Panel.onOptions = function() { |
130 var bkgnd = | 455 var bkgnd = |
131 chrome.extension.getBackgroundPage()['global']['backgroundObj']; | 456 chrome.extension.getBackgroundPage()['global']['backgroundObj']; |
132 bkgnd['showOptionsPage'](); | 457 bkgnd['showOptionsPage'](); |
133 window.location = '#'; | 458 window.location = '#'; |
134 }; | 459 }; |
135 | 460 |
136 /** | 461 /** |
137 * Exit ChromeVox. | 462 * Exit ChromeVox. |
138 */ | 463 */ |
139 Panel.onClose = function() { | 464 Panel.onClose = function() { |
140 window.location = '#close'; | 465 window.location = '#close'; |
141 }; | 466 }; |
142 | 467 |
468 /** | |
469 * Get the callback for whatever item is currently selected. | |
470 * @return {Function} The callback for the current item. | |
471 */ | |
472 Panel.getCallbackForCurrentItem = function() { | |
473 if (this.activeMenu_) | |
474 return this.activeMenu_.getCallbackForCurrentItem(); | |
475 return null; | |
476 }; | |
477 | |
478 /** | |
479 * Close the menus and restore focus to the page. If a menu item's callback | |
480 * was queued, execute it once focus is restored. | |
481 */ | |
482 Panel.closeMenusAndRestoreFocus = function() { | |
483 // Make sure we're not in full-screen mode. | |
484 window.location = '#'; | |
485 | |
486 var bkgnd = | |
487 chrome.extension.getBackgroundPage()['global']['backgroundObj']; | |
488 bkgnd['restoreCurrentRange'](); | |
489 if (Panel.pendingCallback_) | |
490 Panel.pendingCallback_(); | |
491 }; | |
492 | |
143 window.addEventListener('load', function() { | 493 window.addEventListener('load', function() { |
144 Panel.init(); | 494 Panel.init(); |
145 }, false); | 495 }, false); |
OLD | NEW |