tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 287985a9b3a966ba8eb0fd69299d56c1e746818b
parent 6f4719b83cd84eda4fb6d88f6dcd778381766331
Author: James Teh <jteh@mozilla.com>
Date:   Wed, 12 Nov 2025 05:14:26 +0000

Bug 1635774 part 3: Add about:keyboard UI to customise keyboard shortcuts. r=fluent-reviewers,mossop,bolsson,toolkit-telemetry-reviewers,frontend-codestyle-reviewers

This is an unpolished UI for experimentation.
It is likely that this UI will be heavily modified or even completely replaced before it is considered official.
As such, this is only available via a new about:keyboard page with no entry point elsewhere in the UI.

This primarily obtains keyboard shortcuts from the menu bar menus.
This doesn't allow all keyboard shortcuts to be customised, since many aren't available in the menu bar menus and haven't been hard-coded here.
There is a basic framework to allow for hard-coding of additional shortcuts, though, and this has been used for a few shortcuts already.

For now, when a key combination involves a character and a modifier changes the character, the UI shows the modified character.
For example, on a US English QWERTY keyboard, shift+3 produces the `#` character, so the UI shows `Shift+#`.
While this is likely to be a little confusing for some users, this is difficult to fix in a locale independent way.
The current behaviour should allow this to work across locales, albeit with this somewhat quirky presentation.

Differential Revision: https://phabricator.services.mozilla.com/D266792

Diffstat:
Mbrowser/components/DesktopActorRegistry.sys.mjs | 14++++++++++++++
Mbrowser/components/about/AboutRedirector.cpp | 5+++++
Mbrowser/components/about/components.conf | 1+
Abrowser/components/customkeys/CustomKeysChild.sys.mjs | 10++++++++++
Abrowser/components/customkeys/CustomKeysParent.sys.mjs | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/customkeys/content/customkeys.css | 34++++++++++++++++++++++++++++++++++
Abrowser/components/customkeys/content/customkeys.html | 35+++++++++++++++++++++++++++++++++++
Abrowser/components/customkeys/content/customkeys.js | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/customkeys/content/jar.mn | 8++++++++
Abrowser/components/customkeys/content/moz.build | 7+++++++
Abrowser/components/customkeys/metrics.yaml | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/customkeys/moz.build | 9+++++++++
Mbrowser/components/customkeys/tests/browser/browser.toml | 2++
Abrowser/components/customkeys/tests/browser/browser_aboutKeyboard.js | 732+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/customkeys/tests/browser/head.js | 5+++++
Abrowser/locales/en-US/browser/customkeys.ftl | 42++++++++++++++++++++++++++++++++++++++++++
Meslint-file-globals.config.mjs | 1+
Mtoolkit/components/glean/metrics_index.py | 1+
Mtoolkit/modules/RemotePageAccessManager.sys.mjs | 12++++++++++++
Mtools/lint/fluent-lint/exclusions.yml | 2++
20 files changed, 1382 insertions(+), 0 deletions(-)

