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/. */
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(
22 "resource://devtools/client/framework/toolbox.js",
25 loader.lazyRequireGetter(
28 "resource://devtools/client/framework/toolbox-hosts.js",
33 * Implement a wrapper on the chrome side to setup a Toolbox within Firefox UI.
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.
39 * Messages sent by the toolbox to the chrome:
41 * Order to display the toolbox in another host (side, bottom, window, or the
42 * previously used one)
46 * When using the window host, update the window title
48 * Messages sent by the chrome to the toolbox:
50 * The `switch-host` command sent by the toolbox is done
53 const LAST_HOST = "devtools.toolbox.host";
54 const PREVIOUS_HOST = "devtools.toolbox.previousHost";
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++;
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);
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);
95 this.host.frame.setAttribute("aria-label", L10N.getStr("toolbox.label"));
96 this.host.frame.ownerDocument.defaultView.addEventListener(
99 { signal: this.eventController.signal }
102 const toolbox = new Toolbox(
106 this.host.frame.contentWindow,
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");
118 this.setMinWidthWithZoom();
122 setMinWidthWithZoom() {
123 const zoomValue = parseFloat(Services.prefs.getCharPref(ZOOM_VALUE_PREF));
125 if (isNaN(zoomValue)) {
130 this.hostType === Toolbox.HostType.LEFT ||
131 this.hostType === Toolbox.HostType.RIGHT
133 this.host.frame.style.minWidth =
134 WIDTH_CHEVRON_AND_MEATBALL_AND_CLOSE * zoomValue + "px";
136 this.hostType === Toolbox.HostType.WINDOW ||
137 this.hostType === Toolbox.HostType.PAGE ||
138 this.hostType === Toolbox.HostType.BROWSERTOOLBOX
140 this.host.frame.style.minWidth =
141 WIDTH_CHEVRON_AND_MEATBALL * zoomValue + "px";
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(() => {
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) {
166 this.switchHost(msg.hostType);
168 case "switch-host-to-tab":
169 this.switchHostToTab(msg.tabBrowsingContextID);
177 case "set-host-title":
178 this.host.setTitle(msg.title);
184 const window = this.host.frame.contentWindow;
185 window.postMessage(data, "*");
189 Services.prefs.removeObserver(ZOOM_VALUE_PREF, this.setMinWidthWithZoom);
190 this.eventController.abort();
191 this.eventController = null;
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()) {
198 this.hostPerTab.clear();
200 this.hostType = null;
201 this.commands = null;
205 * Create a host object based on the given host type.
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.
211 * @param {string} hostType
212 * The host type of the new host object
214 * @return {Host} host
215 * The created host object
217 createHost(hostType, options) {
218 if (!Hosts[hostType]) {
219 throw new Error("Unknown hostType: " + hostType);
221 const newHost = new Hosts[hostType](this.currentTab, options);
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.
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
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;
253 hostType = Toolbox.HostType.BOTTOM;
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);
268 // change toolbox document's parent to the new host
269 newIframe.swapFrameLoaders(iframe);
270 if (destroyPreviousHost) {
275 this.hostType !== Toolbox.HostType.BROWSERTOOLBOX &&
276 this.hostType !== Toolbox.HostType.PAGE
278 Services.prefs.setCharPref(PREVIOUS_HOST, this.hostType);
282 if (this.currentTab) {
283 this.hostPerTab.set(this.currentTab, newHost);
285 this.hostType = hostType;
286 this.host.setTitle(this.host.frame.contentWindow.document.title);
287 this.host.frame.ownerDocument.defaultView.addEventListener(
290 { signal: this.eventController.signal }
293 this.setMinWidthWithZoom();
296 hostType !== Toolbox.HostType.BROWSERTOOLBOX &&
297 hostType !== Toolbox.HostType.PAGE
299 Services.prefs.setCharPref(LAST_HOST, hostType);
302 // Tell the toolbox the host changed
304 name: "switched-host",
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.
314 * @param {String} tabBrowsingContextID
315 * The ID of the browsing context of the tab we want to move to.
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
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);
329 newHost.frame.swapFrameLoaders(this.host.frame);
332 await this.switchHost(this.hostType, false);
334 previousTab.addEventListener(
337 this.switchHostToTab(event.target.linkedBrowser.browsingContext.id);
339 { once: true, signal: this.eventController.signal }
344 name: "switched-host-to-tab",
345 browsingContextID: tabBrowsingContextID,
350 * Destroy the current host, and remove event listeners from its frame.
352 * @return {promise} to be resolved when the host is destroyed.
355 return this.host.destroy();
358 exports.ToolboxHostManager = ToolboxHostManager;