diff --git a/browser/actors/ScreenshotsComponentChild.jsm b/browser/actors/ScreenshotsComponentChild.jsm new file mode 100644 index 000000000000..8a8bd7e921f6 --- /dev/null +++ b/browser/actors/ScreenshotsComponentChild.jsm @@ -0,0 +1,156 @@ +/* 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/. */ +/* eslint-env mozilla/browser-window */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["ScreenshotsComponentChild"]; + +class ScreenshotsComponentChild extends JSWindowActorChild { + receiveMessage(message) { + switch (message.name) { + case "Screenshots:ShowOverlay": + return this.startScreenshotsOverlay(); + case "Screenshots:HideOverlay": + return this.endScreenshotsOverlay(); + case "Screenshots:getFullPageBounds": + return this.getFullPageBounds(); + case "Screenshots:getVisibleBounds": + return this.getVisibleBounds(); + } + return null; + } + + /** + * Resolves when the document is ready to have an overlay injected into it. + * + * @returns {Promise} + * @resolves {Boolean} true when document is ready or rejects + */ + documentIsReady() { + const document = this.document; + // Some pages take ages to finish loading - if at all. + // We want to respond to enable the screenshots UI as soon that is possible + function readyEnough() { + return ( + document.readyState !== "uninitialized" && document.documentElement + ); + } + + if (readyEnough()) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + function onChange(event) { + if (event.type === "pagehide") { + document.removeEventListener("readystatechange", onChange); + this.contentWindow.removeEventListener("pagehide", onChange); + reject(new Error("document unloaded before it was ready")); + } else if (readyEnough()) { + document.removeEventListener("readystatechange", onChange); + this.contentWindow.removeEventListener("pagehide", onChange); + resolve(); + } + } + document.addEventListener("readystatechange", onChange); + this.contentWindow.addEventListener("pagehide", onChange, { once: true }); + }); + } + + /** + * Wait until the document is ready and then show the screenshots overlay + * + * @returns {Boolean} true when document is ready and the overlay is shown + * otherwise false + */ + async startScreenshotsOverlay(details = {}) { + try { + await this.documentIsReady(); + } catch (ex) { + console.warn(`ScreenshotsComponentChild: ${ex.message}`); + return false; + } + return true; + } + + /** + * Remove the screenshots overlay. + * + * @returns {Boolean} + * true when the overlay has been removed otherwise false + */ + endScreenshotsOverlay() { + // this function will be implemented soon + return true; + } + + /** + * Gets the full page bounds for a full page screenshot. + * + * @returns { object } + * The device pixel ratio and a DOMRect of the scrollable content bounds. + * + * devicePixelRatio (float): + * The device pixel ratio of the screen + * + * rect (object): + * top (int): + * The scroll top position for the content window. + * + * left (int): + * The scroll left position for the content window. + * + * width (int): + * The scroll width of the content window. + * + * height (int): + * The scroll height of the content window. + */ + getFullPageBounds() { + let doc = this.document.documentElement; + let rect = new DOMRect( + doc.scrollTop, + doc.scrollLeft, + doc.scrollWidth, + doc.scrollHeight + ); + let devicePixelRatio = this.document.ownerGlobal.devicePixelRatio; + return { devicePixelRatio, rect }; + } + + /** + * Gets the visible page bounds for a visible screenshot. + * + * @returns { object } + * The device pixel ratio and a DOMRect of the current visible + * content bounds. + * + * devicePixelRatio (float): + * The device pixel ratio of the screen + * + * rect (object): + * top (int): + * The top position for the content window. + * + * left (int): + * The left position for the content window. + * + * width (int): + * The width of the content window. + * + * height (int): + * The height of the content window. + */ + getVisibleBounds() { + let doc = this.document.documentElement; + let rect = new DOMRect( + doc.clientTop, + doc.clientLeft, + doc.clientWidth, + doc.clientHeight + ); + let devicePixelRatio = this.document.ownerGlobal.devicePixelRatio; + return { devicePixelRatio, rect }; + } +} diff --git a/browser/actors/moz.build b/browser/actors/moz.build index 2b3a706d0e49..723f5df9af17 100644 --- a/browser/actors/moz.build +++ b/browser/actors/moz.build @@ -25,6 +25,9 @@ with Files("PageStyleChild.jsm"): with Files("PluginChild.jsm"): BUG_COMPONENT = ("Core", "Plug-ins") +with Files("ScreenshotsComponentChild.jsm"): + BUG_COMPONENT = ("Firefox", "Screenshots") + with Files("WebRTCChild.jsm"): BUG_COMPONENT = ("Firefox", "Site Permissions") @@ -79,6 +82,7 @@ FINAL_TARGET_FILES.actors += [ "RefreshBlockerParent.jsm", "RFPHelperChild.jsm", "RFPHelperParent.jsm", + "ScreenshotsComponentChild.jsm", "SearchSERPTelemetryChild.jsm", "SearchSERPTelemetryParent.jsm", "SwitchDocumentDirectionChild.jsm", diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index ee3fec90c2f4..ec6a50b3dbc8 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -645,6 +645,13 @@ let JSWINDOWACTORS = { enablePreference: "accessibility.blockautorefresh", }, + ScreenshotsComponent: { + child: { + moduleURI: "resource:///actors/ScreenshotsComponentChild.jsm", + }, + enablePreference: "screenshots.browser.component.enabled", + }, + SearchSERPTelemetry: { parent: { moduleURI: "resource:///actors/SearchSERPTelemetryParent.jsm", diff --git a/browser/components/screenshots/ScreenshotsUtils.jsm b/browser/components/screenshots/ScreenshotsUtils.jsm index fb21c6f93824..07edec99faf5 100644 --- a/browser/components/screenshots/ScreenshotsUtils.jsm +++ b/browser/components/screenshots/ScreenshotsUtils.jsm @@ -14,6 +14,14 @@ const PanelOffsetY = -8; var ScreenshotsUtils = { initialize() { + if ( + !Services.prefs.getBoolPref( + "screenshots.browser.component.enabled", + false + ) + ) { + return; + } Services.obs.addObserver(this, "menuitem-screenshot"); Services.obs.addObserver(this, "screenshots-take-screenshot"); }, @@ -23,6 +31,8 @@ var ScreenshotsUtils = { let currDialogBox = browser.tabDialogBox; + let zoom = subj.ZoomManager.getZoomForBrowser(browser); + switch (topic) { case "menuitem-screenshot": // if dialog box exists then find the correct dialog box and close it @@ -56,7 +66,7 @@ var ScreenshotsUtils = { // init UI as a tab dialog box let dialogBox = gBrowser.getTabDialogBox(browser); - return dialogBox.open( + let { dialog } = dialogBox.open( `chrome://browser/content/screenshots/screenshots.html?browsingContextId=${browser.browsingContext.id}`, { features: "resizable=no", @@ -64,9 +74,15 @@ var ScreenshotsUtils = { allowDuplicateDialogs: false, } ); + this.doScreenshot(browser, dialog, zoom, 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( @@ -77,16 +93,40 @@ var ScreenshotsUtils = { 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; + }, + /** + * If the buttons panel exists and the panel is open we will hipe the panel + * popup and hide the screenshot overlay. + * Otherwise create or display the buttons. + * @param browser The current browser. + */ togglePreview(browser) { let buttonsPanel = browser.ownerDocument.querySelector( "#screenshotsPagePanel" ); if (buttonsPanel && buttonsPanel.state !== "closed") { buttonsPanel.hidePopup(); - } else { - this.createOrDisplayButtons(browser); + let actor = this.getActor(browser); + return actor.sendQuery("Screenshots:HideOverlay"); } + return this.createOrDisplayButtons(browser); }, + /** + * 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"); @@ -98,5 +138,90 @@ var ScreenshotsUtils = { } let anchor = doc.querySelector("#navigator-toolbox"); buttonsPanel.openPopup(anchor, PanelPosition, PanelOffsetX, PanelOffsetY); + let actor = this.getActor(browser); + return actor.sendQuery("Screenshots:ShowOverlay"); + }, + /** + * 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 zoom The current zoom level. + * @param type The type of screenshot taken. + */ + async doScreenshot(browser, dialog, zoom, 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, zoom); + }, + /** + * 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. + * @param zoom The current zoom level. + */ + async takeScreenshot(browser, dialog, rect, zoom) { + let browsingContext = BrowsingContext.get(browser.browsingContext.id); + + let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot( + rect, + zoom, + "rgb(255,255,255)" + ); + + let canvas = dialog._frame.contentDocument.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); + + canvas.toBlob(function(blob) { + let newImg = dialog._frame.contentDocument.createElement("img"); + let url = URL.createObjectURL(blob); + + newImg.id = "placeholder-image"; + + newImg.src = url; + dialog._frame.contentDocument + .getElementById("preview-image-div") + .appendChild(newImg); + }); + + snapshot.close(); }, }; diff --git a/browser/components/screenshots/content/screenshots.html b/browser/components/screenshots/content/screenshots.html index 94c9e90fe6d8..b3893c4c1193 100644 --- a/browser/components/screenshots/content/screenshots.html +++ b/browser/components/screenshots/content/screenshots.html @@ -37,7 +37,5 @@ - - diff --git a/browser/components/screenshots/content/screenshots.js b/browser/components/screenshots/content/screenshots.js index 067d34eb1022..f5a712bb9003 100644 --- a/browser/components/screenshots/content/screenshots.js +++ b/browser/components/screenshots/content/screenshots.js @@ -21,8 +21,6 @@ class ScreenshotsUI extends HTMLElement { } async connectedCallback() { this.initialize(); - - await this.takeVisibleScreenshot(); } initialize() {