--- a/browser/components/extensions/parent/ext-browserAction.js
+++ b/browser/components/extensions/parent/ext-browserAction.js
@@ -62,16 +62,17 @@ this.browserAction = class extends Exten
this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
this.widget = null;
this.pendingPopup = null;
this.pendingPopupTimeout = null;
this.eventQueue = [];
this.tabManager = extension.tabManager;
+ extension.browserAction = this;
this.defaults = {
enabled: true,
title: options.default_title || extension.name,
badgeText: "",
badgeBackgroundColor: null,
badgeTextColor: null,
popup: options.default_popup || "",
--- a/browser/components/extensions/parent/ext-commands.js
+++ b/browser/components/extensions/parent/ext-commands.js
@@ -1,367 +1,48 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
-ChromeUtils.defineModuleGetter(this, "ExtensionParent",
- "resource://gre/modules/ExtensionParent.jsm");
-ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
- "resource://gre/modules/ExtensionSettingsStore.jsm");
-
-var {
- chromeModifierKeyMap,
- ExtensionError,
-} = ExtensionUtils;
-
-var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-const EXECUTE_PAGE_ACTION = "_execute_page_action";
-const EXECUTE_BROWSER_ACTION = "_execute_browser_action";
-const EXECUTE_SIDEBAR_ACTION = "_execute_sidebar_action";
-
-function normalizeShortcut(shortcut) {
- return shortcut ? shortcut.replace(/\s+/g, "") : null;
-}
+ChromeUtils.defineModuleGetter(this, "ExtensionShortcuts",
+ "resource://gre/modules/ExtensionShortcuts.jsm");
this.commands = class extends ExtensionAPI {
- static async onUninstall(extensionId) {
+ static onUninstall(extensionId) {
// Cleanup the updated commands. In some cases the extension is installed
// and uninstalled so quickly that `this.commands` hasn't loaded yet. To
// handle that we need to make sure ExtensionSettingsStore is initialized
// before we clean it up.
- await ExtensionSettingsStore.initialize();
- ExtensionSettingsStore
- .getAllForExtension(extensionId, "commands")
- .forEach(key => {
- ExtensionSettingsStore.removeSetting(extensionId, "commands", key);
- });
+ return ExtensionShortcuts.removeCommandsFromStorage(extensionId);
}
async onManifestEntry(entryName) {
- let {extension} = this;
-
- this.id = makeWidgetId(extension.id);
- this.windowOpenListener = null;
-
- // Map[{String} commandName -> {Object} commandProperties]
- this.manifestCommands = this.loadCommandsFromManifest(extension.manifest);
-
- this.commands = new Promise(async (resolve) => {
- // Deep copy the manifest commands to commands so we can keep the original
- // manifest commands and update commands as needed.
- let commands = new Map();
- this.manifestCommands.forEach((command, name) => {
- commands.set(name, {...command});
- });
-
- // Update the manifest commands with the persisted updates from
- // browser.commands.update().
- let savedCommands = await this.loadCommandsFromStorage(extension.id);
- savedCommands.forEach((update, name) => {
- let command = commands.get(name);
- if (command) {
- // We will only update commands, not add them.
- Object.assign(command, update);
- }
- });
-
- resolve(commands);
+ this.shortcuts = new ExtensionShortcuts({
+ extension: this.extension,
+ onCommand: (name) => this.emit("command", name),
});
-
- // WeakMap[Window -> <xul:keyset>]
- this.keysetsMap = new WeakMap();
-
- await this.register();
+ this.extension.shortcuts = this.shortcuts;
+ await this.shortcuts.loadAndRegister();
}
onShutdown(reason) {
- this.unregister();
- }
-
- registerKeys(commands) {
- for (let window of windowTracker.browserWindows()) {
- this.registerKeysToDocument(window, commands);
- }
- }
-
- /**
- * Registers the commands to all open windows and to any which
- * are later created.
- */
- async register() {
- let commands = await this.commands;
- this.registerKeys(commands);
-
- this.windowOpenListener = (window) => {
- if (!this.keysetsMap.has(window)) {
- this.registerKeysToDocument(window, commands);
- }
- };
-
- windowTracker.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 windowTracker.browserWindows()) {
- if (this.keysetsMap.has(window)) {
- this.keysetsMap.get(window).remove();
- }
- }
-
- windowTracker.removeOpenListener(this.windowOpenListener);
- }
-
- /**
- * Creates a Map from commands for each command in the manifest.commands object.
- *
- * @param {Object} manifest The manifest JSON object.
- * @returns {Map<string, object>}
- */
- loadCommandsFromManifest(manifest) {
- let commands = new Map();
- // For Windows, chrome.runtime expects 'win' while chrome.commands
- // expects 'windows'. We can special case this for now.
- let {PlatformInfo} = ExtensionParent;
- let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os;
- for (let [name, command] of Object.entries(manifest.commands)) {
- let suggested_key = command.suggested_key || {};
- let shortcut = normalizeShortcut(suggested_key[os] || suggested_key.default);
- commands.set(name, {
- description: command.description,
- shortcut,
- });
- }
- return commands;
- }
-
- async loadCommandsFromStorage(extensionId) {
- await ExtensionSettingsStore.initialize();
- let names = ExtensionSettingsStore.getAllForExtension(extensionId, "commands");
- return names.reduce((map, name) => {
- let command = ExtensionSettingsStore.getSetting(
- "commands", name, extensionId).value;
- return map.set(name, command);
- }, new Map());
- }
-
- /**
- * Registers the commands to a document.
- * @param {ChromeWindow} window The XUL window to insert the Keyset.
- * @param {Map} commands The commands to be set.
- */
- registerKeysToDocument(window, commands) {
- let doc = window.document;
- let keyset = doc.createElementNS(XUL_NS, "keyset");
- keyset.id = `ext-keyset-id-${this.id}`;
- if (this.keysetsMap.has(window)) {
- this.keysetsMap.get(window).remove();
- }
- let sidebarKey;
- commands.forEach((command, name) => {
- if (command.shortcut) {
- let keyElement = this.buildKey(doc, name, command.shortcut);
- keyset.appendChild(keyElement);
- if (name == EXECUTE_SIDEBAR_ACTION) {
- sidebarKey = keyElement;
- }
- }
- });
- doc.documentElement.appendChild(keyset);
- if (sidebarKey) {
- window.SidebarUI.updateShortcut({key: sidebarKey});
- }
- this.keysetsMap.set(window, 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, name, 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) => {
- let action;
- if (name == EXECUTE_PAGE_ACTION) {
- action = pageActionFor(this.extension);
- } else if (name == EXECUTE_BROWSER_ACTION) {
- action = browserActionFor(this.extension);
- } else if (name == EXECUTE_SIDEBAR_ACTION) {
- action = sidebarActionFor(this.extension);
- } else {
- this.extension.tabManager
- .addActiveTabPermission();
- this.emit("command", name);
- return;
- }
- if (action) {
- let win = event.target.ownerGlobal;
- action.triggerAction(win);
- }
- });
- /* 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 shortcut.
- * @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, name, 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 (name == EXECUTE_SIDEBAR_ACTION) {
- let id = `ext-key-id-${this.id}-sidebar-action`;
- keyElement.setAttribute("id", id);
- }
-
- if (/^[A-Z]$/.test(chromeKey)) {
- // We use the key attribute for all single digits and characters.
- keyElement.setAttribute("key", chromeKey);
- } else {
- keyElement.setAttribute("keycode", this.getKeycodeAttribute(chromeKey));
- keyElement.setAttribute("event", "keydown");
- }
-
- 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} chromeKey The chrome key (e.g. "PageUp", "Space", ...)
- * @returns {string} The constructed value for the Key's 'keycode' attribute.
- */
- getKeycodeAttribute(chromeKey) {
- if (/[0-9]/.test(chromeKey)) {
- return `VK_${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.
- * @returns {string} The constructed value for the Key's 'modifiers' attribute.
- */
- getModifiersAttribute(chromeModifiers) {
- return Array.from(chromeModifiers, modifier => {
- return chromeModifierKeyMap[modifier];
- }).join(" ");
+ this.shortcuts.unregister();
}
getAPI(context) {
return {
commands: {
- getAll: async () => {
- let commands = await this.commands;
- return Array.from(commands, ([name, command]) => {
- return ({
- name,
- description: command.description,
- shortcut: command.shortcut,
- });
- });
+ getAll: () => {
+ return this.shortcuts.allCommands();
},
- update: async ({name, description, shortcut}) => {
- let {extension} = this;
- let commands = await this.commands;
- let command = commands.get(name);
-
- if (!command) {
- throw new ExtensionError(`Unknown command "${name}"`);
- }
-
- // Only store the updates so manifest changes can take precedence
- // later.
- let previousUpdates = await ExtensionSettingsStore.getSetting(
- "commands", name, extension.id);
- let commandUpdates = (previousUpdates && previousUpdates.value) || {};
-
- if (description && description != command.description) {
- commandUpdates.description = description;
- command.description = description;
- }
-
- if (shortcut && shortcut != command.shortcut) {
- shortcut = normalizeShortcut(shortcut);
- commandUpdates.shortcut = shortcut;
- command.shortcut = shortcut;
- }
-
- await ExtensionSettingsStore.addSetting(
- extension.id, "commands", name, commandUpdates);
-
- this.registerKeys(commands);
+ update: (args) => {
+ return this.shortcuts.updateCommand(args);
},
- reset: async (name) => {
- let {extension, manifestCommands} = this;
- let commands = await this.commands;
- let command = commands.get(name);
-
- if (!command) {
- throw new ExtensionError(`Unknown command "${name}"`);
- }
-
- let storedCommand = ExtensionSettingsStore.getSetting(
- "commands", name, extension.id);
-
- if (storedCommand && storedCommand.value) {
- commands.set(name, {...manifestCommands.get(name)});
- ExtensionSettingsStore.removeSetting(extension.id, "commands", name);
- this.registerKeys(commands);
- }
+ reset: (name) => {
+ return this.shortcuts.resetCommand(name);
},
onCommand: new EventManager({
context,
name: "commands.onCommand",
register: fire => {
let listener = (eventName, commandName) => {
fire.async(commandName);
};
--- a/browser/components/extensions/parent/ext-pageAction.js
+++ b/browser/components/extensions/parent/ext-pageAction.js
@@ -34,16 +34,17 @@ this.pageAction = class extends Extensio
async onManifestEntry(entryName) {
let {extension} = this;
let options = extension.manifest.page_action;
let widgetId = makeWidgetId(extension.id);
this.id = widgetId + "-page-action";
this.tabManager = extension.tabManager;
+ extension.pageAction = this;
// `show` can have three different values:
// - `false`. This means the page action is not shown.
// It's set as default if show_matches is empty. Can also be set in a tab via
// `pageAction.hide(tabId)`, e.g. in order to override show_matches.
// - `true`. This means the page action is shown.
// It's never set as default because <all_urls> doesn't really match all URLs
// (e.g. "about:" URLs). But can be set in a tab via `pageAction.show(tabId)`.
--- a/browser/components/extensions/parent/ext-sidebarAction.js
+++ b/browser/components/extensions/parent/ext-sidebarAction.js
@@ -25,16 +25,19 @@ const sidebarURL = "chrome://browser/con
*/
this.sidebarAction = class extends ExtensionAPI {
static for(extension) {
return sidebarActionMap.get(extension);
}
onManifestEntry(entryName) {
let {extension} = this;
+ // FIXME: This is probably bad.
+ // TODO: Listen to an EXECUTE_SIDEBAR_ACTION event to execute action.
+ extension.sidebarAction = this;
extension.once("ready", this.onReady.bind(this));
let options = extension.manifest.sidebar_action;
// Add the extension to the sidebar menu. The sidebar widget will copy
// from that when it is viewed, so we shouldn't need to update that.
let widgetId = makeWidgetId(extension.id);
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/ExtensionShortcuts.jsm
@@ -0,0 +1,364 @@
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+
+ChromeUtils.defineModuleGetter(this, "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
+ "resource://gre/modules/ExtensionSettingsStore.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionUtils",
+ "resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "windowTracker", () => {
+ return ExtensionParent.apiManager.global.windowTracker;
+});
+
+const {
+ chromeModifierKeyMap,
+ ExtensionError,
+} = ExtensionUtils;
+const { makeWidgetId } = ExtensionCommon;
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const EXECUTE_PAGE_ACTION = "_execute_page_action";
+const EXECUTE_BROWSER_ACTION = "_execute_browser_action";
+const EXECUTE_SIDEBAR_ACTION = "_execute_sidebar_action";
+const EXPORTED_SYMBOLS = ["ExtensionShortcuts"];
+
+function normalizeShortcut(shortcut) {
+ return shortcut ? shortcut.replace(/\s+/g, "") : null;
+}
+
+class ExtensionShortcuts {
+ static async removeCommandsFromStorage(extensionId) {
+ await ExtensionSettingsStore.initialize();
+ ExtensionSettingsStore
+ .getAllForExtension(extensionId, "commands")
+ .forEach(key => {
+ ExtensionSettingsStore.removeSetting(extensionId, "commands", key);
+ });
+ }
+
+ constructor({extension, onCommand}) {
+ this.keysetsMap = new WeakMap();
+ this.windowOpenListener = null;
+ this.extension = extension;
+ this.onCommand = onCommand;
+ this.id = makeWidgetId(extension.id);
+ }
+
+ async allCommands() {
+ let commands = await this.commands;
+ return Array.from(commands, ([name, command]) => {
+ return ({
+ name,
+ description: command.description,
+ shortcut: command.shortcut,
+ });
+ });
+ }
+
+ async updateCommand({name, description, shortcut}) {
+ let {extension} = this;
+ let commands = await this.commands;
+ let command = commands.get(name);
+
+ if (!command) {
+ throw new ExtensionError(`Unknown command "${name}"`);
+ }
+
+ // Only store the updates so manifest changes can take precedence
+ // later.
+ let previousUpdates = await ExtensionSettingsStore.getSetting(
+ "commands", name, extension.id);
+ let commandUpdates = (previousUpdates && previousUpdates.value) || {};
+
+ if (description && description != command.description) {
+ commandUpdates.description = description;
+ command.description = description;
+ }
+
+ if (shortcut && shortcut != command.shortcut) {
+ shortcut = normalizeShortcut(shortcut);
+ commandUpdates.shortcut = shortcut;
+ command.shortcut = shortcut;
+ }
+
+ await ExtensionSettingsStore.addSetting(
+ extension.id, "commands", name, commandUpdates);
+
+ this.registerKeys(commands);
+ }
+
+ async resetCommand(name) {
+ let {extension, manifestCommands} = this;
+ let commands = await this.commands;
+ let command = commands.get(name);
+
+ if (!command) {
+ throw new ExtensionError(`Unknown command "${name}"`);
+ }
+
+ let storedCommand = ExtensionSettingsStore.getSetting(
+ "commands", name, extension.id);
+
+ if (storedCommand && storedCommand.value) {
+ commands.set(name, {...manifestCommands.get(name)});
+ ExtensionSettingsStore.removeSetting(extension.id, "commands", name);
+ this.registerKeys(commands);
+ }
+ }
+
+ async loadCommands() {
+ let {extension} = this;
+
+ // Map[{String} commandName -> {Object} commandProperties]
+ this.manifestCommands = this.loadCommandsFromManifest(extension.manifest);
+
+ this.commands = new Promise(async (resolve) => {
+ // Deep copy the manifest commands to commands so we can keep the original
+ // manifest commands and update commands as needed.
+ let commands = new Map();
+ this.manifestCommands.forEach((command, name) => {
+ commands.set(name, {...command});
+ });
+
+ // Update the manifest commands with the persisted updates from
+ // browser.commands.update().
+ let savedCommands = await this.loadCommandsFromStorage(extension.id);
+ savedCommands.forEach((update, name) => {
+ let command = commands.get(name);
+ if (command) {
+ // We will only update commands, not add them.
+ Object.assign(command, update);
+ }
+ });
+
+ resolve(commands);
+ });
+ }
+
+ async loadAndRegister() {
+ await this.loadCommands();
+ return this.register();
+ }
+
+ registerKeys(commands) {
+ for (let window of windowTracker.browserWindows()) {
+ this.registerKeysToDocument(window, commands);
+ }
+ }
+
+ /**
+ * Registers the commands to all open windows and to any which
+ * are later created.
+ */
+ async register() {
+ let commands = await this.commands;
+ this.registerKeys(commands);
+
+ this.windowOpenListener = (window) => {
+ if (!this.keysetsMap.has(window)) {
+ this.registerKeysToDocument(window, commands);
+ }
+ };
+
+ windowTracker.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 windowTracker.browserWindows()) {
+ if (this.keysetsMap.has(window)) {
+ this.keysetsMap.get(window).remove();
+ }
+ }
+
+ windowTracker.removeOpenListener(this.windowOpenListener);
+ }
+
+ /**
+ * Creates a Map from commands for each command in the manifest.commands object.
+ *
+ * @param {Object} manifest The manifest JSON object.
+ * @returns {Map<string, object>}
+ */
+ loadCommandsFromManifest(manifest) {
+ let commands = new Map();
+ // For Windows, chrome.runtime expects 'win' while chrome.commands
+ // expects 'windows'. We can special case this for now.
+ let {PlatformInfo} = ExtensionParent;
+ let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os;
+ for (let [name, command] of Object.entries(manifest.commands)) {
+ let suggested_key = command.suggested_key || {};
+ let shortcut = normalizeShortcut(suggested_key[os] || suggested_key.default);
+ commands.set(name, {
+ description: command.description,
+ shortcut,
+ });
+ }
+ return commands;
+ }
+
+ async loadCommandsFromStorage(extensionId) {
+ await ExtensionSettingsStore.initialize();
+ let names = ExtensionSettingsStore.getAllForExtension(extensionId, "commands");
+ return names.reduce((map, name) => {
+ let command = ExtensionSettingsStore.getSetting(
+ "commands", name, extensionId).value;
+ return map.set(name, command);
+ }, new Map());
+ }
+
+ /**
+ * Registers the commands to a document.
+ * @param {ChromeWindow} window The XUL window to insert the Keyset.
+ * @param {Map} commands The commands to be set.
+ */
+ registerKeysToDocument(window, commands) {
+ let doc = window.document;
+ let keyset = doc.createElementNS(XUL_NS, "keyset");
+ keyset.id = `ext-keyset-id-${this.id}`;
+ if (this.keysetsMap.has(window)) {
+ this.keysetsMap.get(window).remove();
+ }
+ let sidebarKey;
+ commands.forEach((command, name) => {
+ if (command.shortcut) {
+ let keyElement = this.buildKey(doc, name, command.shortcut);
+ keyset.appendChild(keyElement);
+ if (name == EXECUTE_SIDEBAR_ACTION) {
+ sidebarKey = keyElement;
+ }
+ }
+ });
+ doc.documentElement.appendChild(keyset);
+ if (sidebarKey) {
+ window.SidebarUI.updateShortcut({key: sidebarKey});
+ }
+ this.keysetsMap.set(window, 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, name, 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) => {
+ let action;
+ if (name == EXECUTE_PAGE_ACTION) {
+ action = this.extension.pageAction;
+ } else if (name == EXECUTE_BROWSER_ACTION) {
+ action = this.extension.browserAction;
+ } else if (name == EXECUTE_SIDEBAR_ACTION) {
+ action = this.extension.sidebarAction;
+ } else {
+ this.extension.tabManager
+ .addActiveTabPermission();
+ this.onCommand(name);
+ return;
+ }
+ if (action) {
+ let win = event.target.ownerGlobal;
+ action.triggerAction(win);
+ }
+ });
+ /* 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 shortcut.
+ * @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, name, 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 (name == EXECUTE_SIDEBAR_ACTION) {
+ let id = `ext-key-id-${this.id}-sidebar-action`;
+ keyElement.setAttribute("id", id);
+ }
+
+ if (/^[A-Z]$/.test(chromeKey)) {
+ // We use the key attribute for all single digits and characters.
+ keyElement.setAttribute("key", chromeKey);
+ } else {
+ keyElement.setAttribute("keycode", this.getKeycodeAttribute(chromeKey));
+ keyElement.setAttribute("event", "keydown");
+ }
+
+ 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} chromeKey The chrome key (e.g. "PageUp", "Space", ...)
+ * @returns {string} The constructed value for the Key's 'keycode' attribute.
+ */
+ getKeycodeAttribute(chromeKey) {
+ if (/[0-9]/.test(chromeKey)) {
+ return `VK_${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.
+ * @returns {string} The constructed value for the Key's 'modifiers' attribute.
+ */
+ getModifiersAttribute(chromeModifiers) {
+ return Array.from(chromeModifiers, modifier => {
+ return chromeModifierKeyMap[modifier];
+ }).join(" ");
+ }
+}
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -5,16 +5,19 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
with Files('**'):
BUG_COMPONENT = ('Toolkit', 'General')
with Files('addons/**'):
BUG_COMPONENT = ('WebExtensions', 'General')
+with Files('ExtensionShortcuts.jsm'):
+ BUG_COMPONENT = ('Toolkit', 'WebExtensions: General')
+
with Files('docs/**'):
BUG_COMPONENT = ('Toolkit', 'Async Tooling')
with Files('subprocess/**'):
BUG_COMPONENT = ('Toolkit', 'Async Tooling')
with Files('tests/browser/*AsyncPrefs*'):
BUG_COMPONENT = ('Core', 'Security: Process Sandboxing')
@@ -193,16 +196,17 @@ EXTRA_JS_MODULES += [
'CreditCard.jsm',
'css-selector.js',
'DateTimePickerContent.jsm',
'DateTimePickerParent.jsm',
'DeferredTask.jsm',
'Deprecated.jsm',
'E10SUtils.jsm',
'EventEmitter.jsm',
+ 'ExtensionShortcuts.jsm',
'FileUtils.jsm',
'Finder.jsm',
'FinderHighlighter.jsm',
'FinderIterator.jsm',
'FormLikeFactory.jsm',
'Geometry.jsm',
'GMPExtractorWorker.js',
'GMPInstallManager.jsm',