From a90a5cba08fdf6c0ceb95101c275108a152a3aed Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:35:37 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- .../screenshots/ScreenshotsUtils.sys.mjs | 242 ++++++++++++++++++--- 1 file changed, 215 insertions(+), 27 deletions(-) (limited to 'browser/components/screenshots/ScreenshotsUtils.sys.mjs') 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) { -- cgit v1.2.3