1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7var EXPORTED_SYMBOLS = ["ScreenshotsUtils", "ScreenshotsComponentParent"]; 8 9const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 10 11const PanelPosition = "bottomright topright"; 12const PanelOffsetX = -33; 13const PanelOffsetY = -8; 14 15class ScreenshotsComponentParent extends JSWindowActorParent { 16 receiveMessage(message) { 17 switch (message.name) { 18 case "Screenshots:CancelScreenshot": 19 let browser = message.target.browsingContext.topFrameElement; 20 ScreenshotsUtils.closePanel(browser); 21 } 22 } 23 24 didDestroy() { 25 // When restoring a crashed tab the browser is null 26 let browser = this.browsingContext.topFrameElement; 27 if (browser) { 28 ScreenshotsUtils.closePanel(browser, false); 29 } 30 } 31} 32 33var ScreenshotsUtils = { 34 initialized: false, 35 initialize() { 36 if (!this.initialized) { 37 if ( 38 !Services.prefs.getBoolPref( 39 "screenshots.browser.component.enabled", 40 false 41 ) 42 ) { 43 return; 44 } 45 Services.obs.addObserver(this, "menuitem-screenshot"); 46 Services.obs.addObserver(this, "screenshots-take-screenshot"); 47 this.initialized = true; 48 if (Cu.isInAutomation) { 49 Services.obs.notifyObservers(null, "screenshots-component-initialized"); 50 } 51 } 52 }, 53 uninitialize() { 54 if (this.initialized) { 55 Services.obs.removeObserver(this, "menuitem-screenshot"); 56 Services.obs.removeObserver(this, "screenshots-take-screenshot"); 57 this.initialized = false; 58 } 59 }, 60 observe(subj, topic, data) { 61 let { gBrowser } = subj; 62 let browser = gBrowser.selectedBrowser; 63 64 let zoom = subj.ZoomManager.getZoomForBrowser(browser); 65 66 switch (topic) { 67 case "menuitem-screenshot": 68 let success = this.closeDialogBox(browser); 69 if (!success || data === "retry") { 70 // only toggle the buttons if no dialog box is found because 71 // if dialog box is found then the buttons are hidden and we return early 72 // else no dialog box is found and we need to toggle the buttons 73 // or if retry because the dialog box was closed and we need to show the panel 74 this.togglePreview(browser); 75 } 76 break; 77 case "screenshots-take-screenshot": 78 // need to close the preview because screenshot was taken 79 this.closePanel(browser); 80 81 // init UI as a tab dialog box 82 let dialogBox = gBrowser.getTabDialogBox(browser); 83 84 let { dialog } = dialogBox.open( 85 `chrome://browser/content/screenshots/screenshots.html?browsingContextId=${browser.browsingContext.id}`, 86 { 87 features: "resizable=no", 88 sizeTo: "available", 89 allowDuplicateDialogs: false, 90 } 91 ); 92 this.doScreenshot(browser, dialog, zoom, data); 93 } 94 return null; 95 }, 96 /** 97 * Notify screenshots when screenshot command is used. 98 * @param window The current window the screenshot command was used. 99 * @param type The type of screenshot taken. Used for telemetry. 100 */ 101 notify(window, type) { 102 if (Services.prefs.getBoolPref("screenshots.browser.component.enabled")) { 103 Services.obs.notifyObservers( 104 window.event.currentTarget.ownerGlobal, 105 "menuitem-screenshot" 106 ); 107 } else { 108 Services.obs.notifyObservers(null, "menuitem-screenshot-extension", type); 109 } 110 }, 111 /** 112 * Creates and returns a Screenshots actor. 113 * @param browser The current browser. 114 * @returns JSWindowActor The screenshot actor. 115 */ 116 getActor(browser) { 117 let actor = browser.browsingContext.currentWindowGlobal.getActor( 118 "ScreenshotsComponent" 119 ); 120 return actor; 121 }, 122 /** 123 * Open the panel buttons and call child actor to open the overlay 124 * @param browser The current browser 125 */ 126 openPanel(browser) { 127 let actor = this.getActor(browser); 128 actor.sendQuery("Screenshots:ShowOverlay"); 129 this.createOrDisplayButtons(browser); 130 }, 131 /** 132 * Close the panel and call child actor to close the overlay 133 * @param browser The current browser 134 * @param {bool} closeOverlay Whether or not to 135 * send a message to the child to close the overly. 136 * Defaults to true. Will be false when called from didDestroy. 137 */ 138 closePanel(browser, closeOverlay = true) { 139 let buttonsPanel = browser.ownerDocument.querySelector( 140 "#screenshotsPagePanel" 141 ); 142 if (buttonsPanel && buttonsPanel.state !== "closed") { 143 buttonsPanel.hidePopup(); 144 } 145 if (closeOverlay) { 146 let actor = this.getActor(browser); 147 actor.sendQuery("Screenshots:HideOverlay"); 148 } 149 }, 150 /** 151 * If the buttons panel exists and is open we will hide both the panel 152 * popup and the overlay. 153 * Otherwise create or display the buttons. 154 * @param browser The current browser. 155 */ 156 togglePreview(browser) { 157 let buttonsPanel = browser.ownerDocument.querySelector( 158 "#screenshotsPagePanel" 159 ); 160 if (buttonsPanel && buttonsPanel.state !== "closed") { 161 buttonsPanel.hidePopup(); 162 let actor = this.getActor(browser); 163 return actor.sendQuery("Screenshots:HideOverlay"); 164 } 165 let actor = this.getActor(browser); 166 actor.sendQuery("Screenshots:ShowOverlay"); 167 return this.createOrDisplayButtons(browser); 168 }, 169 /** 170 * Gets the screenshots dialog box 171 * @param browser The selected browser 172 * @returns Screenshots dialog box if it exists otherwise null 173 */ 174 getDialog(browser) { 175 let currTabDialogBox = browser.tabDialogBox; 176 let browserContextId = browser.browsingContext.id; 177 if (currTabDialogBox) { 178 currTabDialogBox.getTabDialogManager(); 179 let manager = currTabDialogBox.getTabDialogManager(); 180 let dialogs = manager.hasDialogs && manager.dialogs; 181 if (dialogs.length) { 182 for (let dialog of dialogs) { 183 if ( 184 dialog._openedURL.endsWith( 185 `browsingContextId=${browserContextId}` 186 ) && 187 dialog._openedURL.includes("screenshots.html") 188 ) { 189 return dialog; 190 } 191 } 192 } 193 } 194 return null; 195 }, 196 /** 197 * Closes the dialog box it it exists 198 * @param browser The selected browser 199 */ 200 closeDialogBox(browser) { 201 let dialog = this.getDialog(browser); 202 if (dialog) { 203 dialog.close(); 204 return true; 205 } 206 return false; 207 }, 208 /** 209 * If the buttons panel does not exist then we will replace the buttons 210 * panel template with the buttons panel then open the buttons panel and 211 * show the screenshots overaly. 212 * @param browser The current browser. 213 */ 214 createOrDisplayButtons(browser) { 215 let doc = browser.ownerDocument; 216 let buttonsPanel = doc.querySelector("#screenshotsPagePanel"); 217 if (!buttonsPanel) { 218 let template = doc.querySelector("#screenshotsPagePanelTemplate"); 219 let clone = template.content.cloneNode(true); 220 template.replaceWith(clone); 221 buttonsPanel = doc.querySelector("#screenshotsPagePanel"); 222 } 223 224 let anchor = doc.querySelector("#navigator-toolbox"); 225 buttonsPanel.openPopup(anchor, PanelPosition, PanelOffsetX, PanelOffsetY); 226 }, 227 /** 228 * Gets the full page bounds from the screenshots child actor. 229 * @param browser The current browser. 230 * @returns { object } 231 * Contains the full page bounds from the screenshots child actor. 232 */ 233 fetchFullPageBounds(browser) { 234 let actor = this.getActor(browser); 235 return actor.sendQuery("Screenshots:getFullPageBounds"); 236 }, 237 /** 238 * Gets the visible bounds from the screenshots child actor. 239 * @param browser The current browser. 240 * @returns { object } 241 * Contains the visible bounds from the screenshots child actor. 242 */ 243 fetchVisibleBounds(browser) { 244 let actor = this.getActor(browser); 245 return actor.sendQuery("Screenshots:getVisibleBounds"); 246 }, 247 /** 248 * Add screenshot-ui to the dialog box and then take the screenshot 249 * @param browser The current browser. 250 * @param dialog The dialog box to show the screenshot preview. 251 * @param zoom The current zoom level. 252 * @param type The type of screenshot taken. 253 */ 254 async doScreenshot(browser, dialog, zoom, type) { 255 await dialog._dialogReady; 256 let screenshotsUI = dialog._frame.contentDocument.createElement( 257 "screenshots-ui" 258 ); 259 dialog._frame.contentDocument.body.appendChild(screenshotsUI); 260 261 let rect; 262 if (type === "full-page") { 263 ({ rect } = await this.fetchFullPageBounds(browser)); 264 } else { 265 ({ rect } = await this.fetchVisibleBounds(browser)); 266 } 267 return this.takeScreenshot(browser, dialog, rect, zoom); 268 }, 269 /** 270 * Take the screenshot and add the image to the dialog box 271 * @param browser The current browser. 272 * @param dialog The dialog box to show the screenshot preview. 273 * @param rect DOMRect containing bounds of the screenshot. 274 * @param zoom The current zoom level. 275 */ 276 async takeScreenshot(browser, dialog, rect, zoom) { 277 let browsingContext = BrowsingContext.get(browser.browsingContext.id); 278 279 let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot( 280 rect, 281 zoom, 282 "rgb(255,255,255)" 283 ); 284 285 let canvas = dialog._frame.contentDocument.createElementNS( 286 "http://www.w3.org/1999/xhtml", 287 "html:canvas" 288 ); 289 let context = canvas.getContext("2d"); 290 291 canvas.width = snapshot.width; 292 canvas.height = snapshot.height; 293 294 context.drawImage(snapshot, 0, 0); 295 296 canvas.toBlob(function(blob) { 297 let newImg = dialog._frame.contentDocument.createElement("img"); 298 let url = URL.createObjectURL(blob); 299 300 newImg.id = "placeholder-image"; 301 302 newImg.src = url; 303 dialog._frame.contentDocument 304 .getElementById("preview-image-div") 305 .appendChild(newImg); 306 307 if (Cu.isInAutomation) { 308 Services.obs.notifyObservers(null, "screenshots-preview-ready"); 309 } 310 }); 311 312 snapshot.close(); 313 }, 314}; 315