| 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 The ChromeVox panel and menus. | 6 * @fileoverview ChromeVox panel. |
| 7 * |
| 7 */ | 8 */ |
| 8 | 9 |
| 9 goog.provide('Panel'); | 10 goog.provide('Panel'); |
| 10 | 11 |
| 11 goog.require('Msgs'); | 12 goog.require('Msgs'); |
| 12 goog.require('PanelCommand'); | 13 goog.require('PanelCommand'); |
| 13 goog.require('PanelMenu'); | 14 |
| 14 goog.require('PanelMenuItem'); | 15 function $(id) { |
| 15 goog.require('cvox.ChromeVoxKbHandler'); | 16 return document.getElementById(id); |
| 16 goog.require('cvox.CommandStore'); | 17 } |
| 17 | 18 |
| 18 /** | 19 /** |
| 19 * Class to manage the panel. | 20 * Class to manage the panel. |
| 20 * @constructor | 21 * @constructor |
| 21 */ | 22 */ |
| 22 Panel = function() { | 23 Panel = function() { |
| 23 }; | 24 }; |
| 24 | 25 |
| 25 /** | 26 /** |
| 26 * Initialize the panel. | 27 * Initialize the panel. |
| 27 */ | 28 */ |
| 28 Panel.init = function() { | 29 Panel.init = function() { |
| 29 /** @type {Element} @private */ | 30 /** @type {Element} @private */ |
| 30 this.speechContainer_ = $('speech-container'); | 31 this.speechContainer_ = $('speech-container'); |
| 31 | 32 |
| 32 /** @type {Element} @private */ | 33 /** @type {Element} @private */ |
| 33 this.speechElement_ = $('speech'); | 34 this.speechElement_ = $('speech'); |
| 34 | 35 |
| 35 /** @type {Element} @private */ | 36 /** @type {Element} @private */ |
| 36 this.brailleContainer_ = $('braille-container'); | 37 this.brailleContainer_ = $('braille-container'); |
| 37 | 38 |
| 38 /** @type {Element} @private */ | 39 /** @type {Element} @private */ |
| 39 this.brailleTextElement_ = $('braille-text'); | 40 this.brailleTextElement_ = $('braille-text'); |
| 40 | 41 |
| 41 /** @type {Element} @private */ | 42 /** @type {Element} @private */ |
| 42 this.brailleCellsElement_ = $('braille-cells'); | 43 this.brailleCellsElement_ = $('braille-cells'); |
| 43 | 44 |
| 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 | |
| 75 Panel.updateFromPrefs(); | 45 Panel.updateFromPrefs(); |
| 76 | |
| 77 Msgs.addTranslatedMessagesToDom(document); | |
| 78 | |
| 79 window.addEventListener('storage', function(event) { | 46 window.addEventListener('storage', function(event) { |
| 80 if (event.key == 'brailleCaptions') { | 47 if (event.key == 'brailleCaptions') { |
| 81 Panel.updateFromPrefs(); | 48 Panel.updateFromPrefs(); |
| 82 } | 49 } |
| 83 }, false); | 50 }, false); |
| 84 | 51 |
| 85 window.addEventListener('message', function(message) { | 52 window.addEventListener('message', function(message) { |
| 86 var command = JSON.parse(message.data); | 53 var command = JSON.parse(message.data); |
| 87 Panel.exec(/** @type {PanelCommand} */(command)); | 54 Panel.exec(/** @type {PanelCommand} */(command)); |
| 88 }, false); | 55 }, false); |
| 89 | 56 |
| 90 $('menus_button').addEventListener('mousedown', Panel.onOpenMenus, false); | |
| 91 $('options').addEventListener('click', Panel.onOptions, false); | 57 $('options').addEventListener('click', Panel.onOptions, false); |
| 92 $('close').addEventListener('click', Panel.onClose, false); | 58 $('close').addEventListener('click', Panel.onClose, false); |
| 93 | 59 |
| 94 document.addEventListener('keydown', Panel.onKeyDown, false); | 60 // The ChromeVox menu isn't fully implemented yet, disable it. |
| 95 document.addEventListener('mouseup', Panel.onMouseUp, false); | 61 $('menu').disabled = true; |
| 62 $('triangle').style.display = 'none'; |
| 63 |
| 64 Msgs.addTranslatedMessagesToDom(document); |
| 96 }; | 65 }; |
| 97 | 66 |
| 98 /** | 67 /** |
| 99 * Update the display based on prefs. | 68 * Update the display based on prefs. |
| 100 */ | 69 */ |
| 101 Panel.updateFromPrefs = function() { | 70 Panel.updateFromPrefs = function() { |
| 102 if (localStorage['brailleCaptions'] === String(true)) { | 71 if (localStorage['brailleCaptions'] === String(true)) { |
| 103 this.speechContainer_.style.visibility = 'hidden'; | 72 this.speechContainer_.style.visibility = 'hidden'; |
| 104 this.brailleContainer_.style.visibility = 'visible'; | 73 this.brailleContainer_.style.visibility = 'visible'; |
| 105 } else { | 74 } else { |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 144 case PanelCommandType.ADD_ANNOTATION_SPEECH: | 113 case PanelCommandType.ADD_ANNOTATION_SPEECH: |
| 145 if (this.speechElement_.innerHTML != '') { | 114 if (this.speechElement_.innerHTML != '') { |
| 146 this.speechElement_.innerHTML += ' '; | 115 this.speechElement_.innerHTML += ' '; |
| 147 } | 116 } |
| 148 this.speechElement_.innerHTML += escapeForHtml(command.data); | 117 this.speechElement_.innerHTML += escapeForHtml(command.data); |
| 149 break; | 118 break; |
| 150 case PanelCommandType.UPDATE_BRAILLE: | 119 case PanelCommandType.UPDATE_BRAILLE: |
| 151 this.brailleTextElement_.textContent = command.data.text; | 120 this.brailleTextElement_.textContent = command.data.text; |
| 152 this.brailleCellsElement_.textContent = command.data.braille; | 121 this.brailleCellsElement_.textContent = command.data.braille; |
| 153 break; | 122 break; |
| 154 case PanelCommandType.ENABLE_MENUS: | |
| 155 Panel.onEnableMenus(); | |
| 156 break; | |
| 157 case PanelCommandType.DISABLE_MENUS: | |
| 158 Panel.onDisableMenus(); | |
| 159 break; | |
| 160 case PanelCommandType.OPEN_MENUS: | |
| 161 Panel.onOpenMenus(); | |
| 162 break; | |
| 163 } | 123 } |
| 164 }; | 124 }; |
| 165 | 125 |
| 166 /** | 126 /** |
| 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 /** | |
| 452 * Open the ChromeVox Options. | 127 * Open the ChromeVox Options. |
| 453 */ | 128 */ |
| 454 Panel.onOptions = function() { | 129 Panel.onOptions = function() { |
| 455 var bkgnd = | 130 var bkgnd = |
| 456 chrome.extension.getBackgroundPage()['global']['backgroundObj']; | 131 chrome.extension.getBackgroundPage()['global']['backgroundObj']; |
| 457 bkgnd['showOptionsPage'](); | 132 bkgnd['showOptionsPage'](); |
| 458 window.location = '#'; | 133 window.location = '#'; |
| 459 }; | 134 }; |
| 460 | 135 |
| 461 /** | 136 /** |
| 462 * Exit ChromeVox. | 137 * Exit ChromeVox. |
| 463 */ | 138 */ |
| 464 Panel.onClose = function() { | 139 Panel.onClose = function() { |
| 465 window.location = '#close'; | 140 window.location = '#close'; |
| 466 }; | 141 }; |
| 467 | 142 |
| 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 | |
| 493 window.addEventListener('load', function() { | 143 window.addEventListener('load', function() { |
| 494 Panel.init(); | 144 Panel.init(); |
| 495 }, false); | 145 }, false); |
| OLD | NEW |