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..149f353f7f134443bba055fe54b644b09556dada 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} |
+ * @private |
+ */ |
+ this.activeMenu_ = null; |
+ |
+ /** |
+ * True if the menu button in the panel is enabled at all. It's disabled if |
+ * 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,301 @@ 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. |
+ var categoryToMenu = { |
+ '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']['KeyMap']['fromCurrentKeyMap'](); |
+ |
+ // 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.error('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.getLastFocused(function(lastFocusedWindow) { |
+ 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++) { |
+ var title = tabs[j].title; |
+ if (tabs[j].active && windows[i].id == lastFocusedWindow.id) |
+ title += ' ' + Msgs.getMsg('active_tab'); |
+ tabsMenu.addMenuItem(title, '', (function(win, tab) { |
+ 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; |
+ } 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 +465,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); |