Bug 1864038 - Add keyboard shortcuts for screenshots overlay. r=sfoster,fluent-reviewers,bolsson,desktop-theme-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D193847
This commit is contained in:
Niklas Baumgardner 2024-04-01 13:51:16 +00:00
parent d9cc111e59
commit a335bce89a
11 changed files with 421 additions and 84 deletions

View File

@ -37,6 +37,7 @@ import {
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { ShortcutUtils } from "resource://gre/modules/ShortcutUtils.sys.mjs";
const STATES = {
CROSSHAIRS: "crosshairs",
@ -49,7 +50,7 @@ const STATES = {
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
return new Localization(["browser/screenshotsOverlay.ftl"], true);
return new Localization(["browser/screenshots.ftl"], true);
});
const SCREENSHOTS_LAST_SAVED_METHOD_PREF =
@ -79,13 +80,33 @@ export class ScreenshotsOverlay {
#methodsUsed;
get markup() {
let [cancel, instructions, download, copy] =
lazy.overlayLocalization.formatMessagesSync([
{ id: "screenshots-overlay-cancel-button" },
{ id: "screenshots-overlay-instructions" },
{ id: "screenshots-overlay-download-button" },
{ id: "screenshots-overlay-copy-button" },
]);
let accelString = ShortcutUtils.getModifierString("accel");
let copyShorcut = accelString + this.copyKey;
let downloadShortcut = accelString + this.downloadKey;
let [
cancelLabel,
cancelAttributes,
instructions,
downloadLabel,
downloadAttributes,
copyLabel,
copyAttributes,
] = lazy.overlayLocalization.formatMessagesSync([
{ id: "screenshots-cancel-button" },
{ id: "screenshots-component-cancel-button" },
{ id: "screenshots-instructions" },
{ id: "screenshots-component-download-button-label" },
{
id: "screenshots-component-download-button",
args: { shortcut: downloadShortcut },
},
{ id: "screenshots-component-copy-button-label" },
{
id: "screenshots-component-copy-button",
args: { shortcut: copyShorcut },
},
]);
return `
<template>
@ -98,7 +119,7 @@ export class ScreenshotsOverlay {
<div class="face"></div>
</div>
<div class="preview-instructions">${instructions.value}</div>
<button class="screenshots-button ghost-button" id="screenshots-cancel-button">${cancel.value}</button>
<button class="screenshots-button ghost-button" id="screenshots-cancel-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}">${cancelLabel.value}</button>
</div>
<div id="hover-highlight" hidden></div>
<div id="selection-container" hidden>
@ -138,9 +159,9 @@ export class ScreenshotsOverlay {
</div>
<div id="buttons-container" hidden>
<div class="buttons-wrapper">
<button id="cancel" class="screenshots-button" title="${cancel.value}" aria-label="${cancel.value}" tabindex="0"><img/></button>
<button id="copy" class="screenshots-button" title="${copy.value}" aria-label="${copy.value}" tabindex="0"><img/>${copy.value}</button>
<button id="download" class="screenshots-button primary" title="${download.value}" aria-label="${download.value}" tabindex="0"><img/>${download.value}</button>
<button id="cancel" class="screenshots-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}"><img/></button>
<button id="copy" class="screenshots-button" title="${copyAttributes.attributes[0].value}" aria-label="${copyAttributes.attributes[1].value}"><img/><label>${copyLabel.value}</label></button>
<button id="download" class="screenshots-button primary" title="${downloadAttributes.attributes[0].value}" aria-label="${downloadAttributes.attributes[1].value}"><img/><label>${downloadLabel.value}</label></button>
</div>
</div>
</div>
@ -180,6 +201,14 @@ export class ScreenshotsOverlay {
this.selectionRegion = new Region(this.windowDimensions);
this.hoverElementRegion = new Region(this.windowDimensions);
this.resetMethodsUsed();
let [downloadKey, copyKey] = lazy.overlayLocalization.formatMessagesSync([
{ id: "screenshots-component-download-key" },
{ id: "screenshots-component-copy-key" },
]);
this.downloadKey = downloadKey.value;
this.copyKey = copyKey.value;
}
get content() {
@ -351,14 +380,10 @@ export class ScreenshotsOverlay {
this.maybeCancelScreenshots();
break;
case "copy":
this.#dispatchEvent("Screenshots:Copy", {
region: this.selectionRegion.dimensions,
});
this.copySelectedRegion();
break;
case "download":
this.#dispatchEvent("Screenshots:Download", {
region: this.selectionRegion.dimensions,
});
this.downloadSelectedRegion();
break;
}
}
@ -486,6 +511,18 @@ export class ScreenshotsOverlay {
case "Escape":
this.maybeCancelScreenshots();
break;
case this.copyKey.toLowerCase():
if (this.state === "selected" && this.getAccelKey(event)) {
event.preventDefault();
this.copySelectedRegion();
}
break;
case this.downloadKey.toLowerCase():
if (this.state === "selected" && this.getAccelKey(event)) {
event.preventDefault();
this.downloadSelectedRegion();
}
break;
}
}
@ -904,6 +941,18 @@ export class ScreenshotsOverlay {
}
}
copySelectedRegion() {
this.#dispatchEvent("Screenshots:Copy", {
region: this.selectionRegion.dimensions,
});
}
downloadSelectedRegion() {
this.#dispatchEvent("Screenshots:Download", {
region: this.selectionRegion.dimensions,
});
}
/**
* Hide hover element, selection and buttons containers.
* Show the preview container and the panel.
@ -1332,7 +1381,7 @@ export class ScreenshotsOverlay {
let [selectionSizeTranslation] =
lazy.overlayLocalization.formatMessagesSync([
{
id: "screenshots-overlay-selection-region-size",
id: "screenshots-overlay-selection-region-size-2",
args: {
width: Math.floor(width * zoom),
height: Math.floor(height * zoom),

View File

@ -817,8 +817,9 @@ export var ScreenshotsUtils = {
let dialog = await this.openPreviewDialog(browser);
await dialog._dialogReady;
let screenshotsUI =
dialog._frame.contentDocument.createElement("screenshots-ui");
let screenshotsUI = dialog._frame.contentDocument.createElement(
"screenshots-preview"
);
dialog._frame.contentDocument.body.appendChild(screenshotsUI);
screenshotsUI.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD);

View File

@ -30,13 +30,12 @@ body {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-xsmall);
cursor: pointer;
text-align: center;
user-select: none;
white-space: nowrap;
min-height: 36px;
font-size: 15px;
min-width: 36px;
min-width: 32px;
}
.preview-button > img {
@ -44,11 +43,23 @@ body {
fill: currentColor;
width: 16px;
height: 16px;
pointer-events: none;
}
#retry > img {
content: url("chrome://global/skin/icons/reload.svg");
}
#cancel > img {
content: url("chrome://global/skin/icons/close.svg");
}
#download > img,
#copy > img {
margin-inline-end: 5px;
content: url("chrome://global/skin/icons/edit-copy.svg");
}
#download > img {
content: url("chrome://browser/skin/downloads/downloads.svg");
}
.preview-image {

View File

@ -31,32 +31,34 @@
<button
id="retry"
class="preview-button"
data-l10n-id="screenshots-retry-button-title"
data-l10n-id="screenshots-component-retry-button"
>
<img src="chrome://global/skin/icons/reload.svg" />
<img />
</button>
<button
id="cancel"
class="preview-button"
data-l10n-id="screenshots-cancel-button-title"
data-l10n-id="screenshots-component-cancel-button"
>
<img src="chrome://global/skin/icons/close.svg" />
<img />
</button>
<button
id="copy"
class="preview-button"
data-l10n-id="screenshots-copy-button-title"
data-l10n-id="screenshots-component-copy-button"
>
<img src="chrome://global/skin/icons/edit-copy.svg" />
<span data-l10n-id="screenshots-copy-button" />
<img /><label
data-l10n-id="screenshots-component-copy-button-label"
></label>
</button>
<button
id="download"
class="preview-button primary"
data-l10n-id="screenshots-download-button-title"
data-l10n-id="screenshots-component-download-button"
>
<img src="chrome://browser/skin/downloads/downloads.svg" />
<span data-l10n-id="screenshots-download-button" />
<img /><label
data-l10n-id="screenshots-component-download-button-label"
></label>
</button>
</div>
<div class="preview-image">

View File

@ -6,15 +6,35 @@
"use strict";
ChromeUtils.defineESModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
});
class ScreenshotsUI extends HTMLElement {
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "screenshotsLocalization", () => {
return new Localization(["browser/screenshots.ftl"], true);
});
class ScreenshotsPreview extends HTMLElement {
constructor() {
super();
// we get passed the <browser> as a param via TabDialogBox.open()
this.openerBrowser = window.arguments[0];
window.ensureCustomElements("moz-button");
let [downloadKey, copyKey] =
lazy.screenshotsLocalization.formatMessagesSync([
{ id: "screenshots-component-download-key" },
{ id: "screenshots-component-copy-key" },
]);
this.downloadKey = downloadKey.value;
this.copyKey = copyKey.value;
}
async connectedCallback() {
this.initialize();
}
@ -38,6 +58,31 @@ class ScreenshotsUI extends HTMLElement {
this._copyButton.addEventListener("click", this);
this._downloadButton = this.querySelector("#download");
this._downloadButton.addEventListener("click", this);
let accelString = ShortcutUtils.getModifierString("accel");
let copyShorcut = accelString + this.copyKey;
let downloadShortcut = accelString + this.downloadKey;
document.l10n.setAttributes(
this._cancelButton,
AppConstants.platform === "macosx"
? "screenshots-component-cancel-mac-button"
: "screenshots-component-cancel-button"
);
document.l10n.setAttributes(
this._copyButton,
"screenshots-component-copy-button",
{ shortcut: copyShorcut }
);
document.l10n.setAttributes(
this._downloadButton,
"screenshots-component-download-button",
{ shortcut: downloadShortcut }
);
window.addEventListener("keydown", this, true);
}
close() {
@ -45,33 +90,70 @@ class ScreenshotsUI extends HTMLElement {
window.close();
}
async handleEvent(event) {
if (event.type == "click" && event.currentTarget == this._cancelButton) {
this.close();
ScreenshotsUtils.recordTelemetryEvent("canceled", "preview_cancel", {});
} else if (
event.type == "click" &&
event.currentTarget == this._copyButton
) {
this.saveToClipboard(
this.ownerDocument.getElementById("placeholder-image").src
);
} else if (
event.type == "click" &&
event.currentTarget == this._downloadButton
) {
await this.saveToFile(
this.ownerDocument.getElementById("placeholder-image").src
);
} else if (
event.type == "click" &&
event.currentTarget == this._retryButton
) {
ScreenshotsUtils.scheduleRetry(this.openerBrowser, "preview_retry");
this.close();
handleEvent(event) {
switch (event.type) {
case "click":
this.handleClick(event);
break;
case "keydown":
this.handleKeydown(event);
break;
}
}
handleClick(event) {
switch (event.target.id) {
case "retry":
ScreenshotsUtils.scheduleRetry(this.openerBrowser, "preview_retry");
this.close();
break;
case "cancel":
this.close();
ScreenshotsUtils.recordTelemetryEvent("canceled", "preview_cancel", {});
break;
case "copy":
this.saveToClipboard(
this.ownerDocument.getElementById("placeholder-image").src
);
break;
case "download":
this.saveToFile(
this.ownerDocument.getElementById("placeholder-image").src
);
break;
}
}
handleKeydown(event) {
switch (event.key) {
case this.copyKey.toLowerCase():
if (this.getAccelKey(event)) {
event.preventDefault();
event.stopPropagation();
this.saveToClipboard(
this.ownerDocument.getElementById("placeholder-image").src
);
}
break;
case this.downloadKey.toLowerCase():
if (this.getAccelKey(event)) {
event.preventDefault();
event.stopPropagation();
this.saveToFile(
this.ownerDocument.getElementById("placeholder-image").src
);
}
break;
}
}
getAccelKey(event) {
if (AppConstants.platform === "macosx") {
return event.metaKey;
}
return event.ctrlKey;
}
async saveToFile(dataUrl) {
await ScreenshotsUtils.downloadScreenshot(
null,
@ -102,4 +184,4 @@ class ScreenshotsUI extends HTMLElement {
}
}
}
customElements.define("screenshots-ui", ScreenshotsUI);
customElements.define("screenshots-preview", ScreenshotsPreview);

View File

@ -84,6 +84,8 @@
.screenshots-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-xsmall);
cursor: pointer;
text-align: center;
user-select: none;
@ -143,11 +145,6 @@
content: url("chrome://browser/skin/downloads/downloads.svg");
}
#download > img,
#copy > img {
margin-inline-end: 5px;
}
.face-container {
position: relative;
width: 64px;

View File

@ -18,6 +18,8 @@ prefs = [
["browser_iframe_test.js"]
skip-if = ["os == 'linux'"]
["browser_keyboard_shortcuts.js"]
["browser_overlay_keyboard_test.js"]
["browser_screenshots_drag_scroll_test.js"]

View File

@ -0,0 +1,128 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_download_shortcut() {
await SpecialPowers.pushPrefEnv({
set: [["browser.download.useDownloadDir", true]],
});
let publicDownloads = await Downloads.getList(Downloads.PUBLIC);
// First ensure we catch the download finishing.
let downloadFinishedPromise = new Promise(resolve => {
publicDownloads.addView({
onDownloadChanged(download) {
info("Download changed!");
if (download.succeeded || download.error) {
info("Download succeeded or errored");
publicDownloads.removeView(this);
resolve(download);
}
},
});
});
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_PAGE,
},
async browser => {
let helper = new ScreenshotsHelper(browser);
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
await helper.dragOverlay(10, 10, 500, 500);
let screenshotExit = TestUtils.topicObserved("screenshots-exit");
await SpecialPowers.spawn(browser, [], async () => {
EventUtils.synthesizeKey("s", { accelKey: true }, content);
});
info("wait for download to finish");
let download = await downloadFinishedPromise;
ok(download.succeeded, "Download should succeed");
await publicDownloads.removeFinished();
await screenshotExit;
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
let screenshotReady = TestUtils.topicObserved(
"screenshots-preview-ready"
);
let visibleButton = await helper.getPanelButton(".visible-page");
visibleButton.click();
await screenshotReady;
screenshotExit = TestUtils.topicObserved("screenshots-exit");
EventUtils.synthesizeKey("s", { accelKey: true });
info("wait for download to finish");
download = await downloadFinishedPromise;
ok(download.succeeded, "Download should succeed");
await publicDownloads.removeFinished();
await screenshotExit;
}
);
});
add_task(async function test_copy_shortcut() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_PAGE,
},
async browser => {
let helper = new ScreenshotsHelper(browser);
let contentInfo = await helper.getContentDimensions();
ok(contentInfo, "Got dimensions back from the content");
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
await helper.dragOverlay(10, 10, 500, 500);
let screenshotExit = TestUtils.topicObserved("screenshots-exit");
let clipboardChanged = helper.waitForRawClipboardChange(490, 490);
await SpecialPowers.spawn(browser, [], async () => {
EventUtils.synthesizeKey("c", { accelKey: true }, content);
});
await clipboardChanged;
await screenshotExit;
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
let screenshotReady = TestUtils.topicObserved(
"screenshots-preview-ready"
);
let visibleButton = await helper.getPanelButton(".visible-page");
visibleButton.click();
await screenshotReady;
clipboardChanged = helper.waitForRawClipboardChange(
contentInfo.clientWidth,
contentInfo.clientHeight
);
screenshotExit = TestUtils.topicObserved("screenshots-exit");
EventUtils.synthesizeKey("c", { accelKey: true });
await clipboardChanged;
await screenshotExit;
}
);
});

View File

@ -23,8 +23,6 @@ screenshots-copy-button-title =
.title = Copy screenshot to clipboard
screenshots-cancel-button-title =
.title = Cancel
screenshots-retry-button-title =
.title = Retry screenshot
screenshots-meta-key = {
PLATFORM() ->
@ -58,3 +56,46 @@ screenshots-generic-error-details = Were not sure what just happened. Care to
screenshots-too-large-error-title = Your screenshot was cropped because it was too large
screenshots-too-large-error-details = Try selecting a region thats smaller than 32,700 pixels on its longest side or 124,900,000 pixels total area.
screenshots-component-retry-button =
.title = Retry screenshot
.aria-label = Retry screenshot
screenshots-component-cancel-button =
.title =
{ PLATFORM() ->
[macos] Cancel (esc)
*[other] Cancel (Esc)
}
.aria-label = Cancel
# Variables
# $shortcut (String) - A keyboard shortcut for copying the screenshot.
screenshots-component-copy-button =
.title = Copy ({ $shortcut })
.aria-label = Copy
screenshots-component-copy-button-label = Copy
# Variables
# $shortcut (String) - A keyboard shortcut for saving/downloading the screenshot.
screenshots-component-download-button =
.title = Download ({ $shortcut })
.aria-label = Download
screenshots-component-download-button-label = Download
## The below strings are used to capture keydown events so the strings should
## not be changed unless the keyboard layout in the locale requires it.
screenshots-component-download-key = S
screenshots-component-copy-key = C
##
# This string represents the selection size area
# "x" here represents "by" (i.e 123 by 456)
# Variables:
# $width (Number) - The width of the selection region in pixels
# $height (Number) - The height of the selection region in pixels
screenshots-overlay-selection-region-size-2 = { $width } x { $height }

View File

@ -1,15 +0,0 @@
# 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/.
screenshots-overlay-cancel-button = Cancel
screenshots-overlay-instructions = Drag or click on the page to select a region. Press ESC to cancel.
screenshots-overlay-download-button = Download
screenshots-overlay-copy-button = Copy
# This string represents the selection size area
# "x" here represents "by" (i.e 123 by 456)
# Variables:
# $width (Number) - The width of the selection region in pixels
# $height (Number) - The height of the selection region in pixels
screenshots-overlay-selection-region-size = { $width } x { $height }

View File

@ -0,0 +1,39 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
from fluent.migrate.helpers import transforms_from
def migrate(ctx):
"""Bug 1864038 - Consolidate screenshots fluent files, part {index}."""
source = "browser/browser/screenshotsOverlay.ftl"
target = "browser/browser/screenshots.ftl"
ctx.add_transforms(
target,
target,
transforms_from(
"""
screenshots-component-copy-button-label = {COPY_PATTERN(from_path, "screenshots-overlay-copy-button")}
screenshots-component-download-button-label = {COPY_PATTERN(from_path, "screenshots-overlay-download-button")}
screenshots-overlay-selection-region-size-2 = {COPY_PATTERN(from_path, "screenshots-overlay-selection-region-size")}
""",
from_path=source,
),
)
ctx.add_transforms(
target,
target,
transforms_from(
"""
screenshots-component-retry-button =
.title = {COPY_PATTERN(from_path, "screenshots-retry-button-title.title")}
.aria-label = {COPY_PATTERN(from_path, "screenshots-retry-button-title.title")}
""",
from_path=target,
),
)