summaryrefslogtreecommitdiffstats
path: root/browser/components/screenshots/ScreenshotsUtils.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/screenshots/ScreenshotsUtils.sys.mjs')
-rw-r--r--browser/components/screenshots/ScreenshotsUtils.sys.mjs242
1 files changed, 215 insertions, 27 deletions
diff --git a/browser/components/screenshots/ScreenshotsUtils.sys.mjs b/browser/components/screenshots/ScreenshotsUtils.sys.mjs
index 9df74a4359..4ba925366d 100644
--- a/browser/components/screenshots/ScreenshotsUtils.sys.mjs
+++ b/browser/components/screenshots/ScreenshotsUtils.sys.mjs
@@ -91,6 +91,7 @@ export class ScreenshotsComponentParent extends JSWindowActorParent {
case "Screenshots:OverlaySelection":
ScreenshotsUtils.setPerBrowserState(browser, {
hasOverlaySelection: message.data.hasSelection,
+ overlayState: message.data.overlayState,
});
break;
case "Screenshots:ShowPanel":
@@ -99,6 +100,9 @@ export class ScreenshotsComponentParent extends JSWindowActorParent {
case "Screenshots:HidePanel":
ScreenshotsUtils.closePanel(browser);
break;
+ case "Screenshots:MoveFocusToParent":
+ ScreenshotsUtils.focusPanel(browser, message.data);
+ break;
}
}
@@ -191,11 +195,7 @@ export var ScreenshotsUtils = {
handleEvent(event) {
switch (event.type) {
case "keydown":
- if (event.key === "Escape") {
- // Escape should cancel and exit
- let browser = event.view.gBrowser.selectedBrowser;
- this.cancel(browser, "escape");
- }
+ this.handleKeyDownEvent(event);
break;
case "TabSelect":
this.handleTabSelect(event);
@@ -209,6 +209,33 @@ export var ScreenshotsUtils = {
}
},
+ handleKeyDownEvent(event) {
+ let browser =
+ event.view.browsingContext.topChromeWindow.gBrowser.selectedBrowser;
+ if (!browser) {
+ return;
+ }
+
+ switch (event.key) {
+ case "Escape":
+ // The chromeEventHandler in the child actor will handle events that
+ // don't match this
+ if (event.target.parentElement === this.panelForBrowser(browser)) {
+ this.cancel(browser, "escape");
+ }
+ break;
+ case "ArrowLeft":
+ case "ArrowUp":
+ case "ArrowRight":
+ case "ArrowDown":
+ this.handleArrowKeyDown(event, browser);
+ break;
+ case "Tab":
+ this.maybeLockFocus(event);
+ break;
+ }
+ },
+
/**
* When we swap docshells for a given screenshots browser, we need to update
* the browserToScreenshotsState WeakMap to the correct browser. If the old
@@ -273,6 +300,105 @@ export var ScreenshotsUtils = {
}
},
+ /**
+ * If the overlay state is crosshairs or dragging, move the native cursor
+ * respective to the arrow key pressed.
+ * @param {Event} event A keydown event
+ * @param {Browser} browser The selected browser
+ * @returns
+ */
+ handleArrowKeyDown(event, browser) {
+ // Wayland doesn't support `sendNativeMouseEvent` so just return
+ if (Services.appinfo.isWayland) {
+ return;
+ }
+
+ let { overlayState } = this.browserToScreenshotsState.get(browser);
+
+ if (!["crosshairs", "dragging"].includes(overlayState)) {
+ return;
+ }
+
+ let left = 0;
+ let top = 0;
+ let exponent = event.shiftKey ? 1 : 0;
+ switch (event.key) {
+ case "ArrowLeft":
+ left -= 10 ** exponent;
+ break;
+ case "ArrowUp":
+ top -= 10 ** exponent;
+ break;
+ case "ArrowRight":
+ left += 10 ** exponent;
+ break;
+ case "ArrowDown":
+ top += 10 ** exponent;
+ break;
+ default:
+ return;
+ }
+
+ // Clear and move focus to browser so the child actor can capture events
+ this.clearContentFocus(browser);
+ Services.focus.clearFocus(browser.ownerGlobal);
+ Services.focus.setFocus(browser, 0);
+
+ let x = {};
+ let y = {};
+ let win = browser.ownerGlobal;
+ win.windowUtils.getLastOverWindowPointerLocationInCSSPixels(x, y);
+
+ this.moveCursor(
+ {
+ left: (x.value + left) * win.devicePixelRatio,
+ top: (y.value + top) * win.devicePixelRatio,
+ },
+ browser
+ );
+ },
+
+ /**
+ * Move the native cursor to the given position. Clamp the position to the
+ * window just in case.
+ * @param {Object} position An object containing the left and top position
+ * @param {Browser} browser The selected browser
+ */
+ moveCursor(position, browser) {
+ let { left, top } = position;
+ let win = browser.ownerGlobal;
+
+ const windowLeft = win.mozInnerScreenX * win.devicePixelRatio;
+ const windowTop = win.mozInnerScreenY * win.devicePixelRatio;
+ const contentTop =
+ (win.mozInnerScreenY + (win.innerHeight - browser.clientHeight)) *
+ win.devicePixelRatio;
+ const windowRight =
+ (win.mozInnerScreenX + win.innerWidth) * win.devicePixelRatio;
+ const windowBottom =
+ (win.mozInnerScreenY + win.innerHeight) * win.devicePixelRatio;
+
+ left += windowLeft;
+ top += windowTop;
+
+ // Clamp left and top to content dimensions
+ let parsedLeft = Math.round(
+ Math.min(Math.max(left, windowLeft), windowRight)
+ );
+ let parsedTop = Math.round(
+ Math.min(Math.max(top, contentTop), windowBottom)
+ );
+
+ win.windowUtils.sendNativeMouseEvent(
+ parsedLeft,
+ parsedTop,
+ win.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE,
+ 0,
+ 0,
+ win.document.documentElement
+ );
+ },
+
observe(subj, topic, data) {
let { gBrowser } = subj;
let browser = gBrowser.selectedBrowser;
@@ -335,6 +461,7 @@ export var ScreenshotsUtils = {
browser.addEventListener("SwapDocShells", this);
let gBrowser = browser.getTabBrowser();
gBrowser.tabContainer.addEventListener("TabSelect", this);
+ browser.ownerDocument.addEventListener("keydown", this);
break;
}
case UIPhases.INITIAL:
@@ -364,6 +491,7 @@ export var ScreenshotsUtils = {
browser.removeEventListener("SwapDocShells", this);
const gBrowser = browser.getTabBrowser();
gBrowser.tabContainer.removeEventListener("TabSelect", this);
+ browser.ownerDocument.removeEventListener("keydown", this);
this.browserToScreenshotsState.delete(browser);
if (Cu.isInAutomation) {
@@ -396,6 +524,53 @@ export var ScreenshotsUtils = {
Object.assign(perBrowserState, nameValues);
},
+ maybeLockFocus(event) {
+ let browser = event.view.gBrowser.selectedBrowser;
+
+ if (!Services.focus.focusedElement) {
+ event.preventDefault();
+ this.focusPanel(browser);
+ return;
+ }
+
+ let target = event.explicitOriginalTarget;
+
+ if (!target.closest("moz-button-group")) {
+ return;
+ }
+
+ let isElementFirst = !!target.nextElementSibling;
+
+ if (
+ (isElementFirst && event.shiftKey) ||
+ (!isElementFirst && !event.shiftKey)
+ ) {
+ event.preventDefault();
+ this.moveFocusToContent(browser);
+ }
+ },
+
+ focusPanel(browser, { direction } = {}) {
+ let buttonsPanel = this.panelForBrowser(browser);
+ if (direction) {
+ buttonsPanel
+ .querySelector("screenshots-buttons")
+ .focusButton(direction === "forward" ? "first" : "last");
+ } else {
+ buttonsPanel
+ .querySelector("screenshots-buttons")
+ .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD);
+ }
+ },
+
+ moveFocusToContent(browser) {
+ this.getActor(browser).sendAsyncMessage("Screenshots:MoveFocusToContent");
+ },
+
+ clearContentFocus(browser) {
+ this.getActor(browser).sendAsyncMessage("Screenshots:ClearFocus");
+ },
+
/**
* Attempt to place focus on the element that had focus before screenshots UI was shown
*
@@ -510,7 +685,7 @@ export var ScreenshotsUtils = {
async openPreviewDialog(browser) {
let dialogBox = browser.ownerGlobal.gBrowser.getTabDialogBox(browser);
let { dialog, closedPromise } = await dialogBox.open(
- `chrome://browser/content/screenshots/screenshots.html?browsingContextId=${browser.browsingContext.id}`,
+ `chrome://browser/content/screenshots/screenshots-preview.html?browsingContextId=${browser.browsingContext.id}`,
{
features: "resizable=no",
sizeTo: "available",
@@ -586,14 +761,18 @@ export var ScreenshotsUtils = {
openPanel(browser) {
let buttonsPanel = this.panelForBrowser(browser);
if (!buttonsPanel.hidden) {
- return;
+ return null;
}
buttonsPanel.hidden = false;
- buttonsPanel.ownerDocument.addEventListener("keydown", this);
- buttonsPanel
- .querySelector("screenshots-buttons")
- .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD);
+ return new Promise(resolve => {
+ browser.ownerGlobal.requestAnimationFrame(() => {
+ buttonsPanel
+ .querySelector("screenshots-buttons")
+ .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD);
+ resolve();
+ });
+ });
},
/**
@@ -606,7 +785,6 @@ export var ScreenshotsUtils = {
return;
}
buttonsPanel.hidden = true;
- buttonsPanel.ownerDocument.removeEventListener("keydown", this);
},
/**
@@ -652,7 +830,6 @@ export var ScreenshotsUtils = {
let currTabDialogBox = browser.tabDialogBox;
let browserContextId = browser.browsingContext.id;
if (currTabDialogBox) {
- currTabDialogBox.getTabDialogManager();
let manager = currTabDialogBox.getTabDialogManager();
let dialogs = manager.hasDialogs && manager.dialogs;
if (dialogs.length) {
@@ -661,7 +838,7 @@ export var ScreenshotsUtils = {
dialog._openedURL.endsWith(
`browsingContextId=${browserContextId}`
) &&
- dialog._openedURL.includes("screenshots.html")
+ dialog._openedURL.includes("screenshots-preview.html")
) {
return dialog;
}
@@ -817,12 +994,11 @@ export var ScreenshotsUtils = {
let dialog = await this.openPreviewDialog(browser);
await dialog._dialogReady;
- let screenshotsUI = dialog._frame.contentDocument.createElement(
+ let screenshotsPreviewEl = dialog._frame.contentDocument.querySelector(
"screenshots-preview"
);
- dialog._frame.contentDocument.body.appendChild(screenshotsUI);
- screenshotsUI.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD);
+ screenshotsPreviewEl.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD);
let rect;
let lastUsedMethod;
@@ -852,15 +1028,12 @@ export var ScreenshotsUtils = {
async takeScreenshot(browser, dialog, rect) {
let canvas = await this.createCanvas(rect, browser);
- let newImg = dialog._frame.contentDocument.createElement("img");
let url = canvas.toDataURL();
+ let screenshotsPreviewEl = dialog._frame.contentDocument.querySelector(
+ "screenshots-preview"
+ );
- newImg.id = "placeholder-image";
-
- newImg.src = url;
- dialog._frame.contentDocument
- .getElementById("preview-image-div")
- .appendChild(newImg);
+ screenshotsPreviewEl.previewImg.src = url;
if (Cu.isInAutomation) {
Services.obs.notifyObservers(null, "screenshots-preview-ready");
@@ -1051,14 +1224,19 @@ export var ScreenshotsUtils = {
* @param dataUrl The image data
* @param browser The current browser
* @param data Telemetry data
+ * @returns true if the download succeeds, otherwise false
*/
async downloadScreenshot(title, dataUrl, browser, data) {
// Guard against missing image data.
if (!dataUrl) {
- return;
+ return false;
}
- let filename = await getFilename(title, browser);
+ let { filename, accepted } = await getFilename(title, browser);
+
+ if (!accepted) {
+ return false;
+ }
const targetFile = new lazy.FileUtils.File(filename);
@@ -1080,7 +1258,15 @@ export var ScreenshotsUtils = {
// Await successful completion of the save via the download manager
await download.start();
- } catch (ex) {}
+ } catch (ex) {
+ console.error(
+ `Failed to create download using filename: ${filename} (length: ${
+ new Blob([filename]).size
+ })`
+ );
+
+ return false;
+ }
let extra = await this.getActor(browser).sendQuery(
"Screenshots:GetMethodsUsed"
@@ -1095,6 +1281,8 @@ export var ScreenshotsUtils = {
SCREENSHOTS_LAST_SAVED_METHOD_PREF,
"download"
);
+
+ return true;
},
recordTelemetryEvent(type, object, args) {