Chromium Code Reviews| Index: chrome/browser/resources/chromeos/chromevox/cvox2/background/panel.js |
| diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/panel.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/panel.js |
| index 365c7c71b4d2f9e9a306f12d05e8aef7c05ae860..decc4841846726ffed8631d147b0cd0f09dfe388 100644 |
| --- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/panel.js |
| +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/panel.js |
| @@ -3,18 +3,17 @@ |
| // found in the LICENSE file. |
| /** |
| - * @fileoverview ChromeVox panel. |
| - * |
| + * @fileoverview The ChromeVox panel and menus. |
| */ |
| goog.provide('Panel'); |
| goog.require('Msgs'); |
| goog.require('PanelCommand'); |
| - |
| -function $(id) { |
| - return document.getElementById(id); |
| -} |
| +goog.require('PanelMenu'); |
| +goog.require('PanelMenuItem'); |
| +goog.require('cvox.ChromeVoxKbHandler'); |
| +goog.require('cvox.CommandStore'); |
| /** |
| * Class to manage the panel. |
| @@ -42,7 +41,41 @@ Panel.init = function() { |
| /** @type {Element} @private */ |
| this.brailleCellsElement_ = $('braille-cells'); |
| + /** |
| + * The array of top-level menus. |
| + * @type {!Array<PanelMenu>} |
| + * @private |
| + */ |
| + this.menus_ = []; |
| + |
| + /** |
| + * The currently active menu, if any. |
| + * @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.
|
| + * @private |
| + */ |
| + this.activeMenu_ = null; |
| + |
| + /** |
| + * 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.
|
| + * ChromeVox Next is not active. |
| + * @type {boolean} |
| + * @private |
| + */ |
| + this.menusEnabled_ = false; |
| + |
| + /** |
| + * A callback function to be executed to perform the action from selecting |
| + * a menu item after the menu has been closed and focus has been restored |
| + * to the page or wherever it was previously. |
| + * @type {?Function} |
| + * @private |
| + */ |
| + this.pendingCallback_ = null; |
| + |
| Panel.updateFromPrefs(); |
| + |
| + Msgs.addTranslatedMessagesToDom(document); |
| + |
| window.addEventListener('storage', function(event) { |
| if (event.key == 'brailleCaptions') { |
| Panel.updateFromPrefs(); |
| @@ -54,14 +87,12 @@ Panel.init = function() { |
| Panel.exec(/** @type {PanelCommand} */(command)); |
| }, false); |
| + $('menus_button').addEventListener('mousedown', Panel.onOpenMenus, false); |
| $('options').addEventListener('click', Panel.onOptions, false); |
| $('close').addEventListener('click', Panel.onClose, false); |
| - // The ChromeVox menu isn't fully implemented yet, disable it. |
| - $('menu').disabled = true; |
| - $('triangle').style.display = 'none'; |
| - |
| - Msgs.addTranslatedMessagesToDom(document); |
| + document.addEventListener('keydown', Panel.onKeyDown, false); |
| + document.addEventListener('mouseup', Panel.onMouseUp, false); |
| }; |
| /** |
| @@ -120,7 +151,296 @@ Panel.exec = function(command) { |
| this.brailleTextElement_.textContent = command.data.text; |
| this.brailleCellsElement_.textContent = command.data.braille; |
| break; |
| + case PanelCommandType.ENABLE_MENUS: |
| + Panel.onEnableMenus(); |
| + break; |
| + case PanelCommandType.DISABLE_MENUS: |
| + Panel.onDisableMenus(); |
| + break; |
| + case PanelCommandType.OPEN_MENUS: |
| + Panel.onOpenMenus(); |
| + break; |
| + } |
| +}; |
| + |
| +/** |
| + * Enable the ChromeVox Menus. |
| + */ |
| +Panel.onEnableMenus = function() { |
| + Panel.menusEnabled_ = true; |
| + $('menus_button').disabled = false; |
| + $('triangle').style.display = ''; |
| +}; |
| + |
| +/** |
| + * Disable the ChromeVox Menus. |
| + */ |
| +Panel.onDisableMenus = function() { |
| + Panel.menusEnabled_ = false; |
| + $('menus_button').disabled = true; |
| + $('triangle').style.display = 'none'; |
| +}; |
| + |
| +/** |
| + * Open / show the ChromeVox Menus. |
| + * @param {Event=} opt_event An optional event that triggered this. |
| + */ |
| +Panel.onOpenMenus = function(opt_event) { |
| + // Don't open the menu if it's not enabled, such as when ChromeVox Next |
| + // is not active. |
| + if (!Panel.menusEnabled_) |
| + return; |
| + |
| + // Eat the event so that a mousedown isn't turned into a drag, allowing |
| + // users to click-drag-release to select a menu item. |
| + if (opt_event) { |
| + opt_event.stopPropagation(); |
| + opt_event.preventDefault(); |
| + } |
| + |
| + // Change the url fragment to 'fullscreen', which signals the native |
| + // host code to make the window fullscreen, revealing the menus. |
| + window.location = '#fullscreen'; |
| + |
| + // Clear any existing menus and clear the callback. |
| + Panel.clearMenus(); |
| + Panel.pendingCallback_ = null; |
| + |
| + // Build the top-level menus. |
| + var jumpMenu = Panel.addMenu('Jump'); |
| + var speechMenu = Panel.addMenu('Speech'); |
| + var tabsMenu = Panel.addMenu('Tabs'); |
| + var chromevoxMenu = Panel.addMenu('ChromeVox'); |
| + |
| + // Create a mapping between categories from CommandStore, and our |
| + // 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
|
| + 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
|
| + 'navigation': jumpMenu, |
| + 'jump_commands': jumpMenu, |
| + 'controlling_speech': speechMenu, |
| + 'modifier_keys': chromevoxMenu, |
| + 'help_commands': chromevoxMenu, |
| + |
| + 'information': null, // Get link URL, get page title, etc. |
| + 'overview': null, // Headings list, etc. |
| + 'tables': null, // Table navigation. |
| + 'braille': null, |
| + 'developer': null}; |
| + |
| + // Get the key map from the background page. |
| + var bkgnd = chrome.extension.getBackgroundPage(); |
| + 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.
|
| + |
| + // Make a copy of the key bindings, get the localized title of each |
| + // command, and then sort them. |
| + var sortedBindings = keymap.bindings().slice(); |
| + sortedBindings.forEach(goog.bind(function(binding) { |
| + var command = binding.command; |
| + var keySeq = binding.sequence; |
| + binding.keySeq = cvox.KeyUtil.keySequenceToString(keySeq, true); |
| + var titleMsgId = cvox.CommandStore.messageForCommand(command); |
| + if (!titleMsgId) { |
| + console.log('No localization for: ' + command); |
| + binding.title = ''; |
| + return; |
| + } |
| + var title = Msgs.getMsg(titleMsgId); |
| + // Convert to title case. |
| + title = title.replace(/\w\S*/g, function(word) { |
| + return word.charAt(0).toUpperCase() + word.substr(1); |
| + }); |
| + binding.title = title; |
| + }, this)); |
| + sortedBindings.sort(function(binding1, binding2) { |
| + return binding1.title.localeCompare(binding2.title); |
| + }); |
| + |
| + // Insert items from the bindings into the menus. |
| + sortedBindings.forEach(goog.bind(function(binding) { |
| + var category = cvox.CommandStore.categoryForCommand(binding.command); |
| + var menu = category ? categoryToMenu[category] : null; |
| + if (binding.title && menu) { |
| + menu.addMenuItem( |
| + binding.title, |
| + binding.keySeq, |
| + function() { |
| + var bkgnd = |
| + chrome.extension.getBackgroundPage()['global']['backgroundObj']; |
| + bkgnd['onGotCommand'](binding.command); |
| + }); |
| + } |
| + }, this)); |
| + |
| + // Add all open tabs to the Tabs menu. |
| + bkgnd.chrome.windows.getAll({'populate': true}, function(windows) { |
| + for (var i = 0; i < windows.length; i++) { |
| + var tabs = windows[i].tabs; |
| + for (var j = 0; j < tabs.length; j++) { |
| + 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.
|
| + bkgnd.chrome.windows.update(win.id, {focused: true}, function() { |
| + bkgnd.chrome.tabs.update(tab.id, {active: true}); |
| + }); |
| + }).bind(this, windows[i], tabs[j])); |
| + } |
| + } |
| + }); |
| + |
| + // Add a menu item that disables / closes ChromeVox. |
| + chromevoxMenu.addMenuItem( |
| + Msgs.getMsg('disable_chromevox'), 'Ctrl+Alt+Z', function() { |
| + Panel.onClose(); |
| + }); |
| + |
| + // Activate the first menu. |
| + Panel.activateMenu(Panel.menus_[0]); |
| +}; |
| + |
| +/** |
| + * Clear any previous menus. The menus are all regenerated each time the |
| + * menus are opened. |
| + */ |
| +Panel.clearMenus = function() { |
| + while (this.menus_.length) { |
| + var menu = this.menus_.pop(); |
| + $('menu-bar').removeChild(menu.menuBarItemElement); |
| + $('menus_background').removeChild(menu.menuContainerElement); |
| } |
| + this.activeMenu_ = null; |
| +}; |
| + |
| +/** |
| + * Create a new menu with the given name and add it to the menu bar. |
| + * @param {string} menuTitle The title of the new menu to add. |
| + * @return {PanelMenu} The menu just created. |
| + */ |
| +Panel.addMenu = function(menuTitle) { |
| + var menu = new PanelMenu(menuTitle); |
| + $('menu-bar').appendChild(menu.menuBarItemElement); |
| + menu.menuBarItemElement.addEventListener('mouseover', function() { |
| + Panel.activateMenu(menu); |
| + }, false); |
| + |
| + $('menus_background').appendChild(menu.menuContainerElement); |
| + this.menus_.push(menu); |
| + return menu; |
| +}; |
| + |
| +/** |
| + * Activate a menu, which implies hiding the previous active menu. |
| + * @param {PanelMenu} menu The new menu to activate. |
| + */ |
| +Panel.activateMenu = function(menu) { |
| + if (menu == this.activeMenu_) |
| + return; |
| + |
| + if (this.activeMenu_) { |
| + this.activeMenu_.deactivate(); |
| + this.activeMenu_ = null; |
| + } |
| + |
| + this.activeMenu_ = menu; |
| + this.pendingCallback_ = null; |
| + |
| + if (this.activeMenu_) { |
| + this.activeMenu_.activate(); |
| + } |
| +}; |
| + |
| +/** |
| + * Advance the index of the current active menu by |delta|. |
| + * @param {number} delta The number to add to the active menu index. |
| + */ |
| +Panel.advanceActiveMenuBy = function(delta) { |
| + var activeIndex = -1; |
| + for (var i = 0; i < this.menus_.length; i++) { |
| + if (this.activeMenu_ == this.menus_[i]) { |
| + activeIndex = i; |
| + break; |
| + } |
| + } |
| + |
| + if (activeIndex >= 0) { |
| + activeIndex += delta; |
| + 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
|
| + } else { |
| + if (delta >= 0) |
| + activeIndex = 0; |
| + else |
| + activeIndex = this.menus_.length - 1; |
| + } |
| + Panel.activateMenu(this.menus_[activeIndex]); |
| +}; |
| + |
| +/** |
| + * Advance the index of the current active menu item by |delta|. |
| + * @param {number} delta The number to add to the active menu item index. |
| + */ |
| +Panel.advanceItemBy = function(delta) { |
| + if (this.activeMenu_) |
| + this.activeMenu_.advanceItemBy(delta); |
| +}; |
| + |
| +/** |
| + * Called when the user releases the mouse button. If it's anywhere other |
| + * than on the menus button, close the menus and return focus to the page, |
| + * and if the mouse was released over a menu item, execute that item's |
| + * callback. |
| + * @param {Event} event The mouse event. |
| + */ |
| +Panel.onMouseUp = function(event) { |
| + var target = event.target; |
| + while (target && !target.classList.contains('menu-item')) { |
| + // Allow the user to click and release on the menu button and leave |
| + // the menu button. Otherwise releasing the mouse anywhere else will |
| + // close the menu. |
| + if (target.id == 'menus_button') |
| + return; |
| + |
| + target = target.parentElement; |
| + } |
| + |
| + if (target && Panel.activeMenu_) |
| + Panel.pendingCallback_ = Panel.activeMenu_.getCallbackForElement(target); |
| + Panel.closeMenusAndRestoreFocus(); |
| +}; |
| + |
| +/** |
| + * Called when a key is pressed. Handle arrow keys to navigate the menus, |
| + * Esc to close, and Enter/Space to activate an item. |
| + * @param {Event} event The key event. |
| + */ |
| +Panel.onKeyDown = function(event) { |
| + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) |
| + return; |
| + |
| + switch (event.keyIdentifier) { |
| + case 'Left': |
| + Panel.advanceActiveMenuBy(-1); |
| + break; |
| + case 'Right': |
| + Panel.advanceActiveMenuBy(1); |
| + break; |
| + case 'Up': |
| + Panel.advanceItemBy(-1); |
| + break; |
| + case 'Down': |
| + Panel.advanceItemBy(1); |
| + break; |
| + case 'U+001B': // Escape |
| + Panel.closeMenusAndRestoreFocus(); |
| + break; |
| + case 'Enter': // Enter |
| + case 'U+0020': // Space |
| + Panel.pendingCallback_ = Panel.getCallbackForCurrentItem(); |
| + Panel.closeMenusAndRestoreFocus(); |
| + break; |
| + default: |
| + // Don't mark this event as handled. |
| + return; |
| + } |
| + |
| + event.preventDefault(); |
| + event.stopPropagation(); |
| }; |
| /** |
| @@ -140,6 +460,31 @@ Panel.onClose = function() { |
| window.location = '#close'; |
| }; |
| +/** |
| + * Get the callback for whatever item is currently selected. |
| + * @return {Function} The callback for the current item. |
| + */ |
| +Panel.getCallbackForCurrentItem = function() { |
| + if (this.activeMenu_) |
| + return this.activeMenu_.getCallbackForCurrentItem(); |
| + return null; |
| +}; |
| + |
| +/** |
| + * Close the menus and restore focus to the page. If a menu item's callback |
| + * was queued, execute it once focus is restored. |
| + */ |
| +Panel.closeMenusAndRestoreFocus = function() { |
| + // Make sure we're not in full-screen mode. |
| + window.location = '#'; |
| + |
| + var bkgnd = |
| + chrome.extension.getBackgroundPage()['global']['backgroundObj']; |
| + bkgnd['restoreCurrentRange'](); |
| + if (Panel.pendingCallback_) |
| + Panel.pendingCallback_(); |
| +}; |
| + |
| window.addEventListener('load', function() { |
| Panel.init(); |
| }, false); |