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