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.error('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.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) { |
| 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': |
| 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; |
| 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 |