diff --git a/browser/components/DesktopActorRegistry.sys.mjs b/browser/components/DesktopActorRegistry.sys.mjs @@ -343,6 +343,20 @@ let JSWINDOWACTORS = { allFrames: true, }, + CustomKeys: { + parent: { + esModuleURI: "resource:///actors/CustomKeysParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/CustomKeysChild.sys.mjs", + events: { + DOMDocElementInserted: { wantUntrusted: true }, + }, + }, + matches: ["about:keyboard"], + remoteTypes: ["privilegedabout"], + }, + DecoderDoctor: { parent: { esModuleURI: "resource:///actors/DecoderDoctorParent.sys.mjs", diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp @@ -153,6 +153,11 @@ static const RedirEntry kRedirMap[] = { nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS | nsIAboutModule::HIDE_FROM_ABOUTABOUT}, #endif + {"keyboard", "chrome://browser/content/customkeys/customkeys.html", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::IS_SECURE_CHROME_UI | nsIAboutModule::ALLOW_SCRIPT | + nsIAboutModule::URI_MUST_LOAD_IN_CHILD | + nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS}, }; static nsAutoCString GetAboutModuleName(nsIURI* aURI) { diff --git a/browser/components/about/components.conf b/browser/components/about/components.conf @@ -10,6 +10,7 @@ pages = [ 'certerror', 'downloads', 'framecrashed', + 'keyboard', 'logins', 'loginsimportreport', 'firefoxview', diff --git a/browser/components/customkeys/CustomKeysChild.sys.mjs b/browser/components/customkeys/CustomKeysChild.sys.mjs @@ -0,0 +1,10 @@ +/* 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/. */ + +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +/** + * Empty child actor as most operations are handled by the parent. + */ +export class CustomKeysChild extends RemotePageChild {} diff --git a/browser/components/customkeys/CustomKeysParent.sys.mjs b/browser/components/customkeys/CustomKeysParent.sys.mjs @@ -0,0 +1,191 @@ +/* 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/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { CustomKeys } from "resource:///modules/CustomKeys.sys.mjs"; +import { ShortcutUtils } from "resource://gre/modules/ShortcutUtils.sys.mjs"; + +/** + * Actor implementation for about:keyboard. + */ +export class CustomKeysParent extends JSWindowActorParent { + getKeyData(id) { + const keyEl = + this.browsingContext.topChromeWindow.document.getElementById(id); + if (!keyEl) { + return null; + } + return { + shortcut: ShortcutUtils.prettifyShortcut(keyEl), + isCustomized: !!CustomKeys.getDefaultKey(id), + }; + } + + // Get keyboard shortcuts in the form: + // { <categoryTitle>: { <keyId>: { title: <title>, shortcut: <String>, isCustomized: <Boolean> } } + // If categoryTitle or keyTitle begins with "customkeys-", it is a Fluent id. + getKeys() { + const topWin = this.browsingContext.topChromeWindow; + + const add = (category, id, title) => { + const data = this.getKeyData(id); + if (data) { + data.title = title; + category[id] = data; + } + }; + + const keys = {}; + // Gather as many keys as we can from the menu bar menus. + const mainMenuBar = topWin.document.getElementById("main-menubar"); + for (const item of mainMenuBar.querySelectorAll("menuitem[key]")) { + const menu = item.closest("menu"); + if (menu.id == "historyUndoMenu" || menu.id == "historyUndoWindowMenu") { + // The reopen last tab/window commands have the label of the actual + // tab/window they will reopen. We handle those specially later. + continue; + } + if (!keys[menu.label]) { + keys[menu.label] = {}; + } + add(keys[menu.label], item.getAttribute("key"), item.label); + } + + // Add some shortcuts that aren't available in menus. + const historyCat = topWin.document.getElementById("history-menu").label; + let cat = keys[historyCat]; + add( + cat, + "key_restoreLastClosedTabOrWindowOrSession", + "customkeys-history-reopen-tab" + ); + add(cat, "key_undoCloseWindow", "customkeys-history-reopen-window"); + const toolsCat = topWin.document.getElementById("browserToolsMenu").label; + cat = keys[toolsCat]; + add(cat, "key_toggleToolboxF12", "customkeys-dev-tools"); + add(cat, "key_inspector", "customkeys-dev-inspector"); + add(cat, "key_webconsole", "customkeys-dev-webconsole"); + add(cat, "key_jsdebugger", "customkeys-dev-debugger"); + add(cat, "key_netmonitor", "customkeys-dev-network"); + add(cat, "key_styleeditor", "customkeys-dev-style"); + add(cat, "key_performance", "customkeys-dev-performance"); + add(cat, "key_storage", "customkeys-dev-storage"); + add(cat, "key_dom", "customkeys-dev-dom"); + add(cat, "key_accessibility", "customkeys-dev-accessibility"); + add(cat, "key_profilerStartStop", "customkeys-dev-profiler-toggle"); + add( + cat, + "key_profilerStartStopAlternate", + "customkeys-dev-profiler-toggle" + ); + add(cat, "key_profilerCapture", "customkeys-dev-profiler-capture"); + add(cat, "key_profilerCaptureAlternate", "customkeys-dev-profiler-capture"); + cat = keys["customkeys-category-navigation"] = {}; + add(cat, "goBackKb", "customkeys-nav-back"); + add(cat, "goForwardKb", "customkeys-nav-forward"); + add(cat, "goHome", "customkeys-nav-home"); + add(cat, "key_reload", "customkeys-nav-reload"); + add(cat, "key_reload2", "customkeys-nav-reload"); + add(cat, "key_reload_skip_cache", "customkeys-nav-reload-skip-cache"); + add(cat, "key_reload_skip_cache2", "customkeys-nav-reload-skip-cache"); + add(cat, "key_stop", "customkeys-nav-stop"); + + return keys; + } + + prettifyShortcut({ modifiers, key, keycode }) { + // ShortcutUtils.prettifyShortcut needs a key element, but we don't have + // that here. It simply concatenates the prettified modifiers and key + // anyway. + const prettyMods = ShortcutUtils.getModifierString(modifiers); + const prettyKey = ShortcutUtils.getKeyString(keycode, key); + return prettyMods + prettyKey; + } + + async receiveMessage(message) { + switch (message.name) { + case "CustomKeys:CaptureKey": { + if (message.data) { + this.browsingContext.embedderElement.addEventListener( + "keydown", + this + ); + } else { + this.browsingContext.embedderElement.removeEventListener( + "keydown", + this + ); + } + return null; + } + case "CustomKeys:ChangeKey": { + CustomKeys.changeKey(message.data.id, message.data); + return this.getKeyData(message.data.id); + } + case "CustomKeys:ClearKey": { + const id = message.data; + CustomKeys.clearKey(id); + return this.getKeyData(id); + } + case "CustomKeys:GetDefaultKey": { + const data = { id: message.data }; + Object.assign(data, CustomKeys.getDefaultKey(data.id)); + data.shortcut = this.prettifyShortcut(data); + return data; + } + case "CustomKeys:GetKeys": { + return this.getKeys(); + } + case "CustomKeys:ResetAll": { + return CustomKeys.resetAll(); + } + case "CustomKeys:ResetKey": { + CustomKeys.resetKey(message.data); + return this.getKeyData(message.data); + } + } + return null; + } + + handleEvent(event) { + if (event.type == "keydown") { + event.preventDefault(); + event.stopPropagation(); + const data = {}; + let modifiers = []; + const isMac = AppConstants.platform === "macosx"; + if (event.altKey) { + modifiers.push("Alt"); + } + if (event.ctrlKey) { + modifiers.push(isMac ? "MacCtrl" : "Ctrl"); + } + if (isMac && event.metaKey) { + modifiers.push("Command"); + } + if (event.shiftKey) { + modifiers.push("Shift"); + } + data.modifiers = ShortcutUtils.getModifiersAttribute(modifiers); + if ( + event.key == "Alt" || + event.key == "Control" || + event.key == "Meta" || + event.key == "Shift" + ) { + data.isModifier = true; + data.modifierString = ShortcutUtils.getModifierString(data.modifiers); + this.sendAsyncMessage("CustomKeys:CapturedKey", data); + return; + } + if (event.key.length == 1) { + data.key = event.key.toUpperCase(); + } else { + data.keycode = ShortcutUtils.getKeycodeAttribute(event.key); + } + data.shortcut = this.prettifyShortcut(data); + this.sendAsyncMessage("CustomKeys:CapturedKey", data); + } + } +} diff --git a/browser/components/customkeys/content/customkeys.css b/browser/components/customkeys/content/customkeys.css @@ -0,0 +1,34 @@ +/* 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/. */ + +th { + text-align: start; +} + +.clear { + display: none; +} +.assigned .clear { + display: revert; +} + +.reset { + display: none; +} +.customized .reset { + display: revert; +} + +.newLabel { + display: none; +} + +.editing { + .change { + display: none; + } + .newLabel { + display: revert; + } +} diff --git a/browser/components/customkeys/content/customkeys.html b/browser/components/customkeys/content/customkeys.html @@ -0,0 +1,35 @@ +<!-- 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> + <meta + http-equiv="Content-Security-Policy" + content="default-src chrome:; object-src 'none'" + /> + <meta charset="utf-8" /> + <link rel="localization" href="browser/customkeys.ftl" /> + <link + rel="stylesheet" + href="chrome://browser/content/customkeys/customkeys.css" + /> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> + <title data-l10n-id="customkeys-title"></title> + </head> + <body> + <p> + <em id="cautionMessage" data-l10n-id="customkeys-caution-message"></em> + </p> + <div> + <label for="search" data-l10n-id="customkeys-search"></label> + <input id="search" type="search" /> + </div> + <table id="table"></table> + <div> + <button id="resetAll" data-l10n-id="customkeys-reset-all"></button> + </div> + <script src="chrome://browser/content/customkeys/customkeys.js"></script> + </body> +</html> diff --git a/browser/components/customkeys/content/customkeys.js b/browser/components/customkeys/content/customkeys.js @@ -0,0 +1,222 @@ +/* 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/. */ + +const table = document.getElementById("table"); + +function setTextContent(element, content) { + if (content.startsWith("customkeys-")) { + element.setAttribute("data-l10n-id", content); + } else { + element.textContent = content; + } +} + +function notifyUpdate() { + window.dispatchEvent(new CustomEvent("CustomKeysUpdate")); +} + +async function buildTable() { + const keys = await RPMSendQuery("CustomKeys:GetKeys"); + for (const category in keys) { + const tbody = document.createElement("tbody"); + table.append(tbody); + let row = document.createElement("tr"); + row.className = "category"; + tbody.append(row); + let cell = document.createElement("td"); + row.append(cell); + cell.setAttribute("colspan", 5); + const heading = document.createElement("h1"); + setTextContent(heading, category); + cell.append(heading); + const categoryKeys = keys[category]; + for (const keyId in categoryKeys) { + row = document.createElement("tr"); + row.className = "key"; + tbody.append(row); + row.setAttribute("data-id", keyId); + cell = document.createElement("th"); + const key = categoryKeys[keyId]; + setTextContent(cell, key.title); + row.append(cell); + cell = document.createElement("td"); + cell.textContent = key.shortcut; + row.append(cell); + cell = document.createElement("td"); + let button = document.createElement("button"); + button.className = "change"; + button.setAttribute("data-l10n-id", "customkeys-change"); + cell.append(button); + let label = document.createElement("label"); + label.className = "newLabel"; + let span = document.createElement("span"); + span.setAttribute("data-l10n-id", "customkeys-new-key"); + label.append(span); + let input = document.createElement("input"); + input.className = "new"; + label.append(input); + cell.append(label); + row.append(cell); + cell = document.createElement("td"); + button = document.createElement("button"); + button.className = "clear"; + button.setAttribute("data-l10n-id", "customkeys-clear"); + cell.append(button); + row.append(cell); + cell = document.createElement("td"); + button = document.createElement("button"); + button.className = "reset"; + button.setAttribute("data-l10n-id", "customkeys-reset"); + cell.append(button); + row.append(cell); + updateKey(row, key); + } + } + notifyUpdate(); +} + +function updateKey(row, data) { + row.children[1].textContent = data.shortcut; + row.classList.toggle("customized", data.isCustomized); + row.classList.toggle("assigned", !!data.shortcut); +} + +// Returns false if the assignment should be cancelled. +async function maybeHandleConflict(data) { + for (const row of table.querySelectorAll(".key")) { + if (data.shortcut != row.children[1].textContent) { + continue; // Not a conflict. + } + const conflictId = row.dataset.id; + if (conflictId == data.id) { + // We're trying to assign this key to the shortcut it is already + // assigned to. We don't need to do anything. + return false; + } + const conflictDesc = row.children[0].textContent; + if ( + window.confirm( + await document.l10n.formatValue("customkeys-conflict-confirm", { + conflict: conflictDesc, + }) + ) + ) { + // Clear the conflicting key. + const newData = await RPMSendQuery("CustomKeys:ClearKey", conflictId); + updateKey(row, newData); + return true; + } + return false; + } + return true; +} + +async function onAction(event) { + const row = event.target.closest("tr"); + const keyId = row.dataset.id; + if (event.target.className == "reset") { + Glean.browserCustomkeys.actions.reset.add(); + const data = await RPMSendQuery("CustomKeys:GetDefaultKey", keyId); + if (await maybeHandleConflict(data)) { + const newData = await RPMSendQuery("CustomKeys:ResetKey", keyId); + updateKey(row, newData); + notifyUpdate(); + } + } else if (event.target.className == "change") { + Glean.browserCustomkeys.actions.change.add(); + // The "editing" class will cause the Change button to be replaced by a + // labelled input for the new key. + row.classList.add("editing"); + // We need to listen for keys in the parent process because we want to + // intercept reserved keys, which we can't do in the content process. + RPMSendAsyncMessage("CustomKeys:CaptureKey", true); + row.querySelector(".new").focus(); + } else if (event.target.className == "clear") { + Glean.browserCustomkeys.actions.clear.add(); + const newData = await RPMSendQuery("CustomKeys:ClearKey", keyId); + updateKey(row, newData); + notifyUpdate(); + } +} + +async function onKey({ data }) { + const input = document.activeElement; + const row = input.closest("tr"); + data.id = row.dataset.id; + if (data.isModifier) { + // This is a modifier. Display it, but don't assign yet. We assign when the + // main key is pressed (below). + input.value = data.modifierString; + // Select the input's text so screen readers will report it. + input.select(); + return; + } + if (await maybeHandleConflict(data)) { + const newData = await RPMSendQuery("CustomKeys:ChangeKey", data); + updateKey(row, newData); + } + RPMSendAsyncMessage("CustomKeys:CaptureKey", false); + row.classList.remove("editing"); + row.querySelector(".change").focus(); + notifyUpdate(); +} + +function onFocusLost(event) { + if (event.target.className == "new") { + // If the input loses focus, cancel editing of the key. + RPMSendAsyncMessage("CustomKeys:CaptureKey", false); + const row = event.target.closest("tr"); + row.classList.remove("editing"); + // Clear any modifiers that were displayed, ready for the next edit. + event.target.value = ""; + } +} + +function onSearchInput(event) { + const query = event.target.value.toLowerCase(); + for (const row of table.querySelectorAll(".key")) { + row.hidden = + query && !row.children[0].textContent.toLowerCase().includes(query); + } + for (const tbody of table.tBodies) { + // Show a category only if it has at least 1 shown key. + tbody.hidden = !tbody.querySelector(".key:not([hidden])"); + } + notifyUpdate(); +} + +async function onResetAll() { + Glean.browserCustomkeys.actions.reset_all.add(); + if ( + !window.confirm( + await document.l10n.formatValue("customkeys-reset-all-confirm") + ) + ) { + return; + } + await RPMSendQuery("CustomKeys:ResetAll"); + const keysByCat = await RPMSendQuery("CustomKeys:GetKeys"); + const keysById = {}; + for (const category in keysByCat) { + const categoryKeys = keysByCat[category]; + for (const keyId in categoryKeys) { + keysById[keyId] = categoryKeys[keyId]; + } + } + for (const row of table.querySelectorAll(".key")) { + const data = keysById[row.dataset.id]; + if (data) { + updateKey(row, data); + } + } + notifyUpdate(); +} + +buildTable(); +table.addEventListener("click", onAction); +RPMAddMessageListener("CustomKeys:CapturedKey", onKey); +table.addEventListener("focusout", onFocusLost); +document.getElementById("search").addEventListener("input", onSearchInput); +document.getElementById("resetAll").addEventListener("click", onResetAll); +Glean.browserCustomkeys.opened.add(); diff --git a/browser/components/customkeys/content/jar.mn b/browser/components/customkeys/content/jar.mn @@ -0,0 +1,8 @@ +# 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/. + +browser.jar: + content/browser/customkeys/customkeys.css + content/browser/customkeys/customkeys.html + content/browser/customkeys/customkeys.js diff --git a/browser/components/customkeys/content/moz.build b/browser/components/customkeys/content/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ["jar.mn"] diff --git a/browser/components/customkeys/metrics.yaml b/browser/components/customkeys/metrics.yaml @@ -0,0 +1,49 @@ +# 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/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Firefox :: Keyboard Navigation' + +browser.customkeys: + actions: + type: labeled_counter + description: > + The actions taken in about:keyboard. + labels: + - change + - clear + - reset + - reset_all + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1635774 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1635774 + data_sensitivity: + - interaction + notification_emails: + - accessibility@mozilla.com + - jteh@mozilla.com + - kbryant@mozilla.com + expires: 154 + + opened: + type: counter + description: > + Number of times about:keyboard has been opened. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1635774 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1635774 + data_sensitivity: + - interaction + notification_emails: + - accessibility@mozilla.com + - jteh@mozilla.com + - kbryant@mozilla.com + expires: 154 diff --git a/browser/components/customkeys/moz.build b/browser/components/customkeys/moz.build @@ -4,10 +4,19 @@ # 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/. +DIRS += [ + "content", +] + EXTRA_JS_MODULES += [ "CustomKeys.sys.mjs", ] +FINAL_TARGET_FILES.actors += [ + "CustomKeysChild.sys.mjs", + "CustomKeysParent.sys.mjs", +] + BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"] with Files("**"): diff --git a/browser/components/customkeys/tests/browser/browser.toml b/browser/components/customkeys/tests/browser/browser.toml @@ -2,3 +2,5 @@ support-files = ["head.js"] ["browser_CustomKeys.js"] + +["browser_aboutKeyboard.js"] diff --git a/browser/components/customkeys/tests/browser/browser_aboutKeyboard.js b/browser/components/customkeys/tests/browser/browser_aboutKeyboard.js @@ -0,0 +1,732 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +/** + * Test the about:keyboard UI. + */ + +registerCleanupFunction(async function () { + CustomKeys.resetAll(); + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); +}); + +function addAboutKbTask(task) { + const wrapped = function () { + return BrowserTestUtils.withNewTab("about:keyboard", async tab => { + await SpecialPowers.spawn(tab, [], async () => { + if (!content.document.getElementById("table").firstElementChild) { + await ContentTaskUtils.waitForEvent( + content, + "CustomKeysUpdate", + false, + null, + true + ); + } + }); + await Services.fog.testFlushAllChildren(); + await task(tab); + }); + }; + // Propagate the name of the task function to our wrapper function so it shows up in test run output. + Object.defineProperty(wrapped, "name", { value: task.name }); + add_task(wrapped); +} + +// Check telemetry before about:keyboard is first opened. +add_task(function testBeforeFirstOpen() { + ok(!Glean.browserCustomkeys.opened.testGetValue(), "No telemetry for opened"); +}); + +// Test initial loading of about:keyboard. +addAboutKbTask(async function testInit(tab) { + is( + Glean.browserCustomkeys.opened.testGetValue(), + 1, + "Correct telemetry for opened" + ); + await SpecialPowers.spawn(tab, [], () => { + Assert.greater( + content.document.querySelectorAll("tbody").length, + 5, + "At least 5 categories" + ); + const numKeys = content.document.querySelectorAll(".key").length; + Assert.greater(numKeys, 50, "At least 50 keys"); + is( + content.document.querySelectorAll("tbody[hidden], tr[hidden]").length, + 0, + "No hidden categories or keys" + ); + is( + content.document.querySelectorAll(".customized").length, + 0, + "No shortcuts are customized" + ); + // Currently, we don't have any unassigned shortcuts. That will probably + // change in future, at which point this next assertion will need to be + // reconsidered. + is( + content.document.querySelectorAll(".assigned").length, + numKeys, + "All keys are assigned" + ); + is( + content.document.querySelectorAll(".editing").length, + 0, + "No keys are being edited" + ); + }); +}); + +// Test searching. +addAboutKbTask(async function testSearch(tab) { + is( + Glean.browserCustomkeys.opened.testGetValue(), + 2, + "Correct telemetry for opened" + ); + await SpecialPowers.spawn(tab, [], async () => { + is( + content.document.querySelectorAll("tbody[hidden], tr[hidden]").length, + 0, + "No hidden categories or keys" + ); + const search = content.document.getElementById("search"); + search.focus(); + + info("Searching for zzz"); + let updated = ContentTaskUtils.waitForEvent( + content, + "CustomKeysUpdate", + false, + null, + true + ); + EventUtils.sendString("zzz", content); + await updated; + is( + content.document.querySelectorAll( + "tbody:not([hidden]), .key:not([hidden])" + ).length, + 0, + "No visible categories or keys" + ); + + info("Clearing search"); + updated = ContentTaskUtils.waitForEvent( + content, + "CustomKeysUpdate", + false, + null, + true + ); + EventUtils.synthesizeKey("KEY_Escape", {}, content); + await updated; + is( + content.document.querySelectorAll("tbody[hidden], tr[hidden]").length, + 0, + "No hidden categories or keys" + ); + + info("Searching for download"); + updated = ContentTaskUtils.waitForEvent( + content, + "CustomKeysUpdate", + false, + null, + true + ); + EventUtils.sendString("download", content); + await updated; + let visibleKeys = content.document.querySelectorAll(".key:not([hidden])"); + is(visibleKeys.length, 1, "1 visible key"); + is( + visibleKeys[0].dataset.id, + "key_openDownloads", + "Visible key is key_openDownloads" + ); + let visibleCategories = content.document.querySelectorAll( + "tbody:not([hidden])" + ); + is(visibleCategories.length, 1, "1 visible category"); + is( + visibleKeys[0].closest("tbody"), + visibleCategories[0], + "Visible key is inside visible category" + ); + ok( + !visibleCategories[0].querySelector(".category").hidden, + "Category header is visible" + ); + + info("Clearing search"); + updated = ContentTaskUtils.waitForEvent( + content, + "CustomKeysUpdate", + false, + null, + true + ); + EventUtils.synthesizeKey("KEY_Escape", {}, content); + await updated; + is( + content.document.querySelectorAll("tbody[hidden], tr[hidden]").length, + 0, + "No hidden categories or keys" + ); + + info("Searching for history"); + updated = ContentTaskUtils.waitForEvent( + content, + "CustomKeysUpdate", + false, + null, + true + ); + EventUtils.sendString("history", content); + await updated; + // This gives us results from both the Sidebars and History categories. + visibleKeys = content.document.querySelectorAll(".key:not([hidden])"); + is(visibleKeys.length, 3, "3 visible keys"); + visibleCategories = content.document.querySelectorAll( + "tbody:not([hidden])" + ); + is(visibleCategories.length, 2, "2 visible categories"); + }); +}); + +// Test a simple change. +addAboutKbTask(async function testChange(tab) { + ok( + !Glean.browserCustomkeys.actions.change.testGetValue(), + "No telemetry for change action" + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + content.downloadsRow = content.document.querySelector( + '.key[data-id="key_openDownloads"]' + ); + ok( + !content.downloadsRow.classList.contains("customized"), + "key_openDownloads is not customized" + ); + is( + content.downloadsRow.children[1].textContent, + _consts.downloadsDisplay, + "Key is the default key" + ); + info("Clicking Change for key_openDownloads"); + content.input = content.downloadsRow.querySelector(".new"); + let focused = ContentTaskUtils.waitForEvent(content.input, "focus"); + content.change = content.downloadsRow.querySelector(".change"); + content.change.click(); + await focused; + ok(true, "New key input got focus"); + content.selected = ContentTaskUtils.waitForEvent(content.input, "select"); + }); + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.change.testGetValue(), + 1, + "Correct telemetry for change action" + ); + info(`Pressing ${consts.unusedModifiersDisplay}`); + EventUtils.synthesizeKey(...consts.unusedModifiersArgs, window); + await SpecialPowers.spawn(tab, [consts], async _consts => { + await content.selected; + is( + content.input.value, + _consts.unusedModifiersDisplay, + "Input shows modifiers as they're pressed" + ); + info(`Pressing ${_consts.unusedDisplay}`); + content.focused = ContentTaskUtils.waitForEvent(content.change, "focus"); + }); + EventUtils.synthesizeKey(consts.unusedKey, consts.unusedOptions, window); + await SpecialPowers.spawn(tab, [consts], async _consts => { + await content.focused; + ok(true, "Change button got focus"); + ok( + content.downloadsRow.classList.contains("customized"), + "key_openDownloads is customized" + ); + is( + content.downloadsRow.children[1].textContent, + _consts.unusedDisplay, + "Key is the customized key" + ); + }); + // We deliberately let the result of this test leak into the next one. +}); + +// Test resetting a key. This also tests that the change from the previous test +// is reflected when the page is reloaded. +addAboutKbTask(async function testReset(tab) { + ok( + !Glean.browserCustomkeys.actions.reset.testGetValue(), + "No telemetry for reset action" + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + const downloadsRow = content.document.querySelector( + '.key[data-id="key_openDownloads"]' + ); + ok( + downloadsRow.classList.contains("customized"), + "key_openDownloads is customized" + ); + is( + downloadsRow.children[1].textContent, + _consts.unusedDisplay, + "Key is the customized key" + ); + info("Clicking Reset for key_openDownloads"); + const reset = downloadsRow.querySelector(".reset"); + let updated = ContentTaskUtils.waitForEvent( + content, + "CustomKeysUpdate", + false, + null, + true + ); + reset.click(); + await updated; + ok( + !downloadsRow.classList.contains("customized"), + "key_openDownloads is not customized" + ); + is( + downloadsRow.children[1].textContent, + _consts.downloadsDisplay, + "Key is the default key" + ); + }); + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.reset.testGetValue(), + 1, + "Correct telemetry for reset action" + ); +}); + +// Test clearing a key. +addAboutKbTask(async function testClear(tab) { + ok( + !Glean.browserCustomkeys.actions.clear.testGetValue(), + "No telemetry for clear action" + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + const downloadsRow = content.document.querySelector( + '.key[data-id="key_openDownloads"]' + ); + ok( + !downloadsRow.classList.contains("customized"), + "key_openDownloads is not customized" + ); + ok( + downloadsRow.classList.contains("assigned"), + "key_openDownloads is assigned" + ); + info("Clicking Clear for key_openDownloads"); + const clear = downloadsRow.querySelector(".clear"); + let updated = ContentTaskUtils.waitForEvent( + content, + "CustomKeysUpdate", + false, + null, + true + ); + clear.click(); + await updated; + ok( + downloadsRow.classList.contains("customized"), + "key_openDownloads is customized" + ); + ok( + !downloadsRow.classList.contains("assigned"), + "key_openDownloads is not assigned" + ); + is(downloadsRow.children[1].textContent, "", "Key is empty"); + }); + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.clear.testGetValue(), + 1, + "Correct telemetry for clear action" + ); + // We deliberately let the result of this test leak into the next one. +}); + +// Test resetting all keys. This depends on the state set up by the previous +// test. +addAboutKbTask(async function testResetAll(tab) { + ok( + !Glean.browserCustomkeys.actions.reset_all.testGetValue(), + "No telemetry for reset all action" + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + content.downloadsRow = content.document.querySelector( + '.key[data-id="key_openDownloads"]' + ); + ok( + content.downloadsRow.classList.contains("customized"), + "key_openDownloads is customized" + ); + ok( + !content.downloadsRow.classList.contains("assigned"), + "key_openDownloads is not assigned" + ); + }); + + info("Clicking Reset all, then Cancel"); + let handled = PromptTestUtils.handleNextPrompt( + window, + { modalType: Services.prompt.MODAL_TYPE_CONTENT }, + { buttonNumClick: 1 } + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + content.resetAll = content.document.getElementById("resetAll"); + content.resetAll.click(); + }); + await handled; + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.reset_all.testGetValue(), + 1, + "Correct telemetry for reset all action" + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + ok( + content.downloadsRow.classList.contains("customized"), + "key_openDownloads is customized" + ); + ok( + !content.downloadsRow.classList.contains("assigned"), + "key_openDownloads is not assigned" + ); + + info("Clicking Reset all, then OK"); + content.updated = ContentTaskUtils.waitForEvent( + content, + "CustomKeysUpdate", + false, + null, + true + ); + }); + + handled = PromptTestUtils.handleNextPrompt( + window, + { modalType: Services.prompt.MODAL_TYPE_CONTENT }, + { buttonNumClick: 0 } + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + content.resetAll.click(); + }); + await handled; + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.reset_all.testGetValue(), + 2, + "Correct telemetry for reset all action" + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + await content.updated; + ok( + !content.downloadsRow.classList.contains("customized"), + "key_openDownloads is not customized" + ); + ok( + content.downloadsRow.classList.contains("assigned"), + "key_openDownloads is assigned" + ); + }); +}); + +// Test a change which conflicts with another key. +addAboutKbTask(async function testConflictingChange(tab) { + await SpecialPowers.spawn(tab, [consts], async _consts => { + content.downloadsRow = content.document.querySelector( + '.key[data-id="key_openDownloads"]' + ); + ok( + !content.downloadsRow.classList.contains("customized"), + "key_openDownloads is not customized" + ); + content.historyRow = content.document.querySelector( + '.key[data-id="key_gotoHistory"]' + ); + ok( + !content.historyRow.classList.contains("customized"), + "key_gotoHistory is not customized" + ); + + info("Clicking Change for key_openDownloads"); + content.input = content.downloadsRow.querySelector(".new"); + let focused = ContentTaskUtils.waitForEvent(content.input, "focus"); + content.change = content.downloadsRow.querySelector(".change"); + content.change.click(); + await focused; + ok(true, "New key input got focus"); + content.focused = ContentTaskUtils.waitForEvent(content.change, "focus"); + }); + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.change.testGetValue(), + 2, + "Correct telemetry for change action" + ); + info(`Pressing ${consts.historyDisplay}, then clicking Cancel`); + let handled = PromptTestUtils.handleNextPrompt( + window, + { modalType: Services.prompt.MODAL_TYPE_CONTENT }, + { buttonNumClick: 1 } + ); + EventUtils.synthesizeKey("H", consts.historyOptions, window); + await handled; + await SpecialPowers.spawn(tab, [consts], async _consts => { + await content.focused; + ok(true, "Change button got focus"); + ok( + !content.downloadsRow.classList.contains("customized"), + "key_openDownloads is not customized" + ); + ok( + !content.historyRow.classList.contains("customized"), + "key_gotoHistory is not customized" + ); + + info("Clicking Change for key_openDownloads"); + let focused = ContentTaskUtils.waitForEvent(content.input, "focus"); + content.change.click(); + await focused; + ok(true, "New key input got focus"); + content.focused = ContentTaskUtils.waitForEvent(content.change, "focus"); + }); + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.change.testGetValue(), + 3, + "Correct telemetry for change action" + ); + info(`Pressing ${consts.historyDisplay}, then clicking OK`); + handled = PromptTestUtils.handleNextPrompt( + window, + { modalType: Services.prompt.MODAL_TYPE_CONTENT }, + { buttonNumClick: 0 } + ); + EventUtils.synthesizeKey("H", consts.historyOptions, window); + await handled; + await SpecialPowers.spawn(tab, [consts], async _consts => { + await content.focused; + ok(true, "Change button got focus"); + ok( + content.downloadsRow.classList.contains("customized"), + "key_openDownloads is customized" + ); + ok( + content.downloadsRow.classList.contains("assigned"), + "key_openDownloads is assigned" + ); + is( + content.downloadsRow.children[1].textContent, + _consts.historyDisplay, + "Key is the customized key" + ); + ok( + content.historyRow.classList.contains("customized"), + "key_gotoHistory is customized" + ); + ok( + !content.historyRow.classList.contains("assigned"), + "key_gotoHistory is not assigned" + ); + is(content.historyRow.children[1].textContent, "", "Key is empty"); + }); + // We deliberately let the result of this test leak into the next one. +}); + +// Test a reset which conflicts with another key. This depends on the state set +// up by the previous test. +addAboutKbTask(async function testConflictingReset(tab) { + await SpecialPowers.spawn(tab, [consts], async _consts => { + content.downloadsRow = content.document.querySelector( + '.key[data-id="key_openDownloads"]' + ); + ok( + content.downloadsRow.classList.contains("customized"), + "key_openDownloads is customized" + ); + content.historyRow = content.document.querySelector( + '.key[data-id="key_gotoHistory"]' + ); + ok( + content.historyRow.classList.contains("customized"), + "key_gotoHistory is customized" + ); + }); + + info("Clicking Reset for key_gotoHistory, then Cancel"); + let handled = PromptTestUtils.handleNextPrompt( + window, + { modalType: Services.prompt.MODAL_TYPE_CONTENT }, + { buttonNumClick: 1 } + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + content.reset = content.historyRow.querySelector(".reset"); + content.reset.click(); + }); + await handled; + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.reset.testGetValue(), + 2, + "Correct telemetry for reset action" + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + ok( + content.downloadsRow.classList.contains("customized"), + "key_openDownloads is customized" + ); + ok( + content.historyRow.classList.contains("customized"), + "key_gotoHistory is customized" + ); + }); + + info("Clicking Reset for key_gotoHistory, then OK"); + handled = PromptTestUtils.handleNextPrompt( + window, + { modalType: Services.prompt.MODAL_TYPE_CONTENT }, + { buttonNumClick: 0 } + ); + await SpecialPowers.spawn(tab, [], async () => { + content.updated = ContentTaskUtils.waitForEvent( + content, + "CustomKeysUpdate", + false, + null, + true + ); + content.reset.click(); + }); + await handled; + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.reset.testGetValue(), + 3, + "Correct telemetry for reset action" + ); + await SpecialPowers.spawn(tab, [consts], async _consts => { + await content.updated; + ok( + content.downloadsRow.classList.contains("customized"), + "key_openDownloads is customized" + ); + ok( + !content.downloadsRow.classList.contains("assigned"), + "key_openDownloads is not assigned" + ); + is(content.downloadsRow.children[1].textContent, "", "Key is empty"); + ok( + !content.historyRow.classList.contains("customized"), + "key_gotoHistory is not customized" + ); + ok( + content.historyRow.classList.contains("assigned"), + "key_gotoHistory is assigned" + ); + is( + content.historyRow.children[1].textContent, + _consts.historyDisplay, + "Key is the default key" + ); + }); + + CustomKeys.resetAll(); +}); + +// Test that a reserved key is captured correctly. +addAboutKbTask(async function testReservedKey(tab) { + await SpecialPowers.spawn(tab, [consts], async _consts => { + content.downloadsRow = content.document.querySelector( + '.key[data-id="key_openDownloads"]' + ); + info("Clicking Change for key_openDownloads"); + content.input = content.downloadsRow.querySelector(".new"); + let focused = ContentTaskUtils.waitForEvent(content.input, "focus"); + content.change = content.downloadsRow.querySelector(".change"); + content.change.click(); + await focused; + ok(true, "New key input got focus"); + content.focused = ContentTaskUtils.waitForEvent(content.change, "focus"); + }); + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.change.testGetValue(), + 4, + "Correct telemetry for change action" + ); + info(`Pressing ${consts.newWindowDisplay}, then clicking Cancel`); + let handled = PromptTestUtils.handleNextPrompt( + window, + { modalType: Services.prompt.MODAL_TYPE_CONTENT }, + { buttonNumClick: 1 } + ); + EventUtils.synthesizeKey("N", { accelKey: true }, window); + await handled; + await SpecialPowers.spawn(tab, [], async () => { + await content.focused; + ok(true, "Change button got focus"); + }); +}); + +// Test that changing to a function key works correctly; i.e. that we handle key +// vs keycode. +addAboutKbTask(async function testFunctionKey(tab) { + await SpecialPowers.spawn(tab, [consts], async _consts => { + content.downloadsRow = content.document.querySelector( + '.key[data-id="key_openDownloads"]' + ); + ok( + !content.downloadsRow.classList.contains("customized"), + "key_openDownloads is not customized" + ); + info("Clicking Change for key_openDownloads"); + content.input = content.downloadsRow.querySelector(".new"); + let focused = ContentTaskUtils.waitForEvent(content.input, "focus"); + content.change = content.downloadsRow.querySelector(".change"); + content.change.click(); + await focused; + ok(true, "New key input got focus"); + content.focused = ContentTaskUtils.waitForEvent(content.change, "focus"); + }); + await Services.fog.testFlushAllChildren(); + is( + Glean.browserCustomkeys.actions.change.testGetValue(), + 5, + "Correct telemetry for change action" + ); + info("Pressing F1"); + EventUtils.synthesizeKey("KEY_F1", {}, window); + await SpecialPowers.spawn(tab, [consts], async _consts => { + await content.focused; + ok(true, "Change button got focus"); + ok( + content.downloadsRow.classList.contains("customized"), + "key_openDownloads is customized" + ); + is( + content.downloadsRow.children[1].textContent, + "F1", + "Key is the customized key" + ); + }); + CustomKeys.resetAll(); +}); diff --git a/browser/components/customkeys/tests/browser/head.js b/browser/components/customkeys/tests/browser/head.js @@ -13,6 +13,10 @@ const consts = { historyDisplay: isMac ? "⇧⌘H" : "Ctrl+H", historyModifiers: isMac ? "accel,shift" : "accel", historyOptions: { accelKey: true, shiftKey: isMac }, + // key_openDownloads + downloadsDisplay: (isMac && "⌘J") || (isLinux && "Ctrl+Shift+Y") || "Ctrl+J", + // key_newNavigator + newWindowDisplay: isMac ? "⌘N" : "Ctrl+N", // The following unused* constants specify a key combination which is unused by // default. This will need to be updated if this key combination is assigned to @@ -21,5 +25,6 @@ const consts = { unusedOptions: { accelKey: true, shiftKey: true }, unusedKey: isLinux ? "Q" : "Y", unusedModifiersDisplay: isMac ? "⇧⌘" : "Ctrl+Shift+", + unusedModifiersArgs: ["KEY_Shift", { accelKey: true }], }; consts.unusedDisplay = `${consts.unusedModifiersDisplay}${consts.unusedKey}`; diff --git a/browser/locales/en-US/browser/customkeys.ftl b/browser/locales/en-US/browser/customkeys.ftl @@ -0,0 +1,42 @@ +# 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/. + +customkeys-title = Keyboard Shortcuts +customkeys-search = Search: +customkeys-change = Change +customkeys-reset = Reset +customkeys-clear = Clear +customkeys-new-key = Press new key: +customkeys-reset-all = Reset all shortcuts to defaults + +# Variables +# $conflict (string) - The title of the conflicting shortcut. +customkeys-conflict-confirm = This key is already assigned to { $conflict }. Do you want to replace it? + +customkeys-reset-all-confirm = Are you sure you wish to reset all keyboard shortcuts to their defaults? + +customkeys-history-reopen-tab = Reopen Last Closed Tab +customkeys-history-reopen-window = Reopen Last Closed window +customkeys-dev-tools = Web Developer Tools +customkeys-dev-inspector = DOM and Style Inspector +customkeys-dev-webconsole = Web Console +customkeys-dev-debugger = JavaScript Debugger +customkeys-dev-network = Network Monitor +customkeys-dev-style = Style Editor +customkeys-dev-performance = Performance +customkeys-dev-storage = Storage Inspector +customkeys-dev-dom = DOM +customkeys-dev-accessibility = Accessibility +customkeys-dev-profiler-toggle = Start/Stop the Performance Profiler +customkeys-dev-profiler-capture = Capture a Performance Profile + +customkeys-category-navigation = Navigation +customkeys-nav-back = Back +customkeys-nav-forward = Forward +customkeys-nav-home = Home +customkeys-nav-reload = Reload +customkeys-nav-reload-skip-cache = Reload (Override Cache) +customkeys-nav-stop = Stop + +customkeys-caution-message = This feature is experimental and may not work as expected. diff --git a/eslint-file-globals.config.mjs b/eslint-file-globals.config.mjs @@ -364,6 +364,7 @@ export default [ files: [ "browser/base/content/aboutRestartRequired.js", "browser/base/content/aboutTabCrashed.js", + "browser/components/customkeys/content/customkeys.js", "browser/components/privatebrowsing/content/aboutPrivateBrowsing.js", "browser/components/profiles/content/delete-profile-card.mjs", "browser/components/profiles/content/edit-profile-card.mjs", diff --git a/toolkit/components/glean/metrics_index.py b/toolkit/components/glean/metrics_index.py @@ -121,6 +121,7 @@ firefox_desktop_metrics = [ "browser/components/attribution/metrics.yaml", "browser/components/backup/metrics.yaml", "browser/components/contextualidentity/metrics.yaml", + "browser/components/customkeys/metrics.yaml", "browser/components/downloads/metrics.yaml", "browser/components/extensions/metrics.yaml", "browser/components/firefoxview/metrics.yaml", diff --git a/toolkit/modules/RemotePageAccessManager.sys.mjs b/toolkit/modules/RemotePageAccessManager.sys.mjs @@ -74,6 +74,18 @@ export let RemotePageAccessManager = { "about:certificate": { RPMSendQuery: ["getCertificates"], }, + "about:keyboard": { + RPMAddMessageListener: ["CustomKeys:CapturedKey"], + RPMSendAsyncMessage: ["CustomKeys:CaptureKey"], + RPMSendQuery: [ + "CustomKeys:ChangeKey", + "CustomKeys:ClearKey", + "CustomKeys:GetDefaultKey", + "CustomKeys:GetKeys", + "CustomKeys:ResetAll", + "CustomKeys:ResetKey", + ], + }, "about:neterror": { RPMSendAsyncMessage: [ "Browser:EnableOnlineMode", diff --git a/tools/lint/fluent-lint/exclusions.yml b/tools/lint/fluent-lint/exclusions.yml @@ -144,6 +144,8 @@ CO01: - identity-description-custom-root2 # browser/components/urlbar/content/enUS-searchFeatures.ftl - addressbar-firefox-suggest-online + # browser/customkeys.ftl + - customkeys-dev-network # browser/migrationWizard.ftl - migration-wizard-migrator-display-name-firefox # browser/newtab/onboarding.ftl