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 {
|
||||||
|
EventManager,
|
||||||
PlatformInfo,
|
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) => {
|
||||||
|
let commandsList = commandsMap.get(extension);
|
||||||
|
if (commandsList) {
|
||||||
|
commandsList.unregister();
|
||||||
commandsMap.delete(extension);
|
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