diff options
Diffstat (limited to 'browser/components/screenshots/ScreenshotsUtils.sys.mjs')
-rw-r--r-- | browser/components/screenshots/ScreenshotsUtils.sys.mjs | 459 |
1 files changed, 459 insertions, 0 deletions
diff --git a/browser/components/screenshots/ScreenshotsUtils.sys.mjs b/browser/components/screenshots/ScreenshotsUtils.sys.mjs new file mode 100644 index 0000000000..d0f827acf3 --- /dev/null +++ b/browser/components/screenshots/ScreenshotsUtils.sys.mjs @@ -0,0 +1,459 @@ +/* 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 { getFilename } from "chrome://browser/content/screenshots/fileHelpers.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Downloads: "resource://gre/modules/Downloads.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +const PanelPosition = "bottomright topright"; +const PanelOffsetX = -33; +const PanelOffsetY = -8; + +export class ScreenshotsComponentParent extends JSWindowActorParent { + async receiveMessage(message) { + let browser = message.target.browsingContext.topFrameElement; + switch (message.name) { + case "Screenshots:CancelScreenshot": + await ScreenshotsUtils.closePanel(browser); + break; + case "Screenshots:CopyScreenshot": + await ScreenshotsUtils.closePanel(browser); + let copyBox = message.data; + ScreenshotsUtils.copyScreenshotFromRegion(copyBox, browser); + break; + case "Screenshots:DownloadScreenshot": + await ScreenshotsUtils.closePanel(browser); + let { title, downloadBox } = message.data; + ScreenshotsUtils.downloadScreenshotFromRegion( + title, + downloadBox, + browser + ); + break; + case "Screenshots:ShowPanel": + ScreenshotsUtils.createOrDisplayButtons(browser); + break; + case "Screenshots:HidePanel": + ScreenshotsUtils.closePanel(browser); + break; + } + } + + didDestroy() { + // When restoring a crashed tab the browser is null + let browser = this.browsingContext.topFrameElement; + if (browser) { + ScreenshotsUtils.closePanel(browser); + } + } +} + +export var ScreenshotsUtils = { + initialized: false, + initialize() { + if (!this.initialized) { + if ( + !Services.prefs.getBoolPref( + "screenshots.browser.component.enabled", + false + ) + ) { + return; + } + Services.obs.addObserver(this, "menuitem-screenshot"); + Services.obs.addObserver(this, "screenshots-take-screenshot"); + this.initialized = true; + if (Cu.isInAutomation) { + Services.obs.notifyObservers(null, "screenshots-component-initialized"); + } + } + }, + uninitialize() { + if (this.initialized) { + Services.obs.removeObserver(this, "menuitem-screenshot"); + Services.obs.removeObserver(this, "screenshots-take-screenshot"); + this.initialized = false; + } + }, + handleEvent(event) { + if (event.type === "keydown" && event.key === "Escape") { + this.closePanel(event.view.gBrowser.selectedBrowser, true); + } + }, + observe(subj, topic, data) { + let { gBrowser } = subj; + let browser = gBrowser.selectedBrowser; + + switch (topic) { + case "menuitem-screenshot": + let success = this.closeDialogBox(browser); + if (!success || data === "retry") { + // only toggle the buttons if no dialog box is found because + // if dialog box is found then the buttons are hidden and we return early + // else no dialog box is found and we need to toggle the buttons + // or if retry because the dialog box was closed and we need to show the panel + this.togglePanelAndOverlay(browser); + } + break; + case "screenshots-take-screenshot": + // need to close the preview because screenshot was taken + this.closePanel(browser, true); + + // init UI as a tab dialog box + let dialogBox = gBrowser.getTabDialogBox(browser); + + let { dialog } = dialogBox.open( + `chrome://browser/content/screenshots/screenshots.html?browsingContextId=${browser.browsingContext.id}`, + { + features: "resizable=no", + sizeTo: "available", + allowDuplicateDialogs: false, + } + ); + this.doScreenshot(browser, dialog, data); + } + return null; + }, + /** + * Notify screenshots when screenshot command is used. + * @param window The current window the screenshot command was used. + * @param type The type of screenshot taken. Used for telemetry. + */ + notify(window, type) { + if (Services.prefs.getBoolPref("screenshots.browser.component.enabled")) { + Services.obs.notifyObservers( + window.event.currentTarget.ownerGlobal, + "menuitem-screenshot" + ); + } else { + Services.obs.notifyObservers(null, "menuitem-screenshot-extension", type); + } + }, + /** + * Creates and returns a Screenshots actor. + * @param browser The current browser. + * @returns JSWindowActor The screenshot actor. + */ + getActor(browser) { + let actor = browser.browsingContext.currentWindowGlobal.getActor( + "ScreenshotsComponent" + ); + return actor; + }, + /** + * Open the panel buttons and call child actor to open the overlay + * @param browser The current browser + */ + openPanel(browser) { + let actor = this.getActor(browser); + actor.sendQuery("Screenshots:ShowOverlay"); + this.createOrDisplayButtons(browser); + }, + /** + * Close the panel and call child actor to close the overlay + * @param browser The current browser + * @param {bool} closeOverlay Whether or not to + * send a message to the child to close the overly. + * Defaults to false. Will be false when called from didDestroy. + */ + async closePanel(browser, closeOverlay = false) { + let buttonsPanel = browser.ownerDocument.querySelector( + "#screenshotsPagePanel" + ); + if (buttonsPanel && buttonsPanel.state !== "closed") { + buttonsPanel.hidePopup(); + } + buttonsPanel?.ownerDocument.removeEventListener("keydown", this); + if (closeOverlay) { + let actor = this.getActor(browser); + await actor.sendQuery("Screenshots:HideOverlay"); + } + }, + /** + * If the buttons panel exists and is open we will hide both the panel + * and the overlay. If the overlay is showing, we will hide the overlay. + * Otherwise create or display the buttons. + * @param browser The current browser. + */ + async togglePanelAndOverlay(browser) { + let buttonsPanel = browser.ownerDocument.querySelector( + "#screenshotsPagePanel" + ); + let isOverlayShowing = await this.getActor(browser).sendQuery( + "Screenshots:isOverlayShowing" + ); + if (buttonsPanel && (isOverlayShowing || buttonsPanel.state !== "closed")) { + buttonsPanel.hidePopup(); + let actor = this.getActor(browser); + return actor.sendQuery("Screenshots:HideOverlay"); + } + let actor = this.getActor(browser); + actor.sendQuery("Screenshots:ShowOverlay"); + return this.createOrDisplayButtons(browser); + }, + /** + * Gets the screenshots dialog box + * @param browser The selected browser + * @returns Screenshots dialog box if it exists otherwise null + */ + getDialog(browser) { + 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) { + for (let dialog of dialogs) { + if ( + dialog._openedURL.endsWith( + `browsingContextId=${browserContextId}` + ) && + dialog._openedURL.includes("screenshots.html") + ) { + return dialog; + } + } + } + } + return null; + }, + /** + * Closes the dialog box it it exists + * @param browser The selected browser + */ + closeDialogBox(browser) { + let dialog = this.getDialog(browser); + if (dialog) { + dialog.close(); + return true; + } + return false; + }, + /** + * If the buttons panel does not exist then we will replace the buttons + * panel template with the buttons panel then open the buttons panel and + * show the screenshots overaly. + * @param browser The current browser. + */ + createOrDisplayButtons(browser) { + let doc = browser.ownerDocument; + let buttonsPanel = doc.querySelector("#screenshotsPagePanel"); + if (!buttonsPanel) { + let template = doc.querySelector("#screenshotsPagePanelTemplate"); + let clone = template.content.cloneNode(true); + template.replaceWith(clone); + buttonsPanel = doc.querySelector("#screenshotsPagePanel"); + } + + buttonsPanel.ownerDocument.addEventListener("keydown", this); + + let anchor = doc.querySelector("#navigator-toolbox"); + buttonsPanel.openPopup(anchor, PanelPosition, PanelOffsetX, PanelOffsetY); + }, + /** + * Gets the full page bounds from the screenshots child actor. + * @param browser The current browser. + * @returns { object } + * Contains the full page bounds from the screenshots child actor. + */ + fetchFullPageBounds(browser) { + let actor = this.getActor(browser); + return actor.sendQuery("Screenshots:getFullPageBounds"); + }, + /** + * Gets the visible bounds from the screenshots child actor. + * @param browser The current browser. + * @returns { object } + * Contains the visible bounds from the screenshots child actor. + */ + fetchVisibleBounds(browser) { + let actor = this.getActor(browser); + return actor.sendQuery("Screenshots:getVisibleBounds"); + }, + /** + * Add screenshot-ui to the dialog box and then take the screenshot + * @param browser The current browser. + * @param dialog The dialog box to show the screenshot preview. + * @param type The type of screenshot taken. + */ + async doScreenshot(browser, dialog, type) { + await dialog._dialogReady; + let screenshotsUI = dialog._frame.contentDocument.createElement( + "screenshots-ui" + ); + dialog._frame.contentDocument.body.appendChild(screenshotsUI); + + let rect; + if (type === "full-page") { + rect = await this.fetchFullPageBounds(browser); + } else { + rect = await this.fetchVisibleBounds(browser); + } + return this.takeScreenshot(browser, dialog, rect); + }, + /** + * Take the screenshot and add the image to the dialog box + * @param browser The current browser. + * @param dialog The dialog box to show the screenshot preview. + * @param rect DOMRect containing bounds of the screenshot. + */ + async takeScreenshot(browser, dialog, rect) { + let { canvas, snapshot } = await this.createCanvas(rect, browser); + + let newImg = dialog._frame.contentDocument.createElement("img"); + let url = canvas.toDataURL(); + + newImg.id = "placeholder-image"; + + newImg.src = url; + dialog._frame.contentDocument + .getElementById("preview-image-div") + .appendChild(newImg); + + if (Cu.isInAutomation) { + Services.obs.notifyObservers(null, "screenshots-preview-ready"); + } + + snapshot.close(); + }, + /** + * Creates a canvas and draws a snapshot of the screenshot on the canvas + * @param box The bounds of screenshots + * @param browser The current browser + * @returns The canvas and snapshot in an object + */ + async createCanvas(box, browser) { + let rect = new DOMRect(box.x1, box.y1, box.width, box.height); + let { devicePixelRatio } = box; + + let browsingContext = BrowsingContext.get(browser.browsingContext.id); + + let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot( + rect, + devicePixelRatio, + "rgb(255,255,255)" + ); + + let canvas = browser.ownerDocument.createElementNS( + "http://www.w3.org/1999/xhtml", + "html:canvas" + ); + let context = canvas.getContext("2d"); + + canvas.width = snapshot.width; + canvas.height = snapshot.height; + + context.drawImage(snapshot, 0, 0); + + return { canvas, snapshot }; + }, + /** + * Copy the screenshot + * @param region The bounds of the screenshots + * @param browser The current browser + */ + async copyScreenshotFromRegion(region, browser) { + let { canvas, snapshot } = await this.createCanvas(region, browser); + + let url = canvas.toDataURL(); + + this.copyScreenshot(url, browser); + + snapshot.close(); + }, + /** + * Copy the image to the clipboard + * @param dataUrl The image data + */ + copyScreenshot(dataUrl) { + // Guard against missing image data. + if (!dataUrl) { + return; + } + + const imageTools = Cc["@mozilla.org/image/tools;1"].getService( + Ci.imgITools + ); + + const base64Data = dataUrl.replace("data:image/png;base64,", ""); + + const image = atob(base64Data); + const imgDecoded = imageTools.decodeImageFromBuffer( + image, + image.length, + "image/png" + ); + + const transferable = Cc[ + "@mozilla.org/widget/transferable;1" + ].createInstance(Ci.nsITransferable); + transferable.init(null); + transferable.addDataFlavor("image/png"); + transferable.setTransferData("image/png", imgDecoded); + + Services.clipboard.setData( + transferable, + null, + Services.clipboard.kGlobalClipboard + ); + }, + /** + * Download the screenshot + * @param title The title of the current page + * @param box The bounds of the screenshot + * @param browser The current browser + */ + async downloadScreenshotFromRegion(title, box, browser) { + let { canvas, snapshot } = await this.createCanvas(box, browser); + + let dataUrl = canvas.toDataURL(); + + await this.downloadScreenshot(title, dataUrl, browser); + + snapshot.close(); + }, + /** + * Download the screenshot + * @param title The title of the current page or null and getFilename will get the title + * @param dataUrl The image data + * @param browser The current browser + */ + async downloadScreenshot(title, dataUrl, browser) { + // Guard against missing image data. + if (!dataUrl) { + return; + } + + let filename = await getFilename(title, browser); + + const targetFile = new lazy.FileUtils.File(filename); + + // Create download and track its progress. + try { + const download = await lazy.Downloads.createDownload({ + source: dataUrl, + target: targetFile, + }); + + let isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate( + browser.ownerGlobal + ); + const list = await lazy.Downloads.getList( + isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC + ); + // add the download to the download list in the Downloads list in the Browser UI + list.add(download); + + // Await successful completion of the save via the download manager + await download.start(); + } catch (ex) {} + }, +}; |