Bug 1874684 - Part 4: Prefer const references instead of copying Instant values....
[gecko.git] / devtools / client / framework / toolbox-host-manager.js
blob6c1a0e645d8abfa339c0e041402d53ac37e64e16
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/. */
5 "use strict";
7 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
8 const L10N = new LocalizationHelper(
9   "devtools/client/locales/toolbox.properties"
11 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
12 const { DOMHelpers } = require("resource://devtools/shared/dom-helpers.js");
14 // The min-width of toolbox and browser toolbox.
15 const WIDTH_CHEVRON_AND_MEATBALL = 50;
16 const WIDTH_CHEVRON_AND_MEATBALL_AND_CLOSE = 74;
17 const ZOOM_VALUE_PREF = "devtools.toolbox.zoomValue";
19 loader.lazyRequireGetter(
20   this,
21   "Toolbox",
22   "resource://devtools/client/framework/toolbox.js",
23   true
25 loader.lazyRequireGetter(
26   this,
27   "Hosts",
28   "resource://devtools/client/framework/toolbox-hosts.js",
29   true
32 /**
33  * Implement a wrapper on the chrome side to setup a Toolbox within Firefox UI.
34  *
35  * This component handles iframe creation within Firefox, in which we are loading
36  * the toolbox document. Then both the chrome and the toolbox document communicate
37  * via "message" events.
38  *
39  * Messages sent by the toolbox to the chrome:
40  * - switch-host:
41  *   Order to display the toolbox in another host (side, bottom, window, or the
42  *   previously used one)
43  * - raise-host:
44  *   Focus the tools
45  * - set-host-title:
46  *   When using the window host, update the window title
47  *
48  * Messages sent by the chrome to the toolbox:
49  * - switched-host:
50  *   The `switch-host` command sent by the toolbox is done
51  */
53 const LAST_HOST = "devtools.toolbox.host";
54 const PREVIOUS_HOST = "devtools.toolbox.previousHost";
55 let ID_COUNTER = 1;
57 function ToolboxHostManager(commands, hostType, hostOptions) {
58   this.commands = commands;
60   // When debugging a local tab, we keep a reference of the current tab into which the toolbox is displayed.
61   // This will only change from the descriptor's localTab when we start debugging popups (i.e. window.open).
62   this.currentTab = this.commands.descriptorFront.localTab;
64   // Keep the previously instantiated Host for all tabs where we displayed the Toolbox.
65   // This will only be useful when we start debugging popups (i.e. window.open).
66   // This is used to re-use the previous host instance when we re-select the original tab
67   // we were debugging before the popup opened.
68   this.hostPerTab = new Map();
70   this.frameId = ID_COUNTER++;
72   if (!hostType) {
73     hostType = Services.prefs.getCharPref(LAST_HOST);
74     if (!Hosts[hostType]) {
75       // If the preference value is unexpected, restore to the default value.
76       Services.prefs.clearUserPref(LAST_HOST);
77       hostType = Services.prefs.getCharPref(LAST_HOST);
78     }
79   }
80   this.eventController = new AbortController();
81   this.host = this.createHost(hostType, hostOptions);
82   this.hostType = hostType;
83   this.setMinWidthWithZoom = this.setMinWidthWithZoom.bind(this);
84   this._onMessage = this._onMessage.bind(this);
85   Services.prefs.addObserver(ZOOM_VALUE_PREF, this.setMinWidthWithZoom);
88 ToolboxHostManager.prototype = {
89   async create(toolId) {
90     await this.host.create();
91     if (this.currentTab) {
92       this.hostPerTab.set(this.currentTab, this.host);
93     }
95     this.host.frame.setAttribute("aria-label", L10N.getStr("toolbox.label"));
96     this.host.frame.ownerDocument.defaultView.addEventListener(
97       "message",
98       this._onMessage,
99       { signal: this.eventController.signal }
100     );
102     const toolbox = new Toolbox(
103       this.commands,
104       toolId,
105       this.host.type,
106       this.host.frame.contentWindow,
107       this.frameId
108     );
109     toolbox.once("destroyed", this._onToolboxDestroyed.bind(this));
111     // Prevent reloading the toolbox when loading the tools in a tab
112     // (e.g. from about:debugging)
113     const location = this.host.frame.contentWindow.location;
114     if (!location.href.startsWith("about:devtools-toolbox")) {
115       this.host.frame.setAttribute("src", "about:devtools-toolbox");
116     }
118     this.setMinWidthWithZoom();
119     return toolbox;
120   },
122   setMinWidthWithZoom() {
123     const zoomValue = parseFloat(Services.prefs.getCharPref(ZOOM_VALUE_PREF));
125     if (isNaN(zoomValue)) {
126       return;
127     }
129     if (
130       this.hostType === Toolbox.HostType.LEFT ||
131       this.hostType === Toolbox.HostType.RIGHT
132     ) {
133       this.host.frame.style.minWidth =
134         WIDTH_CHEVRON_AND_MEATBALL_AND_CLOSE * zoomValue + "px";
135     } else if (
136       this.hostType === Toolbox.HostType.WINDOW ||
137       this.hostType === Toolbox.HostType.PAGE ||
138       this.hostType === Toolbox.HostType.BROWSERTOOLBOX
139     ) {
140       this.host.frame.style.minWidth =
141         WIDTH_CHEVRON_AND_MEATBALL * zoomValue + "px";
142     }
143   },
145   _onToolboxDestroyed() {
146     // Delay self-destruction to let the debugger complete async destruction.
147     // Otherwise it throws when running browser_dbg-breakpoints-in-evaled-sources.js
148     // because the promise middleware delay each promise action using setTimeout...
149     DevToolsUtils.executeSoon(() => {
150       this.destroy();
151     });
152   },
154   _onMessage(event) {
155     if (!event.data) {
156       return;
157     }
158     const msg = event.data;
159     // Toolbox document is still chrome and disallow identifying message
160     // origin via event.source as it is null. So use a custom id.
161     if (msg.frameId != this.frameId) {
162       return;
163     }
164     switch (msg.name) {
165       case "switch-host":
166         this.switchHost(msg.hostType);
167         break;
168       case "switch-host-to-tab":
169         this.switchHostToTab(msg.tabBrowsingContextID);
170         break;
171       case "raise-host":
172         this.host.raise();
173         this.postMessage({
174           name: "host-raised",
175         });
176         break;
177       case "set-host-title":
178         this.host.setTitle(msg.title);
179         break;
180     }
181   },
183   postMessage(data) {
184     const window = this.host.frame.contentWindow;
185     window.postMessage(data, "*");
186   },
188   destroy() {
189     Services.prefs.removeObserver(ZOOM_VALUE_PREF, this.setMinWidthWithZoom);
190     this.eventController.abort();
191     this.eventController = null;
192     this.destroyHost();
193     // When we are debugging popup, we created host for each popup opened
194     // in some other tabs. Ensure destroying them here.
195     for (const host of this.hostPerTab.values()) {
196       host.destroy();
197     }
198     this.hostPerTab.clear();
199     this.host = null;
200     this.hostType = null;
201     this.commands = null;
202   },
204   /**
205    * Create a host object based on the given host type.
206    *
207    * Warning: bottom and sidebar hosts require that the toolbox target provides
208    * a reference to the attached tab. Not all Targets have a tab property -
209    * make sure you correctly mix and match hosts and targets.
210    *
211    * @param {string} hostType
212    *        The host type of the new host object
213    *
214    * @return {Host} host
215    *        The created host object
216    */
217   createHost(hostType, options) {
218     if (!Hosts[hostType]) {
219       throw new Error("Unknown hostType: " + hostType);
220     }
221     const newHost = new Hosts[hostType](this.currentTab, options);
222     return newHost;
223   },
225   /**
226    * Migrate the toolbox to a new host, while keeping it fully functional.
227    * The toolbox's iframe will be moved as-is to the new host.
228    *
229    * @param {String} hostType
230    *        The new type of host to spawn
231    * @param {Boolean} destroyPreviousHost
232    *        Defaults to true. If false is passed, we will avoid destroying
233    *        the previous host. This is helpful for popup debugging,
234    *        where we migrate the toolbox between two tabs. In this scenario
235    *        we are reusing previously instantiated hosts. This is especially
236    *        useful when we close the current tab and have to have an
237    *        already instantiated host to migrate to. If we don't have one,
238    *        the toolbox iframe will already be destroyed before we have a chance
239    *        to migrate it.
240    */
241   async switchHost(hostType, destroyPreviousHost = true) {
242     if (hostType == "previous") {
243       // Switch to the last used host for the toolbox UI.
244       // This is determined by the devtools.toolbox.previousHost pref.
245       hostType = Services.prefs.getCharPref(PREVIOUS_HOST);
247       // Handle the case where the previous host happens to match the current
248       // host. If so, switch to bottom if it's not already used, and right side if not.
249       if (hostType === this.hostType) {
250         if (hostType === Toolbox.HostType.BOTTOM) {
251           hostType = Toolbox.HostType.RIGHT;
252         } else {
253           hostType = Toolbox.HostType.BOTTOM;
254         }
255       }
256     }
257     const iframe = this.host.frame;
258     const newHost = this.createHost(hostType);
259     const newIframe = await newHost.create();
261     // Load a blank document in the host frame. The new iframe must have a valid
262     // document before using swapFrameLoaders().
263     await new Promise(resolve => {
264       newIframe.setAttribute("src", "about:blank");
265       DOMHelpers.onceDOMReady(newIframe.contentWindow, resolve);
266     });
268     // change toolbox document's parent to the new host
269     newIframe.swapFrameLoaders(iframe);
270     if (destroyPreviousHost) {
271       this.destroyHost();
272     }
274     if (
275       this.hostType !== Toolbox.HostType.BROWSERTOOLBOX &&
276       this.hostType !== Toolbox.HostType.PAGE
277     ) {
278       Services.prefs.setCharPref(PREVIOUS_HOST, this.hostType);
279     }
281     this.host = newHost;
282     if (this.currentTab) {
283       this.hostPerTab.set(this.currentTab, newHost);
284     }
285     this.hostType = hostType;
286     this.host.setTitle(this.host.frame.contentWindow.document.title);
287     this.host.frame.ownerDocument.defaultView.addEventListener(
288       "message",
289       this._onMessage,
290       { signal: this.eventController.signal }
291     );
293     this.setMinWidthWithZoom();
295     if (
296       hostType !== Toolbox.HostType.BROWSERTOOLBOX &&
297       hostType !== Toolbox.HostType.PAGE
298     ) {
299       Services.prefs.setCharPref(LAST_HOST, hostType);
300     }
302     // Tell the toolbox the host changed
303     this.postMessage({
304       name: "switched-host",
305       hostType,
306     });
307   },
309   /**
310    * When we are debugging popup, we are moving around the toolbox between original tab
311    * and popup tabs. This method will only move the host to a new tab, while
312    * keeping the same host type.
313    *
314    * @param {String} tabBrowsingContextID
315    *        The ID of the browsing context of the tab we want to move to.
316    */
317   async switchHostToTab(tabBrowsingContextID) {
318     const { gBrowser } = this.host.frame.ownerDocument.defaultView;
320     const previousTab = this.currentTab;
321     const newTab = gBrowser.tabs.find(
322       tab => tab.linkedBrowser.browsingContext.id == tabBrowsingContextID
323     );
324     // Note that newTab will be undefined when the popup opens in a new top level window.
325     if (newTab && newTab != previousTab) {
326       this.currentTab = newTab;
327       const newHost = this.hostPerTab.get(this.currentTab);
328       if (newHost) {
329         newHost.frame.swapFrameLoaders(this.host.frame);
330         this.host = newHost;
331       } else {
332         await this.switchHost(this.hostType, false);
333       }
334       previousTab.addEventListener(
335         "TabSelect",
336         event => {
337           this.switchHostToTab(event.target.linkedBrowser.browsingContext.id);
338         },
339         { once: true, signal: this.eventController.signal }
340       );
341     }
343     this.postMessage({
344       name: "switched-host-to-tab",
345       browsingContextID: tabBrowsingContextID,
346     });
347   },
349   /**
350    * Destroy the current host, and remove event listeners from its frame.
351    *
352    * @return {promise} to be resolved when the host is destroyed.
353    */
354   destroyHost() {
355     return this.host.destroy();
356   },
358 exports.ToolboxHostManager = ToolboxHostManager;