diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/framework/toolbox-host-manager.js | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/framework/toolbox-host-manager.js')
-rw-r--r-- | devtools/client/framework/toolbox-host-manager.js | 358 |
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; |