summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/toolbox-host-manager.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework/toolbox-host-manager.js')
-rw-r--r--devtools/client/framework/toolbox-host-manager.js358
1 files changed, 358 insertions, 0 deletions
diff --git a/devtools/client/framework/toolbox-host-manager.js b/devtools/client/framework/toolbox-host-manager.js
new file mode 100644
index 0000000000..6c1a0e645d
--- /dev/null
+++ b/devtools/client/framework/toolbox-host-manager.js
@@ -0,0 +1,358 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const { DOMHelpers } = require("resource://devtools/shared/dom-helpers.js");
+
+// The min-width of toolbox and browser toolbox.
+const WIDTH_CHEVRON_AND_MEATBALL = 50;
+const WIDTH_CHEVRON_AND_MEATBALL_AND_CLOSE = 74;
+const ZOOM_VALUE_PREF = "devtools.toolbox.zoomValue";
+
+loader.lazyRequireGetter(
+ this,
+ "Toolbox",
+ "resource://devtools/client/framework/toolbox.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "Hosts",
+ "resource://devtools/client/framework/toolbox-hosts.js",
+ true
+);
+
+/**
+ * Implement a wrapper on the chrome side to setup a Toolbox within Firefox UI.
+ *
+ * This component handles iframe creation within Firefox, in which we are loading
+ * the toolbox document. Then both the chrome and the toolbox document communicate
+ * via "message" events.
+ *
+ * Messages sent by the toolbox to the chrome:
+ * - switch-host:
+ * Order to display the toolbox in another host (side, bottom, window, or the
+ * previously used one)
+ * - raise-host:
+ * Focus the tools
+ * - set-host-title:
+ * When using the window host, update the window title
+ *
+ * Messages sent by the chrome to the toolbox:
+ * - switched-host:
+ * The `switch-host` command sent by the toolbox is done
+ */
+
+const LAST_HOST = "devtools.toolbox.host";
+const PREVIOUS_HOST = "devtools.toolbox.previousHost";
+let ID_COUNTER = 1;
+
+function ToolboxHostManager(commands, hostType, hostOptions) {
+ this.commands = commands;
+
+ // When debugging a local tab, we keep a reference of the current tab into which the toolbox is displayed.
+ // This will only change from the descriptor's localTab when we start debugging popups (i.e. window.open).
+ this.currentTab = this.commands.descriptorFront.localTab;
+
+ // Keep the previously instantiated Host for all tabs where we displayed the Toolbox.
+ // This will only be useful when we start debugging popups (i.e. window.open).
+ // This is used to re-use the previous host instance when we re-select the original tab
+ // we were debugging before the popup opened.
+ this.hostPerTab = new Map();
+
+ this.frameId = ID_COUNTER++;
+
+ if (!hostType) {
+ hostType = Services.prefs.getCharPref(LAST_HOST);
+ if (!Hosts[hostType]) {
+ // If the preference value is unexpected, restore to the default value.
+ Services.prefs.clearUserPref(LAST_HOST);
+ hostType = Services.prefs.getCharPref(LAST_HOST);
+ }
+ }
+ this.eventController = new AbortController();
+ this.host = this.createHost(hostType, hostOptions);
+ this.hostType = hostType;
+ this.setMinWidthWithZoom = this.setMinWidthWithZoom.bind(this);
+ this._onMessage = this._onMessage.bind(this);
+ Services.prefs.addObserver(ZOOM_VALUE_PREF, this.setMinWidthWithZoom);
+}
+
+ToolboxHostManager.prototype = {
+ async create(toolId) {
+ await this.host.create();
+ if (this.currentTab) {
+ this.hostPerTab.set(this.currentTab, this.host);
+ }
+
+ this.host.frame.setAttribute("aria-label", L10N.getStr("toolbox.label"));
+ this.host.frame.ownerDocument.defaultView.addEventListener(
+ "message",
+ this._onMessage,
+ { signal: this.eventController.signal }
+ );
+
+ const toolbox = new Toolbox(
+ this.commands,
+ toolId,
+ this.host.type,
+ this.host.frame.contentWindow,
+ this.frameId
+ );
+ toolbox.once("destroyed", this._onToolboxDestroyed.bind(this));
+
+ // Prevent reloading the toolbox when loading the tools in a tab
+ // (e.g. from about:debugging)
+ const location = this.host.frame.contentWindow.location;
+ if (!location.href.startsWith("about:devtools-toolbox")) {
+ this.host.frame.setAttribute("src", "about:devtools-toolbox");
+ }
+
+ this.setMinWidthWithZoom();
+ return toolbox;
+ },
+
+ setMinWidthWithZoom() {
+ const zoomValue = parseFloat(Services.prefs.getCharPref(ZOOM_VALUE_PREF));
+
+ if (isNaN(zoomValue)) {
+ return;
+ }
+
+ if (
+ this.hostType === Toolbox.HostType.LEFT ||
+ this.hostType === Toolbox.HostType.RIGHT
+ ) {
+ this.host.frame.style.minWidth =
+ WIDTH_CHEVRON_AND_MEATBALL_AND_CLOSE * zoomValue + "px";
+ } else if (
+ this.hostType === Toolbox.HostType.WINDOW ||
+ this.hostType === Toolbox.HostType.PAGE ||
+ this.hostType === Toolbox.HostType.BROWSERTOOLBOX
+ ) {
+ this.host.frame.style.minWidth =
+ WIDTH_CHEVRON_AND_MEATBALL * zoomValue + "px";
+ }
+ },
+
+ _onToolboxDestroyed() {
+ // Delay self-destruction to let the debugger complete async destruction.
+ // Otherwise it throws when running browser_dbg-breakpoints-in-evaled-sources.js
+ // because the promise middleware delay each promise action using setTimeout...
+ DevToolsUtils.executeSoon(() => {
+ this.destroy();
+ });
+ },
+
+ _onMessage(event) {
+ if (!event.data) {
+ return;
+ }
+ const msg = event.data;
+ // Toolbox document is still chrome and disallow identifying message
+ // origin via event.source as it is null. So use a custom id.
+ if (msg.frameId != this.frameId) {
+ return;
+ }
+ switch (msg.name) {
+ case "switch-host":
+ this.switchHost(msg.hostType);
+ break;
+ case "switch-host-to-tab":
+ this.switchHostToTab(msg.tabBrowsingContextID);
+ break;
+ case "raise-host":
+ this.host.raise();
+ this.postMessage({
+ name: "host-raised",
+ });
+ break;
+ case "set-host-title":
+ this.host.setTitle(msg.title);
+ break;
+ }
+ },
+
+ postMessage(data) {
+ const window = this.host.frame.contentWindow;
+ window.postMessage(data, "*");
+ },
+
+ destroy() {
+ Services.prefs.removeObserver(ZOOM_VALUE_PREF, this.setMinWidthWithZoom);
+ this.eventController.abort();
+ this.eventController = null;
+ this.destroyHost();
+ // When we are debugging popup, we created host for each popup opened
+ // in some other tabs. Ensure destroying them here.
+ for (const host of this.hostPerTab.values()) {
+ host.destroy();
+ }
+ this.hostPerTab.clear();
+ this.host = null;
+ this.hostType = null;
+ this.commands = null;
+ },
+
+ /**
+ * Create a host object based on the given host type.
+ *
+ * Warning: bottom and sidebar hosts require that the toolbox target provides
+ * a reference to the attached tab. Not all Targets have a tab property -
+ * make sure you correctly mix and match hosts and targets.
+ *
+ * @param {string} hostType
+ * The host type of the new host object
+ *
+ * @return {Host} host
+ * The created host object
+ */
+ createHost(hostType, options) {
+ if (!Hosts[hostType]) {
+ throw new Error("Unknown hostType: " + hostType);
+ }
+ const newHost = new Hosts[hostType](this.currentTab, options);
+ return newHost;
+ },
+
+ /**
+ * Migrate the toolbox to a new host, while keeping it fully functional.
+ * The toolbox's iframe will be moved as-is to the new host.
+ *
+ * @param {String} hostType
+ * The new type of host to spawn
+ * @param {Boolean} destroyPreviousHost
+ * Defaults to true. If false is passed, we will avoid destroying
+ * the previous host. This is helpful for popup debugging,
+ * where we migrate the toolbox between two tabs. In this scenario
+ * we are reusing previously instantiated hosts. This is especially
+ * useful when we close the current tab and have to have an
+ * already instantiated host to migrate to. If we don't have one,
+ * the toolbox iframe will already be destroyed before we have a chance
+ * to migrate it.
+ */
+ async switchHost(hostType, destroyPreviousHost = true) {
+ if (hostType == "previous") {
+ // Switch to the last used host for the toolbox UI.
+ // This is determined by the devtools.toolbox.previousHost pref.
+ hostType = Services.prefs.getCharPref(PREVIOUS_HOST);
+
+ // Handle the case where the previous host happens to match the current
+ // host. If so, switch to bottom if it's not already used, and right side if not.
+ if (hostType === this.hostType) {
+ if (hostType === Toolbox.HostType.BOTTOM) {
+ hostType = Toolbox.HostType.RIGHT;
+ } else {
+ hostType = Toolbox.HostType.BOTTOM;
+ }
+ }
+ }
+ const iframe = this.host.frame;
+ const newHost = this.createHost(hostType);
+ const newIframe = await newHost.create();
+
+ // Load a blank document in the host frame. The new iframe must have a valid
+ // document before using swapFrameLoaders().
+ await new Promise(resolve => {
+ newIframe.setAttribute("src", "about:blank");
+ DOMHelpers.onceDOMReady(newIframe.contentWindow, resolve);
+ });
+
+ // change toolbox document's parent to the new host
+ newIframe.swapFrameLoaders(iframe);
+ if (destroyPreviousHost) {
+ this.destroyHost();
+ }
+
+ if (
+ this.hostType !== Toolbox.HostType.BROWSERTOOLBOX &&
+ this.hostType !== Toolbox.HostType.PAGE
+ ) {
+ Services.prefs.setCharPref(PREVIOUS_HOST, this.hostType);
+ }
+
+ this.host = newHost;
+ if (this.currentTab) {
+ this.hostPerTab.set(this.currentTab, newHost);
+ }
+ this.hostType = hostType;
+ this.host.setTitle(this.host.frame.contentWindow.document.title);
+ this.host.frame.ownerDocument.defaultView.addEventListener(
+ "message",
+ this._onMessage,
+ { signal: this.eventController.signal }
+ );
+
+ this.setMinWidthWithZoom();
+
+ if (
+ hostType !== Toolbox.HostType.BROWSERTOOLBOX &&
+ hostType !== Toolbox.HostType.PAGE
+ ) {
+ Services.prefs.setCharPref(LAST_HOST, hostType);
+ }
+
+ // Tell the toolbox the host changed
+ this.postMessage({
+ name: "switched-host",
+ hostType,
+ });
+ },
+
+ /**
+ * When we are debugging popup, we are moving around the toolbox between original tab
+ * and popup tabs. This method will only move the host to a new tab, while
+ * keeping the same host type.
+ *
+ * @param {String} tabBrowsingContextID
+ * The ID of the browsing context of the tab we want to move to.
+ */
+ async switchHostToTab(tabBrowsingContextID) {
+ const { gBrowser } = this.host.frame.ownerDocument.defaultView;
+
+ const previousTab = this.currentTab;
+ const newTab = gBrowser.tabs.find(
+ tab => tab.linkedBrowser.browsingContext.id == tabBrowsingContextID
+ );
+ // Note that newTab will be undefined when the popup opens in a new top level window.
+ if (newTab && newTab != previousTab) {
+ this.currentTab = newTab;
+ const newHost = this.hostPerTab.get(this.currentTab);
+ if (newHost) {
+ newHost.frame.swapFrameLoaders(this.host.frame);
+ this.host = newHost;
+ } else {
+ await this.switchHost(this.hostType, false);
+ }
+ previousTab.addEventListener(
+ "TabSelect",
+ event => {
+ this.switchHostToTab(event.target.linkedBrowser.browsingContext.id);
+ },
+ { once: true, signal: this.eventController.signal }
+ );
+ }
+
+ this.postMessage({
+ name: "switched-host-to-tab",
+ browsingContextID: tabBrowsingContextID,
+ });
+ },
+
+ /**
+ * Destroy the current host, and remove event listeners from its frame.
+ *
+ * @return {promise} to be resolved when the host is destroyed.
+ */
+ destroyHost() {
+ return this.host.destroy();
+ },
+};
+exports.ToolboxHostManager = ToolboxHostManager;