Backed out changeset a24a00e9a529 (bug 1884172) for causing bc failures @ browser...
[gecko.git] / browser / actors / ScreenshotsComponentChild.sys.mjs
blob465b9472d93289a25e6d2c59b2d65e0e348e23d2
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 /* eslint-env mozilla/browser-window */
6 const lazy = {};
8 ChromeUtils.defineESModuleGetters(lazy, {
9   DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
10   ScreenshotsOverlay: "resource:///modules/ScreenshotsOverlayChild.sys.mjs",
11 });
13 export class ScreenshotsComponentChild extends JSWindowActorChild {
14   #resizeTask;
15   #scrollTask;
16   #overlay;
18   static OVERLAY_EVENTS = [
19     "click",
20     "pointerdown",
21     "pointermove",
22     "pointerup",
23     "keyup",
24     "keydown",
25   ];
27   get overlay() {
28     return this.#overlay;
29   }
31   receiveMessage(message) {
32     switch (message.name) {
33       case "Screenshots:ShowOverlay":
34         return this.startScreenshotsOverlay();
35       case "Screenshots:HideOverlay":
36         return this.endScreenshotsOverlay(message.data);
37       case "Screenshots:isOverlayShowing":
38         return this.overlay?.initialized;
39       case "Screenshots:getFullPageBounds":
40         return this.getFullPageBounds();
41       case "Screenshots:getVisibleBounds":
42         return this.getVisibleBounds();
43       case "Screenshots:getDocumentTitle":
44         return this.getDocumentTitle();
45       case "Screenshots:GetMethodsUsed":
46         return this.getMethodsUsed();
47       case "Screenshots:RemoveEventListeners":
48         return this.removeEventListeners();
49       case "Screenshots:AddEventListeners":
50         return this.addEventListeners();
51     }
52     return null;
53   }
55   handleEvent(event) {
56     if (!event.isTrusted) {
57       return;
58     }
60     switch (event.type) {
61       case "click":
62       case "pointerdown":
63       case "pointermove":
64       case "pointerup":
65       case "keyup":
66       case "keydown":
67         if (!this.overlay?.initialized) {
68           return;
69         }
70         this.overlay.handleEvent(event);
71         break;
72       case "beforeunload":
73         this.requestCancelScreenshot("navigation");
74         break;
75       case "resize":
76         if (!this.#resizeTask && this.overlay?.initialized) {
77           this.#resizeTask = new lazy.DeferredTask(() => {
78             this.overlay.updateScreenshotsOverlayDimensions("resize");
79           }, 16);
80         }
81         this.#resizeTask.arm();
82         break;
83       case "scroll":
84         if (!this.#scrollTask && this.overlay?.initialized) {
85           this.#scrollTask = new lazy.DeferredTask(() => {
86             this.overlay.updateScreenshotsOverlayDimensions("scroll");
87           }, 16);
88         }
89         this.#scrollTask.arm();
90         break;
91       case "Screenshots:Close":
92         this.requestCancelScreenshot(event.detail.reason);
93         break;
94       case "Screenshots:Copy":
95         this.requestCopyScreenshot(event.detail.region);
96         break;
97       case "Screenshots:Download":
98         this.requestDownloadScreenshot(event.detail.region);
99         break;
100       case "Screenshots:OverlaySelection":
101         let { hasSelection } = event.detail;
102         this.sendOverlaySelection({ hasSelection });
103         break;
104       case "Screenshots:RecordEvent":
105         let { eventName, reason, args } = event.detail;
106         this.recordTelemetryEvent(eventName, reason, args);
107         break;
108       case "Screenshots:ShowPanel":
109         this.showPanel();
110         break;
111       case "Screenshots:HidePanel":
112         this.hidePanel();
113         break;
114     }
115   }
117   /**
118    * Send a request to cancel the screenshot to the parent process
119    */
120   requestCancelScreenshot(reason) {
121     this.sendAsyncMessage("Screenshots:CancelScreenshot", {
122       closeOverlay: false,
123       reason,
124     });
125     this.endScreenshotsOverlay();
126   }
128   /**
129    * Send a request to copy the screenshots
130    * @param {Object} region The region dimensions of the screenshot to be copied
131    */
132   requestCopyScreenshot(region) {
133     region.devicePixelRatio = this.contentWindow.devicePixelRatio;
134     this.sendAsyncMessage("Screenshots:CopyScreenshot", { region });
135     this.endScreenshotsOverlay({ doNotResetMethods: true });
136   }
138   /**
139    * Send a request to download the screenshots
140    * @param {Object} region The region dimensions of the screenshot to be downloaded
141    */
142   requestDownloadScreenshot(region) {
143     region.devicePixelRatio = this.contentWindow.devicePixelRatio;
144     this.sendAsyncMessage("Screenshots:DownloadScreenshot", {
145       title: this.getDocumentTitle(),
146       region,
147     });
148     this.endScreenshotsOverlay({ doNotResetMethods: true });
149   }
151   showPanel() {
152     this.sendAsyncMessage("Screenshots:ShowPanel");
153   }
155   hidePanel() {
156     this.sendAsyncMessage("Screenshots:HidePanel");
157   }
159   getDocumentTitle() {
160     return this.document.title;
161   }
163   sendOverlaySelection(data) {
164     this.sendAsyncMessage("Screenshots:OverlaySelection", data);
165   }
167   getMethodsUsed() {
168     let methodsUsed = this.#overlay.methodsUsed;
169     this.#overlay.resetMethodsUsed();
170     return methodsUsed;
171   }
173   /**
174    * Resolves when the document is ready to have an overlay injected into it.
175    *
176    * @returns {Promise}
177    * @resolves {Boolean} true when document is ready or rejects
178    */
179   documentIsReady() {
180     const document = this.document;
181     // Some pages take ages to finish loading - if at all.
182     // We want to respond to enable the screenshots UI as soon that is possible
183     function readyEnough() {
184       return (
185         document.readyState !== "uninitialized" && document.documentElement
186       );
187     }
189     if (readyEnough()) {
190       return Promise.resolve();
191     }
192     return new Promise((resolve, reject) => {
193       function onChange(event) {
194         if (event.type === "pagehide") {
195           document.removeEventListener("readystatechange", onChange);
196           this.contentWindow.removeEventListener("pagehide", onChange);
197           reject(new Error("document unloaded before it was ready"));
198         } else if (readyEnough()) {
199           document.removeEventListener("readystatechange", onChange);
200           this.contentWindow.removeEventListener("pagehide", onChange);
201           resolve();
202         }
203       }
204       document.addEventListener("readystatechange", onChange);
205       this.contentWindow.addEventListener("pagehide", onChange, { once: true });
206     });
207   }
209   addEventListeners() {
210     this.contentWindow.addEventListener("beforeunload", this);
211     this.contentWindow.addEventListener("resize", this);
212     this.contentWindow.addEventListener("scroll", this);
213     this.addOverlayEventListeners();
214   }
216   addOverlayEventListeners() {
217     let chromeEventHandler = this.docShell.chromeEventHandler;
218     for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
219       chromeEventHandler.addEventListener(event, this, true);
220     }
221   }
223   /**
224    * Wait until the document is ready and then show the screenshots overlay
225    *
226    * @returns {Boolean} true when document is ready and the overlay is shown
227    * otherwise false
228    */
229   async startScreenshotsOverlay() {
230     try {
231       await this.documentIsReady();
232     } catch (ex) {
233       console.warn(`ScreenshotsComponentChild: ${ex.message}`);
234       return false;
235     }
236     await this.documentIsReady();
237     let overlay =
238       this.overlay ||
239       (this.#overlay = new lazy.ScreenshotsOverlay(this.document));
240     this.addEventListeners();
242     overlay.initialize();
243     return true;
244   }
246   removeEventListeners() {
247     this.contentWindow.removeEventListener("beforeunload", this);
248     this.contentWindow.removeEventListener("resize", this);
249     this.contentWindow.removeEventListener("scroll", this);
250     this.removeOverlayEventListeners();
251   }
253   removeOverlayEventListeners() {
254     let chromeEventHandler = this.docShell.chromeEventHandler;
255     for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
256       chromeEventHandler.removeEventListener(event, this, true);
257     }
258   }
260   /**
261    * Removes event listeners and the screenshots overlay.
262    */
263   endScreenshotsOverlay(options = {}) {
264     this.removeEventListeners();
266     this.overlay?.tearDown(options);
267     this.#resizeTask?.disarm();
268     this.#scrollTask?.disarm();
269   }
271   didDestroy() {
272     this.#resizeTask?.disarm();
273     this.#scrollTask?.disarm();
274   }
276   /**
277    * Gets the full page bounds for a full page screenshot.
278    *
279    * @returns { object }
280    *   The device pixel ratio and a DOMRect of the scrollable content bounds.
281    *
282    *   devicePixelRatio (float):
283    *      The device pixel ratio of the screen
284    *
285    *   rect (object):
286    *      top (int):
287    *        The scroll top position for the content window.
288    *
289    *      left (int):
290    *        The scroll left position for the content window.
291    *
292    *      width (int):
293    *        The scroll width of the content window.
294    *
295    *      height (int):
296    *        The scroll height of the content window.
297    */
298   getFullPageBounds() {
299     let {
300       scrollMinX,
301       scrollMinY,
302       scrollWidth,
303       scrollHeight,
304       devicePixelRatio,
305     } = this.#overlay.windowDimensions.dimensions;
306     let rect = {
307       left: scrollMinX,
308       top: scrollMinY,
309       right: scrollWidth,
310       bottom: scrollHeight,
311       width: scrollWidth,
312       height: scrollHeight,
313       devicePixelRatio,
314     };
315     return rect;
316   }
318   /**
319    * Gets the visible page bounds for a visible screenshot.
320    *
321    * @returns { object }
322    *   The device pixel ratio and a DOMRect of the current visible
323    *   content bounds.
324    *
325    *   devicePixelRatio (float):
326    *      The device pixel ratio of the screen
327    *
328    *   rect (object):
329    *      top (int):
330    *        The top position for the content window.
331    *
332    *      left (int):
333    *        The left position for the content window.
334    *
335    *      width (int):
336    *        The width of the content window.
337    *
338    *      height (int):
339    *        The height of the content window.
340    */
341   getVisibleBounds() {
342     let { scrollX, scrollY, clientWidth, clientHeight, devicePixelRatio } =
343       this.#overlay.windowDimensions.dimensions;
344     let rect = {
345       left: scrollX,
346       top: scrollY,
347       right: scrollX + clientWidth,
348       bottom: scrollY + clientHeight,
349       width: clientWidth,
350       height: clientHeight,
351       devicePixelRatio,
352     };
353     return rect;
354   }
356   recordTelemetryEvent(type, object, args = {}) {
357     Services.telemetry.recordEvent("screenshots", type, object, null, args);
358   }