Bug 1246029 Implement chrome.commands.onCommand. r=kmag

This commit is contained in:
Matthew Wein 2016-02-29 19:04:03 -08:00
parent 2ece6215a9
commit 22fd2a0fc9
6 changed files with 285 additions and 30 deletions

View file

@ -2,36 +2,208 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://devtools/shared/event-emitter.js");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
PlatformInfo,
EventManager,
PlatformInfo,
} = 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();
function Command(description, shortcut) {
this.description = description;
this.shortcut = shortcut;
function CommandList(commandsObj, extensionID) {
this.commands = this.loadCommandsFromManifest(commandsObj);
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 */
extensions.on("manifest_commands", (type, directive, extension, manifest) => {
let commands = new Map();
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);
commandsMap.set(extension, new CommandList(manifest.commands, extension.id));
});
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 */
@ -39,15 +211,24 @@ extensions.registerSchemaAPI("commands", null, (extension, context) => {
return {
commands: {
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 ({
name,
description: command.description,
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(),
},
};
});

View file

@ -107,7 +107,6 @@
"events": [
{
"name": "onCommand",
"unsupported": true,
"description": "Fired when a registered command is activated using a keyboard shortcut.",
"type": "function",
"parameters": [

View file

@ -11,7 +11,6 @@ support-files =
file_iframe_document.sjs
[browser_ext_simple.js]
[browser_ext_commands.js]
[browser_ext_currentWindow.js]
[browser_ext_browserAction_simple.js]
[browser_ext_browserAction_pageAction_icon.js]
@ -22,6 +21,8 @@ support-files =
[browser_ext_browserAction_popup.js]
[browser_ext_popup_api_injection.js]
[browser_ext_contextMenus.js]
[browser_ext_commands_getAll.js]
[browser_ext_commands_onCommand.js]
[browser_ext_getViews.js]
[browser_ext_lastError.js]
[browser_ext_runtime_setUninstallURL.js]

View file

@ -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);
});

View file

@ -986,23 +986,23 @@ function detectLanguage(text) {
}
this.ExtensionUtils = {
runSafeWithoutClone,
runSafeSyncWithoutClone,
detectLanguage,
extend,
flushJarCache,
ignoreEvent,
injectAPI,
instanceOf,
runSafe,
runSafeSync,
runSafeSyncWithoutClone,
runSafeWithoutClone,
BaseContext,
DefaultWeakMap,
EventManager,
LocaleData,
SingletonEventManager,
ignoreEvent,
injectAPI,
MessageBroker,
Messenger,
PlatformInfo,
SingletonEventManager,
SpreadArgs,
extend,
flushJarCache,
instanceOf,
detectLanguage,
};