Chromium Code Reviews| 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 |