зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1303384 - Part 3: Manage extension shortcuts page r=aswan,Gijs,flod
MozReview-Commit-ID: KeZsoB6qj88 Differential Revision: https://phabricator.services.mozilla.com/D4507 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
50cfffaeaa
Коммит
890829b813
|
@ -106,6 +106,7 @@ legacyWarning.description=Missing something? Some extensions are no longer suppo
|
|||
legacyThemeWarning.description=Missing something? Some themes are no longer supported by %S.
|
||||
|
||||
listHeading.extension=Manage Your Extensions
|
||||
listHeading.shortcuts=Manage Extension Shortcuts
|
||||
listHeading.theme=Manage Your Themes
|
||||
listHeading.plugin=Manage Your Plugins
|
||||
listHeading.locale=Manage Your Languages
|
||||
|
|
|
@ -275,3 +275,23 @@ extensions-updates-manual-updates-found =
|
|||
extensions-updates-update-selected =
|
||||
.label = Install Updates
|
||||
.tooltiptext = Install available updates in this list
|
||||
|
||||
## Extension shortcut management
|
||||
|
||||
shortcuts-manage =
|
||||
.label = Keyboard Shortcuts
|
||||
shortcuts-empty-message = There are no shortcuts for this extension.
|
||||
# TODO: Confirm this copy.
|
||||
shortcuts-no-addons = You don't have any active add-ons.
|
||||
shortcuts-input =
|
||||
.placeholder = Type a shortcut
|
||||
|
||||
shortcuts-browserAction = Activate extension
|
||||
shortcuts-pageAction = Activate page action
|
||||
shortcuts-sidebarAction = Toggle the sidebar
|
||||
|
||||
shortcuts-modifier-mac = Include Ctrl, Alt, or ⌘
|
||||
shortcuts-modifier-other = Include Ctrl or Alt
|
||||
shortcuts-invalid = Invalid combination
|
||||
shortcuts-letter = Type a letter
|
||||
shortcuts-system = Can’t override a { -brand-short-name } shortcut
|
||||
|
|
|
@ -280,9 +280,14 @@ function isDiscoverEnabled() {
|
|||
|
||||
function setSearchLabel(type) {
|
||||
let searchLabel = document.getElementById("search-label");
|
||||
if (type == "extension" || type == "theme") {
|
||||
let keyMap = {
|
||||
extension: "extension",
|
||||
shortcuts: "extension",
|
||||
theme: "theme",
|
||||
};
|
||||
if (type in keyMap) {
|
||||
searchLabel
|
||||
.textContent = gStrings.ext.GetStringFromName(`searchLabel.${type}`);
|
||||
.textContent = gStrings.ext.GetStringFromName(`searchLabel.${keyMap[type]}`);
|
||||
searchLabel.hidden = false;
|
||||
} else {
|
||||
searchLabel.textContent = "";
|
||||
|
@ -692,6 +697,7 @@ var gViewController = {
|
|||
this.viewObjects.legacy = gLegacyView;
|
||||
this.viewObjects.detail = gDetailView;
|
||||
this.viewObjects.updates = gUpdatesView;
|
||||
this.viewObjects.shortcuts = gShortcutsView;
|
||||
|
||||
for (let type in this.viewObjects) {
|
||||
let view = this.viewObjects[type];
|
||||
|
@ -1383,6 +1389,15 @@ var gViewController = {
|
|||
gViewController.loadView("addons://list/extension");
|
||||
},
|
||||
},
|
||||
|
||||
cmd_showShortcuts: {
|
||||
isEnabled() {
|
||||
return true;
|
||||
},
|
||||
doCommand() {
|
||||
gViewController.loadView("addons://shortcuts/shortcuts");
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
supportsCommand(aCommand) {
|
||||
|
@ -2448,6 +2463,9 @@ var gListView = {
|
|||
}
|
||||
}
|
||||
|
||||
// Only show the manage shortcuts button for extensions.
|
||||
document.getElementById("manage-shortcuts").hidden = this._type != "extension";
|
||||
|
||||
this.filterDisabledUnsigned(showOnlyDisabledUnsigned);
|
||||
let legacyNotice = document.getElementById("legacy-extensions-notice");
|
||||
if (showLegacyInfo) {
|
||||
|
@ -2474,6 +2492,7 @@ var gListView = {
|
|||
hide() {
|
||||
gEventManager.unregisterInstallListener(this);
|
||||
doPendingUninstalls(this._listBox);
|
||||
document.getElementById("manage-shortcuts").hidden = true;
|
||||
},
|
||||
|
||||
filterDisabledUnsigned(aFilter = true) {
|
||||
|
@ -3471,6 +3490,39 @@ var gUpdatesView = {
|
|||
},
|
||||
};
|
||||
|
||||
var gShortcutsView = {
|
||||
node: null,
|
||||
loaded: null,
|
||||
|
||||
initialize() {
|
||||
this.node = document.getElementById("shortcuts-view");
|
||||
this.node.loadURI("chrome://mozapps/content/extensions/shortcuts.html", {
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
// Store a Promise for when the contentWindow will exist.
|
||||
this.loaded = new Promise(resolve => this.node.addEventListener("load", resolve, {once: true}));
|
||||
},
|
||||
|
||||
async show() {
|
||||
// Ensure the Extensions category is selected in case of refresh/restart.
|
||||
gCategories.select("addons://list/extension");
|
||||
|
||||
await this.loaded;
|
||||
await this.node.contentWindow.render();
|
||||
gViewController.notifyViewChanged();
|
||||
},
|
||||
|
||||
refresh() {
|
||||
return this.show();
|
||||
},
|
||||
|
||||
hide() {},
|
||||
|
||||
getSelectedAddon() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
var gDragDrop = {
|
||||
onDragOver(aEvent) {
|
||||
if (!XPINSTALL_ENABLED) {
|
||||
|
|
|
@ -105,6 +105,7 @@
|
|||
<command id="cmd_resetAddonAutoUpdate"/>
|
||||
<command id="cmd_showUnsignedExtensions"/>
|
||||
<command id="cmd_showAllExtensions"/>
|
||||
<command id="cmd_showShortcuts"/>
|
||||
</commandset>
|
||||
|
||||
<!-- view commands - these act on the selected addon -->
|
||||
|
@ -251,6 +252,8 @@
|
|||
command="cmd_restartApp"/>
|
||||
</hbox>
|
||||
|
||||
<button id="manage-shortcuts" data-l10n-id="shortcuts-manage" command="cmd_showShortcuts" hidden="true"/>
|
||||
|
||||
<toolbarbutton id="header-utils-btn" type="menu"
|
||||
data-l10n-id="tools-menu">
|
||||
<menupopup id="utils-menu">
|
||||
|
@ -357,6 +360,9 @@
|
|||
<richlistbox id="addon-list" class="list" flex="1"/>
|
||||
</vbox>
|
||||
|
||||
<!-- extension shortcuts view -->
|
||||
<browser id="shortcuts-view" type="content" flex="1" disablehistory="true"/>
|
||||
|
||||
<!-- legacy extensions view -->
|
||||
<vbox id="legacy-view" flex="1" class="view-pane" align="stretch" tabindex="0">
|
||||
<vbox id="legacy-extensions-info">
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
.body {
|
||||
margin-inline-start: 28px;
|
||||
}
|
||||
|
||||
.shortcut.card {
|
||||
/* Preferences content is 664px and the cards have 16px of left/right padding. */
|
||||
width: 632px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.shortcut.card:first-of-type {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.shortcut.card:hover {
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.card-heading-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
.card-heading {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.shortcuts-empty-label {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.shortcut-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.shortcut-input {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.extension-heading {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
--error-background: var(--red-60);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.error-message-icon {
|
||||
margin-left: 10px;
|
||||
width: 14px;
|
||||
height: 8px;
|
||||
fill: var(--error-background);
|
||||
stroke: var(--error-background);
|
||||
-moz-context-properties: fill, stroke;
|
||||
}
|
||||
|
||||
.error-message-label {
|
||||
background-color: var(--error-background);
|
||||
border-radius: 2px;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.error-message-arrow {
|
||||
background-color: var(--error-background);
|
||||
content: "";
|
||||
max-height: 8px;
|
||||
width: 8px;
|
||||
transform: translate(4px, -6px) rotate(45deg);
|
||||
position: absolute;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://mozapps/content/extensions/shortcuts.css" type="text/css"/>
|
||||
|
||||
<link rel="localization" href="branding/brand.ftl"/>
|
||||
<link rel="localization" href="toolkit/about/aboutAddons.ftl"/>
|
||||
|
||||
<script type="application/javascript" src="chrome://mozapps/content/extensions/shortcuts.js"></script>
|
||||
</head>
|
||||
<body id="body">
|
||||
<div class="body">
|
||||
<div class="error-message">
|
||||
<img class="error-message-icon" src="chrome://global/skin/arrow/panelarrow-vertical.svg"/>
|
||||
<div class="error-message-label"></div>
|
||||
</div>
|
||||
|
||||
<div id="addon-shortcuts"></div>
|
||||
|
||||
<template id="card-template">
|
||||
<div class="card shortcut">
|
||||
<div class="card-heading">
|
||||
<img class="card-heading-icon addon-icon"/>
|
||||
<span class="addon-name"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="shortcut-row-template">
|
||||
<div class="shortcut-row">
|
||||
<label class="shortcut-label"></label>
|
||||
<input class="shortcut-input" data-l10n-id="shortcuts-input" type="text" readonly/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="shortcuts-empty-template">
|
||||
<div class="shortcuts-empty-label" data-l10n-id="shortcuts-empty-message"></div>
|
||||
</template>
|
||||
|
||||
<template id="shortcuts-no-addons">
|
||||
<div data-l10n-id="shortcuts-no-addons"></div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,321 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
/* exported render */
|
||||
|
||||
"use strict";
|
||||
|
||||
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
AddonManager: "resource://gre/modules/AddonManager.jsm",
|
||||
AppConstants: "resource://gre/modules/AppConstants.jsm",
|
||||
ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
|
||||
});
|
||||
|
||||
let templatesLoaded = false;
|
||||
const templates = {};
|
||||
|
||||
function loadTemplates() {
|
||||
if (templatesLoaded) return;
|
||||
templatesLoaded = true;
|
||||
|
||||
templates.card = document.getElementById("card-template");
|
||||
templates.row = document.getElementById("shortcut-row-template");
|
||||
templates.empty = document.getElementById("shortcuts-empty-template");
|
||||
templates.noAddons = document.getElementById("shortcuts-no-addons");
|
||||
}
|
||||
|
||||
function extensionForAddonId(id) {
|
||||
let policy = WebExtensionPolicy.getByID(id);
|
||||
return policy && policy.extension;
|
||||
}
|
||||
|
||||
let builtInNames = new Map([
|
||||
["_execute_browser_action", "shortcuts-browserAction"],
|
||||
["_execute_page_action", "shortcuts-pageAction"],
|
||||
["_execute_sidebar_action", "shortcuts-sidebarAction"],
|
||||
]);
|
||||
let getCommandDescriptionId = (command) => {
|
||||
if (!command.description && builtInNames.has(command.name)) {
|
||||
return builtInNames.get(command.name);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const _functionKeys = [
|
||||
"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",
|
||||
];
|
||||
const functionKeys = new Set(_functionKeys);
|
||||
const validKeys = new Set([
|
||||
"Home", "End", "PageUp", "PageDown", "Insert", "Delete",
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
..._functionKeys,
|
||||
"MediaNextTrack", "MediaPlayPause", "MediaPrevTrack", "MediaStop",
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
||||
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
||||
"Up", "Down", "Left", "Right",
|
||||
"Comma", "Period", "Space",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Trim a valid prefix from an event string.
|
||||
*
|
||||
* "Digit3" ~> "3"
|
||||
* "ArrowUp" ~> "Up"
|
||||
* "W" ~> "W"
|
||||
*
|
||||
* @param {string} string The input string.
|
||||
* @returns {string} The trimmed string, or unchanged.
|
||||
*/
|
||||
function trimPrefix(string) {
|
||||
return string.replace(/^(?:Digit|Numpad|Arrow)/, "");
|
||||
}
|
||||
|
||||
const remapKeys = {
|
||||
",": "Comma",
|
||||
".": "Period",
|
||||
" ": "Space",
|
||||
};
|
||||
/**
|
||||
* Map special keys to their shortcut name.
|
||||
*
|
||||
* "," ~> "Comma"
|
||||
* " " ~> "Space"
|
||||
*
|
||||
* @param {string} string The input string.
|
||||
* @returns {string} The remapped string, or unchanged.
|
||||
*/
|
||||
function remapKey(string) {
|
||||
if (remapKeys.hasOwnProperty(string)) {
|
||||
return remapKeys[string];
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
const keyOptions = [
|
||||
e => String.fromCharCode(e.which), // A letter?
|
||||
e => e.code.toUpperCase(), // A letter.
|
||||
e => trimPrefix(e.code), // Digit3, ArrowUp, Numpad9.
|
||||
e => trimPrefix(e.key), // Digit3, ArrowUp, Numpad9.
|
||||
e => remapKey(e.key), // Comma, Period, Space.
|
||||
];
|
||||
/**
|
||||
* Map a DOM event to a shortcut string character.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* "a" ~> "A"
|
||||
* "Digit3" ~> "3"
|
||||
* "," ~> "Comma"
|
||||
*
|
||||
* @param {object} event A KeyboardEvent.
|
||||
* @returns {string} A string corresponding to the pressed key.
|
||||
*/
|
||||
function getStringForEvent(event) {
|
||||
for (let option of keyOptions) {
|
||||
let value = option(event);
|
||||
if (validKeys.has(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function getShortcutValue(shortcut) {
|
||||
if (!shortcut) {
|
||||
// Ensure the shortcut is a string, even if it is unset.
|
||||
return null;
|
||||
}
|
||||
|
||||
let modifiers = shortcut.split("+");
|
||||
let key = modifiers.pop();
|
||||
|
||||
if (modifiers.length > 0) {
|
||||
let modifiersAttribute = ShortcutUtils.getModifiersAttribute(modifiers);
|
||||
let displayString =
|
||||
ShortcutUtils.getModifierString(modifiersAttribute) + key;
|
||||
return displayString;
|
||||
}
|
||||
|
||||
if (functionKeys.has(key)) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
let error;
|
||||
|
||||
function setError(input, messageId) {
|
||||
if (!error) error = document.querySelector(".error-message");
|
||||
|
||||
let {x, y, height} = input.getBoundingClientRect();
|
||||
error.style.top = `${y + window.scrollY + height - 5}px`;
|
||||
error.style.left = `${x}px`;
|
||||
document.l10n.setAttributes(
|
||||
error.querySelector(".error-message-label"), messageId);
|
||||
error.style.visibility = "visible";
|
||||
}
|
||||
|
||||
function inputBlurred(e) {
|
||||
if (!error) error = document.querySelector(".error-message");
|
||||
|
||||
error.style.visibility = "hidden";
|
||||
e.target.value = getShortcutValue(e.target.getAttribute("shortcut"));
|
||||
}
|
||||
|
||||
function clearValue(e) {
|
||||
e.target.value = "";
|
||||
}
|
||||
|
||||
function getShortcutForEvent(e) {
|
||||
let modifierMap;
|
||||
|
||||
if (AppConstants.platform == "macosx") {
|
||||
modifierMap = {
|
||||
MacCtrl: e.ctrlKey,
|
||||
Alt: e.altKey,
|
||||
Command: e.metaKey,
|
||||
Shift: e.shiftKey,
|
||||
};
|
||||
} else {
|
||||
modifierMap = {
|
||||
Ctrl: e.ctrlKey,
|
||||
Alt: e.altKey,
|
||||
Shift: e.shiftKey,
|
||||
};
|
||||
}
|
||||
|
||||
return Object.entries(modifierMap)
|
||||
.filter(([key, isDown]) => isDown)
|
||||
.map(([key]) => key)
|
||||
.concat(getStringForEvent(e))
|
||||
.join("+");
|
||||
}
|
||||
|
||||
function onShortcutChange(e) {
|
||||
let input = e.target;
|
||||
|
||||
if (e.key == "Escape") {
|
||||
input.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key == "Tab") {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let shortcutString = getShortcutForEvent(e);
|
||||
input.value = getShortcutValue(shortcutString);
|
||||
|
||||
if (e.type == "keyup" || shortcutString.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let validation = ShortcutUtils.validate(shortcutString);
|
||||
switch (validation) {
|
||||
case ShortcutUtils.IS_VALID:
|
||||
// Show an error if this is already a system shortcut.
|
||||
let chromeWindow = window.windowRoot.ownerGlobal;
|
||||
if (ShortcutUtils.isSystem(chromeWindow, shortcutString)) {
|
||||
setError(input, "shortcuts-system");
|
||||
break;
|
||||
}
|
||||
|
||||
// Update the shortcut if it isn't reserved.
|
||||
let addonId = input.closest(".card").getAttribute("addon-id");
|
||||
let extension = extensionForAddonId(addonId);
|
||||
|
||||
// This is async, but we're not awaiting it to keep the handler sync.
|
||||
extension.shortcuts.updateCommand({
|
||||
name: input.getAttribute("name"),
|
||||
shortcut: shortcutString,
|
||||
});
|
||||
input.setAttribute("shortcut", shortcutString);
|
||||
input.blur();
|
||||
break;
|
||||
case ShortcutUtils.MODIFIER_REQUIRED:
|
||||
if (AppConstants.platform == "macosx")
|
||||
setError(input, "shortcuts-modifier-mac");
|
||||
else
|
||||
setError(input, "shortcuts-modifier-other");
|
||||
break;
|
||||
case ShortcutUtils.INVALID_COMBINATION:
|
||||
setError(input, "shortcuts-invalid");
|
||||
break;
|
||||
case ShortcutUtils.INVALID_KEY:
|
||||
setError(input, "shortcuts-letter");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function renderAddons(addons) {
|
||||
let frag = document.createDocumentFragment();
|
||||
for (let addon of addons) {
|
||||
let extension = extensionForAddonId(addon.id);
|
||||
|
||||
// Skip this extension if it isn't a webextension.
|
||||
if (!extension) continue;
|
||||
|
||||
let card = document.importNode(
|
||||
templates.card.content, true).firstElementChild;
|
||||
let icon = AddonManager.getPreferredIconURL(addon, 24, window);
|
||||
card.setAttribute("addon-id", addon.id);
|
||||
card.querySelector(".addon-icon").src = icon;
|
||||
card.querySelector(".addon-name").textContent = addon.name;
|
||||
|
||||
if (extension.shortcuts) {
|
||||
let commands = await extension.shortcuts.allCommands();
|
||||
|
||||
for (let command of commands) {
|
||||
let row = document.importNode(templates.row.content, true);
|
||||
let label = row.querySelector(".shortcut-label");
|
||||
let descriptionId = getCommandDescriptionId(command);
|
||||
if (descriptionId) {
|
||||
document.l10n.setAttributes(label, descriptionId);
|
||||
} else {
|
||||
label.textContent = command.description || command.name;
|
||||
}
|
||||
let input = row.querySelector(".shortcut-input");
|
||||
input.value = getShortcutValue(command.shortcut);
|
||||
input.setAttribute("name", command.name);
|
||||
input.setAttribute("shortcut", command.shortcut);
|
||||
input.addEventListener("keydown", onShortcutChange);
|
||||
input.addEventListener("keyup", onShortcutChange);
|
||||
input.addEventListener("blur", inputBlurred);
|
||||
input.addEventListener("focus", clearValue);
|
||||
|
||||
card.appendChild(row);
|
||||
}
|
||||
} else {
|
||||
card.appendChild(document.importNode(templates.empty.content, true));
|
||||
}
|
||||
|
||||
frag.appendChild(card);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
|
||||
async function render() {
|
||||
loadTemplates();
|
||||
let allAddons = await AddonManager.getAddonsByTypes(["extension"]);
|
||||
let addons = allAddons
|
||||
.filter(addon => !addon.isSystem && addon.isActive)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
let frag;
|
||||
|
||||
if (addons.length > 0) {
|
||||
frag = await renderAddons(addons);
|
||||
} else {
|
||||
frag = document.importNode(templates.noAddons.content, true);
|
||||
}
|
||||
|
||||
let container = document.getElementById("addon-shortcuts");
|
||||
container.textContent = "";
|
||||
container.appendChild(frag);
|
||||
}
|
|
@ -5,6 +5,9 @@
|
|||
toolkit.jar:
|
||||
% content mozapps %content/mozapps/
|
||||
content/mozapps/extensions/default-theme-icon.svg (content/default-theme-icon.svg)
|
||||
content/mozapps/extensions/shortcuts.html (content/shortcuts.html)
|
||||
content/mozapps/extensions/shortcuts.css (content/shortcuts.css)
|
||||
content/mozapps/extensions/shortcuts.js (content/shortcuts.js)
|
||||
#ifndef MOZ_FENNEC
|
||||
* content/mozapps/extensions/extensions.xul (content/extensions.xul)
|
||||
content/mozapps/extensions/extensions.css (content/extensions.css)
|
||||
|
|
|
@ -5,6 +5,10 @@ module.exports = {
|
|||
"plugin:mozilla/browser-test"
|
||||
],
|
||||
|
||||
"env": {
|
||||
"webextensions": true,
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"no-unused-vars": ["error", {"args": "none", "varsIgnorePattern": "^end_test$"}],
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ skip-if = verify
|
|||
[browser_legacy.js]
|
||||
[browser_legacy_pre57.js]
|
||||
[browser_list.js]
|
||||
[browser_theme_previews.js]
|
||||
[browser_manage_shortcuts.js]
|
||||
[browser_manualupdates.js]
|
||||
[browser_pluginprefs.js]
|
||||
[browser_pluginprefs_is_not_disabled.js]
|
||||
|
@ -92,6 +92,7 @@ skip-if = verify
|
|||
[browser_sorting_plugins.js]
|
||||
[browser_tabsettings.js]
|
||||
[browser_task_next_test.js]
|
||||
[browser_theme_previews.js]
|
||||
[browser_types.js]
|
||||
[browser_uninstalling.js]
|
||||
[browser_updateid.js]
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
"use strict";
|
||||
|
||||
let gManagerWindow;
|
||||
let gCategoryUtilities;
|
||||
|
||||
const {PromiseTestUtils} = ChromeUtils.import("resource://testing-common/PromiseTestUtils.jsm", {});
|
||||
PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
|
||||
|
||||
add_task(async function testUpdatingCommands() {
|
||||
let commands = {
|
||||
commandOne: {
|
||||
suggested_key: {default: "Shift+Alt+4"},
|
||||
},
|
||||
commandTwo: {
|
||||
description: "Command Two!",
|
||||
suggested_key: {default: "Alt+4"},
|
||||
},
|
||||
_execute_browser_action: {
|
||||
suggested_key: {default: "Shift+Alt+5"},
|
||||
},
|
||||
};
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
commands,
|
||||
browser_action: {default_popup: "popup.html"},
|
||||
},
|
||||
background() {
|
||||
browser.commands.onCommand.addListener(commandName => {
|
||||
browser.test.sendMessage("oncommand", commandName);
|
||||
});
|
||||
browser.test.sendMessage("ready");
|
||||
},
|
||||
useAddonManager: "temporary",
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("ready");
|
||||
|
||||
gManagerWindow = await open_manager(null);
|
||||
gCategoryUtilities = new CategoryUtilities(gManagerWindow);
|
||||
await gCategoryUtilities.openType("extension");
|
||||
|
||||
async function checkShortcut(name, key, modifiers) {
|
||||
EventUtils.synthesizeKey(key, modifiers);
|
||||
let message = await extension.awaitMessage("oncommand");
|
||||
is(message, name, `Expected onCommand listener to fire with the correct name: ${name}`);
|
||||
}
|
||||
|
||||
// Check that the original shortcuts work.
|
||||
await checkShortcut("commandOne", "4", {shiftKey: true, altKey: true});
|
||||
await checkShortcut("commandTwo", "4", {altKey: true});
|
||||
|
||||
// There should be a manage shortcuts link.
|
||||
let doc = gManagerWindow.document;
|
||||
let shortcutsLink = doc.getElementById("manage-shortcuts");
|
||||
ok(!shortcutsLink.hidden, "The shortcuts link is visible");
|
||||
|
||||
// Open the shortcuts view.
|
||||
shortcutsLink.click();
|
||||
await wait_for_view_load(gManagerWindow);
|
||||
|
||||
doc = doc.getElementById("shortcuts-view").contentDocument;
|
||||
|
||||
let card = doc.querySelector(`.card[addon-id="${extension.id}"]`);
|
||||
ok(card, `There is a card for the extension`);
|
||||
|
||||
let inputs = card.querySelectorAll(".shortcut-input");
|
||||
is(inputs.length, Object.keys(commands).length, "There is an input for each command");
|
||||
|
||||
for (let input of inputs) {
|
||||
// Change the shortcut.
|
||||
input.focus();
|
||||
EventUtils.synthesizeKey("7", {shiftKey: true, altKey: true});
|
||||
|
||||
// Wait for the shortcut attribute to change.
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => input.getAttribute("shortcut") == "Alt+Shift+7");
|
||||
|
||||
// Check that the change worked (but skip if browserAction).
|
||||
if (input.getAttribute("name") != "_execute_browser_action") {
|
||||
await checkShortcut(input.getAttribute("name"), "7", {shiftKey: true, altKey: true});
|
||||
}
|
||||
|
||||
// Change it again so it doesn't conflict with the next command.
|
||||
input.focus();
|
||||
EventUtils.synthesizeKey("9", {shiftKey: true, altKey: true});
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => input.getAttribute("shortcut") == "Alt+Shift+9");
|
||||
}
|
||||
|
||||
// Check that errors can be shown.
|
||||
let input = inputs[0];
|
||||
let error = doc.querySelector(".error-message");
|
||||
let label = error.querySelector(".error-message-label");
|
||||
is(error.style.visibility, "hidden", "The error is initially hidden");
|
||||
|
||||
// Try a shortcut with only shift for a modifier.
|
||||
input.focus();
|
||||
EventUtils.synthesizeKey("J", {shiftKey: true});
|
||||
let possibleErrors = ["shortcuts-modifier-mac", "shortcuts-modifier-other"];
|
||||
ok(possibleErrors.includes(label.dataset.l10nId), `The message is set`);
|
||||
is(error.style.visibility, "visible", "The error is shown");
|
||||
|
||||
// Escape should clear the focus and hide the error.
|
||||
is(doc.activeElement, input, "The input is focused");
|
||||
EventUtils.synthesizeKey("Escape", {});
|
||||
ok(doc.activeElement != input, "The input is no longer focused");
|
||||
is(error.style.visibility, "hidden", "The error is hidden");
|
||||
|
||||
// Check the label uses the description first, and has a default for the special cases.
|
||||
function checkLabel(name, value) {
|
||||
let input = doc.querySelector(`.shortcut-input[name="${name}"]`);
|
||||
let label = input.previousElementSibling;
|
||||
if (label.dataset.l10nId) {
|
||||
is(label.dataset.l10nId, value, "The l10n-id is set");
|
||||
} else {
|
||||
is(label.textContent, value, "The textContent is set");
|
||||
}
|
||||
}
|
||||
checkLabel("commandOne", "commandOne");
|
||||
checkLabel("commandTwo", "Command Two!");
|
||||
checkLabel("_execute_browser_action", "shortcuts-browserAction");
|
||||
|
||||
await close_manager(gManagerWindow);
|
||||
await extension.unload();
|
||||
});
|
|
@ -250,11 +250,17 @@ button.warning {
|
|||
}
|
||||
}
|
||||
|
||||
#manage-shortcuts {
|
||||
margin: 0 4px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
#header-utils-btn {
|
||||
-moz-appearance: none;
|
||||
border: 1px solid var(--in-content-box-border-color);
|
||||
border-radius: 2px;
|
||||
line-height: 20px;
|
||||
min-height: 30px;
|
||||
background-color: var(--in-content-page-background);
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
--grey-90-a50: rgba(12, 12, 13, 0.5);
|
||||
--red-50: #ff0039;
|
||||
--red-50-a30: rgba(255, 0, 57, 0.3);
|
||||
--red-60: #d70022;
|
||||
--yellow-50: #ffe900;
|
||||
--yellow-90: #3e2800;
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче