forked from mirrors/gecko-dev
		
	Bug 1246029 Implement chrome.commands.onCommand. r=kmag
This commit is contained in:
		
							parent
							
								
									2ece6215a9
								
							
						
					
					
						commit
						22fd2a0fc9
					
				
					 6 changed files with 285 additions and 30 deletions
				
			
		|  | @ -2,36 +2,208 @@ | ||||||
| /* vim: set sts=2 sw=2 et tw=80: */ | /* vim: set sts=2 sw=2 et tw=80: */ | ||||||
| "use strict"; | "use strict"; | ||||||
| 
 | 
 | ||||||
|  | Cu.import("resource://devtools/shared/event-emitter.js"); | ||||||
| Cu.import("resource://gre/modules/ExtensionUtils.jsm"); | Cu.import("resource://gre/modules/ExtensionUtils.jsm"); | ||||||
| 
 | 
 | ||||||
| var { | var { | ||||||
|    PlatformInfo, |   EventManager, | ||||||
|  |   PlatformInfo, | ||||||
| } = ExtensionUtils; | } = ExtensionUtils; | ||||||
| 
 | 
 | ||||||
| // WeakMap[Extension -> Map[name => Command]]
 | const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; | ||||||
|  | 
 | ||||||
|  | // WeakMap[Extension -> CommandList]
 | ||||||
| var commandsMap = new WeakMap(); | var commandsMap = new WeakMap(); | ||||||
| 
 | 
 | ||||||
| function Command(description, shortcut) { | function CommandList(commandsObj, extensionID) { | ||||||
|   this.description = description; |   this.commands = this.loadCommandsFromManifest(commandsObj); | ||||||
|   this.shortcut = shortcut; |   this.keysetID = `ext-keyset-id-${makeWidgetId(extensionID)}`; | ||||||
|  |   this.windowOpenListener = null; | ||||||
|  |   this.register(); | ||||||
|  |   EventEmitter.decorate(this); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | CommandList.prototype = { | ||||||
|  |   /** | ||||||
|  |    * Registers the commands to all open windows and to any which | ||||||
|  |    * are later created. | ||||||
|  |    */ | ||||||
|  |   register() { | ||||||
|  |     for (let window of WindowListManager.browserWindows()) { | ||||||
|  |       this.registerKeysToDocument(window.document); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.windowOpenListener = (window) => { | ||||||
|  |       this.registerKeysToDocument(window.document); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     WindowListManager.addOpenListener(this.windowOpenListener); | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Unregisters the commands from all open windows and stops commands | ||||||
|  |    * from being registered to windows which are later created. | ||||||
|  |    */ | ||||||
|  |   unregister() { | ||||||
|  |     for (let window of WindowListManager.browserWindows()) { | ||||||
|  |       let keyset = window.document.getElementById(this.keysetID); | ||||||
|  |       if (keyset) { | ||||||
|  |         keyset.remove(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     WindowListManager.removeOpenListener(this.windowOpenListener); | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Creates a Map from commands for each command in the manifest.commands object. | ||||||
|  |    * @param {Object} commandsObj The manifest.commands JSON object. | ||||||
|  |    */ | ||||||
|  |   loadCommandsFromManifest(commandsObj) { | ||||||
|  |     let commands = new Map(); | ||||||
|  |     // For Windows, chrome.runtime expects 'win' while chrome.commands
 | ||||||
|  |     // expects 'windows'.  We can special case this for now.
 | ||||||
|  |     let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os; | ||||||
|  |     for (let name of Object.keys(commandsObj)) { | ||||||
|  |       let command = commandsObj[name]; | ||||||
|  |       commands.set(name, { | ||||||
|  |         description: command.description, | ||||||
|  |         shortcut: command.suggested_key[os] || command.suggested_key.default, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     return commands; | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Registers the commands to a document. | ||||||
|  |    * @param {Document} doc The XUL document to insert the Keyset. | ||||||
|  |    */ | ||||||
|  |   registerKeysToDocument(doc) { | ||||||
|  |     let keyset = doc.createElementNS(XUL_NS, "keyset"); | ||||||
|  |     keyset.id = this.keysetID; | ||||||
|  |     this.commands.forEach((command, name) => { | ||||||
|  |       let keyElement = this.buildKey(doc, name, command.shortcut); | ||||||
|  |       keyset.appendChild(keyElement); | ||||||
|  |     }); | ||||||
|  |     doc.documentElement.appendChild(keyset); | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Builds a XUL Key element and attaches an onCommand listener which | ||||||
|  |    * emits a command event with the provided name when fired. | ||||||
|  |    * | ||||||
|  |    * @param {Document} doc The XUL document. | ||||||
|  |    * @param {String} name The name of the command. | ||||||
|  |    * @param {String} shortcut The shortcut provided in the manifest. | ||||||
|  |    * @see https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/key
 | ||||||
|  |    * | ||||||
|  |    * @returns {Document} The newly created Key element. | ||||||
|  |    */ | ||||||
|  |   buildKey(doc, name, shortcut) { | ||||||
|  |     let keyElement = this.buildKeyFromShortcut(doc, shortcut); | ||||||
|  | 
 | ||||||
|  |     // We need to have the attribute "oncommand" for the "command" listener to fire,
 | ||||||
|  |     // and it is currently ignored when set to the empty string.
 | ||||||
|  |     keyElement.setAttribute("oncommand", "//"); | ||||||
|  | 
 | ||||||
|  |     /* eslint-disable mozilla/balanced-listeners */ | ||||||
|  |     // We remove all references to the key elements when the extension is shutdown,
 | ||||||
|  |     // therefore the listeners for these elements will be garbage collected.
 | ||||||
|  |     keyElement.addEventListener("command", (event) => { | ||||||
|  |       this.emit("command", name); | ||||||
|  |     }); | ||||||
|  |     /* eslint-enable mozilla/balanced-listeners */ | ||||||
|  | 
 | ||||||
|  |     return keyElement; | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Builds a XUL Key element from the provided shortcut. | ||||||
|  |    * | ||||||
|  |    * @param {Document} doc The XUL document. | ||||||
|  |    * @param {String} name The name of the command. | ||||||
|  |    * @param {String} shortcut The shortcut provided in the manifest. | ||||||
|  |    * | ||||||
|  |    * @see https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/key
 | ||||||
|  |    * @returns {Document} The newly created Key element. | ||||||
|  |    */ | ||||||
|  |   buildKeyFromShortcut(doc, shortcut) { | ||||||
|  |     let keyElement = doc.createElementNS(XUL_NS, "key"); | ||||||
|  | 
 | ||||||
|  |     let parts = shortcut.split("+"); | ||||||
|  | 
 | ||||||
|  |     // The key is always the last element.
 | ||||||
|  |     let chromeKey = parts.pop(); | ||||||
|  | 
 | ||||||
|  |     // The modifiers are the remaining elements.
 | ||||||
|  |     keyElement.setAttribute("modifiers", this.getModifiersAttribute(parts)); | ||||||
|  | 
 | ||||||
|  |     if (/^[A-Z0-9]$/.test(chromeKey)) { | ||||||
|  |       // We use the key attribute for all single digits and characters.
 | ||||||
|  |       keyElement.setAttribute("key", chromeKey); | ||||||
|  |     } else { | ||||||
|  |       keyElement.setAttribute("keycode", this.getKeycodeAttribute(chromeKey)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return keyElement; | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Determines the corresponding XUL keycode from the given chrome key. | ||||||
|  |    * | ||||||
|  |    * For example: | ||||||
|  |    * | ||||||
|  |    *    input     |  output | ||||||
|  |    *    --------------------------------------- | ||||||
|  |    *    "PageUP"  |  "VK_PAGE_UP" | ||||||
|  |    *    "Delete"  |  "VK_DELETE" | ||||||
|  |    * | ||||||
|  |    * @param {String} key The chrome key (e.g. "PageUp", "Space", ...) | ||||||
|  |    * @return The constructed value for the Key's 'keycode' attribute. | ||||||
|  |    */ | ||||||
|  |   getKeycodeAttribute(chromeKey) { | ||||||
|  |     return `VK${chromeKey.replace(/([A-Z])/g, "_$&").toUpperCase()}`; | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Determines the corresponding XUL modifiers from the chrome modifiers. | ||||||
|  |    * | ||||||
|  |    * For example: | ||||||
|  |    * | ||||||
|  |    *    input             |   output | ||||||
|  |    *    --------------------------------------- | ||||||
|  |    *    ["Ctrl", "Shift"] |   "accel shift" | ||||||
|  |    *    ["MacCtrl"]       |   "control" | ||||||
|  |    * | ||||||
|  |    * @param {Array} chromeModifiers The array of chrome modifiers. | ||||||
|  |    * @return The constructed value for the Key's 'modifiers' attribute. | ||||||
|  |    */ | ||||||
|  |   getModifiersAttribute(chromeModifiers) { | ||||||
|  |     let modifiersMap = { | ||||||
|  |       "Alt"     : "alt", | ||||||
|  |       "Command" : "accel", | ||||||
|  |       "Ctrl"    : "accel", | ||||||
|  |       "MacCtrl" : "control", | ||||||
|  |       "Shift"   : "shift", | ||||||
|  |     }; | ||||||
|  |     return Array.from(chromeModifiers, modifier => { | ||||||
|  |       return modifiersMap[modifier]; | ||||||
|  |     }).join(" "); | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| /* eslint-disable mozilla/balanced-listeners */ | /* eslint-disable mozilla/balanced-listeners */ | ||||||
| extensions.on("manifest_commands", (type, directive, extension, manifest) => { | extensions.on("manifest_commands", (type, directive, extension, manifest) => { | ||||||
|   let commands = new Map(); |   commandsMap.set(extension, new CommandList(manifest.commands, extension.id)); | ||||||
|   for (let name of Object.keys(manifest.commands)) { |  | ||||||
|     let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os; |  | ||||||
|     let manifestCommand = manifest.commands[name]; |  | ||||||
|     let description = manifestCommand.description; |  | ||||||
|     let shortcut = manifestCommand.suggested_key[os] || manifestCommand.suggested_key.default; |  | ||||||
|     let command = new Command(description, shortcut); |  | ||||||
|     commands.set(name, command); |  | ||||||
|   } |  | ||||||
|   commandsMap.set(extension, commands); |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| extensions.on("shutdown", (type, extension) => { | extensions.on("shutdown", (type, extension) => { | ||||||
|   commandsMap.delete(extension); |   let commandsList = commandsMap.get(extension); | ||||||
|  |   if (commandsList) { | ||||||
|  |     commandsList.unregister(); | ||||||
|  |     commandsMap.delete(extension); | ||||||
|  |   } | ||||||
| }); | }); | ||||||
| /* eslint-enable mozilla/balanced-listeners */ | /* eslint-enable mozilla/balanced-listeners */ | ||||||
| 
 | 
 | ||||||
|  | @ -39,15 +211,24 @@ extensions.registerSchemaAPI("commands", null, (extension, context) => { | ||||||
|   return { |   return { | ||||||
|     commands: { |     commands: { | ||||||
|       getAll() { |       getAll() { | ||||||
|         let commands = Array.from(commandsMap.get(extension), ([name, command]) => { |         let commands = commandsMap.get(extension).commands; | ||||||
|  |         return Promise.resolve(Array.from(commands, ([name, command]) => { | ||||||
|           return ({ |           return ({ | ||||||
|             name, |             name, | ||||||
|             description: command.description, |             description: command.description, | ||||||
|             shortcut: command.shortcut, |             shortcut: command.shortcut, | ||||||
|           }); |           }); | ||||||
|         }); |         })); | ||||||
|         return Promise.resolve(commands); |  | ||||||
|       }, |       }, | ||||||
|  |       onCommand: new EventManager(context, "commands.onCommand", fire => { | ||||||
|  |         let listener = (event, name) => { | ||||||
|  |           fire(name); | ||||||
|  |         }; | ||||||
|  |         commandsMap.get(extension).on("command", listener); | ||||||
|  |         return () => { | ||||||
|  |           commandsMap.get(extension).off("command", listener); | ||||||
|  |         }; | ||||||
|  |       }).api(), | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -107,7 +107,6 @@ | ||||||
|     "events": [ |     "events": [ | ||||||
|       { |       { | ||||||
|         "name": "onCommand", |         "name": "onCommand", | ||||||
|         "unsupported": true, |  | ||||||
|         "description": "Fired when a registered command is activated using a keyboard shortcut.", |         "description": "Fired when a registered command is activated using a keyboard shortcut.", | ||||||
|         "type": "function", |         "type": "function", | ||||||
|         "parameters": [ |         "parameters": [ | ||||||
|  |  | ||||||
|  | @ -11,7 +11,6 @@ support-files = | ||||||
|   file_iframe_document.sjs |   file_iframe_document.sjs | ||||||
| 
 | 
 | ||||||
| [browser_ext_simple.js] | [browser_ext_simple.js] | ||||||
| [browser_ext_commands.js] |  | ||||||
| [browser_ext_currentWindow.js] | [browser_ext_currentWindow.js] | ||||||
| [browser_ext_browserAction_simple.js] | [browser_ext_browserAction_simple.js] | ||||||
| [browser_ext_browserAction_pageAction_icon.js] | [browser_ext_browserAction_pageAction_icon.js] | ||||||
|  | @ -22,6 +21,8 @@ support-files = | ||||||
| [browser_ext_browserAction_popup.js] | [browser_ext_browserAction_popup.js] | ||||||
| [browser_ext_popup_api_injection.js] | [browser_ext_popup_api_injection.js] | ||||||
| [browser_ext_contextMenus.js] | [browser_ext_contextMenus.js] | ||||||
|  | [browser_ext_commands_getAll.js] | ||||||
|  | [browser_ext_commands_onCommand.js] | ||||||
| [browser_ext_getViews.js] | [browser_ext_getViews.js] | ||||||
| [browser_ext_lastError.js] | [browser_ext_lastError.js] | ||||||
| [browser_ext_runtime_setUninstallURL.js] | [browser_ext_runtime_setUninstallURL.js] | ||||||
|  |  | ||||||
|  | @ -0,0 +1,74 @@ | ||||||
|  | /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ | ||||||
|  | /* vim: set sts=2 sw=2 et tw=80: */ | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | add_task(function* () { | ||||||
|  |   // Create a window before the extension is loaded.
 | ||||||
|  |   let win1 = yield BrowserTestUtils.openNewBrowserWindow(); | ||||||
|  |   yield BrowserTestUtils.loadURI(win1.gBrowser.selectedBrowser, "about:robots"); | ||||||
|  |   yield BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser); | ||||||
|  | 
 | ||||||
|  |   let extension = ExtensionTestUtils.loadExtension({ | ||||||
|  |     manifest: { | ||||||
|  |       "name": "Commands Extension", | ||||||
|  |       "commands": { | ||||||
|  |         "toggle-feature-using-alt-shift-3": { | ||||||
|  |           "suggested_key": { | ||||||
|  |             "default": "Alt+Shift+3", | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         "toggle-feature-using-alt-shift-comma": { | ||||||
|  |           "suggested_key": { | ||||||
|  |             "default": "Alt+Shift+Comma", | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     background: function() { | ||||||
|  |       browser.commands.onCommand.addListener((message) => { | ||||||
|  |         browser.test.sendMessage("oncommand", message); | ||||||
|  |       }); | ||||||
|  |       browser.test.sendMessage("ready"); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   yield extension.startup(); | ||||||
|  |   yield extension.awaitMessage("ready"); | ||||||
|  | 
 | ||||||
|  |   // Create another window after the extension is loaded.
 | ||||||
|  |   let win2 = yield BrowserTestUtils.openNewBrowserWindow(); | ||||||
|  |   yield BrowserTestUtils.loadURI(win2.gBrowser.selectedBrowser, "about:config"); | ||||||
|  |   yield BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser); | ||||||
|  | 
 | ||||||
|  |   // Confirm the keysets have been added to both windows.
 | ||||||
|  |   let keysetID = `ext-keyset-id-${makeWidgetId(extension.id)}`; | ||||||
|  |   let keyset = win1.document.getElementById(keysetID); | ||||||
|  |   is(keyset.childNodes.length, 2, "Expected keyset to exist and have 2 children"); | ||||||
|  | 
 | ||||||
|  |   keyset = win2.document.getElementById(keysetID); | ||||||
|  |   is(keyset.childNodes.length, 2, "Expected keyset to exist and have 2 children"); | ||||||
|  | 
 | ||||||
|  |   // Confirm that the commands are registered to both windows.
 | ||||||
|  |   yield focusWindow(win1); | ||||||
|  |   EventUtils.synthesizeKey("3", {altKey: true, shiftKey: true}); | ||||||
|  |   let message = yield extension.awaitMessage("oncommand"); | ||||||
|  |   is(message, "toggle-feature-using-alt-shift-3", "Expected onCommand listener to fire with correct message"); | ||||||
|  | 
 | ||||||
|  |   yield focusWindow(win2); | ||||||
|  |   EventUtils.synthesizeKey("VK_COMMA", {altKey: true, shiftKey: true}); | ||||||
|  |   message = yield extension.awaitMessage("oncommand"); | ||||||
|  |   is(message, "toggle-feature-using-alt-shift-comma", "Expected onCommand listener to fire with correct message"); | ||||||
|  | 
 | ||||||
|  |   yield extension.unload(); | ||||||
|  | 
 | ||||||
|  |   // Confirm that the keysets have been removed from both windows after the extension is unloaded.
 | ||||||
|  |   keyset = win1.document.getElementById(keysetID); | ||||||
|  |   is(keyset, null, "Expected keyset to be removed from the window"); | ||||||
|  | 
 | ||||||
|  |   keyset = win2.document.getElementById(keysetID); | ||||||
|  |   is(keyset, null, "Expected keyset to be removed from the window"); | ||||||
|  | 
 | ||||||
|  |   yield BrowserTestUtils.closeWindow(win1); | ||||||
|  |   yield BrowserTestUtils.closeWindow(win2); | ||||||
|  | }); | ||||||
|  | @ -986,23 +986,23 @@ function detectLanguage(text) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| this.ExtensionUtils = { | this.ExtensionUtils = { | ||||||
|   runSafeWithoutClone, |   detectLanguage, | ||||||
|   runSafeSyncWithoutClone, |   extend, | ||||||
|  |   flushJarCache, | ||||||
|  |   ignoreEvent, | ||||||
|  |   injectAPI, | ||||||
|  |   instanceOf, | ||||||
|   runSafe, |   runSafe, | ||||||
|   runSafeSync, |   runSafeSync, | ||||||
|  |   runSafeSyncWithoutClone, | ||||||
|  |   runSafeWithoutClone, | ||||||
|   BaseContext, |   BaseContext, | ||||||
|   DefaultWeakMap, |   DefaultWeakMap, | ||||||
|   EventManager, |   EventManager, | ||||||
|   LocaleData, |   LocaleData, | ||||||
|   SingletonEventManager, |  | ||||||
|   ignoreEvent, |  | ||||||
|   injectAPI, |  | ||||||
|   MessageBroker, |   MessageBroker, | ||||||
|   Messenger, |   Messenger, | ||||||
|   PlatformInfo, |   PlatformInfo, | ||||||
|  |   SingletonEventManager, | ||||||
|   SpreadArgs, |   SpreadArgs, | ||||||
|   extend, |  | ||||||
|   flushJarCache, |  | ||||||
|   instanceOf, |  | ||||||
|   detectLanguage, |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Matthew Wein
						Matthew Wein