const Services = require("Services"); const {Ci} = require("chrome"); const {LocalizationHelper} = require("devtools/shared/l10n"); const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const {Task} = require("devtools/shared/task"); loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true); loader.lazyRequireGetter(this, "Hosts", "devtools/client/framework/toolbox-hosts", 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) * - toggle-minimize-mode: * When using the bottom host, the toolbox can be miximized to only display * the tool titles * - maximize-host: * When using the bottom host in minimized mode, revert back to regular mode * in order to see tool titles and the tools * - 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: * - host-minimized: * The bottom host is done minimizing (after animation end) * - host-maximized: * The bottom host is done switching back to regular mode (after animation * end) * - 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(target, hostType, hostOptions) { this.target = target; this.frameId = ID_COUNTER++; if (!hostType) { hostType = Services.prefs.getCharPref(LAST_HOST); } this.onHostMinimized = this.onHostMinimized.bind(this); this.onHostMaximized = this.onHostMaximized.bind(this); this.host = this.createHost(hostType, hostOptions); this.hostType = hostType; } ToolboxHostManager.prototype = { create: Task.async(function* (toolId) { yield this.host.create(); this.host.frame.setAttribute("aria-label", L10N.getStr("toolbox.label")); this.host.frame.ownerDocument.defaultView.addEventListener("message", this); // We have to listen on capture as no event fires on bubble this.host.frame.addEventListener("unload", this, true); let toolbox = new Toolbox(this.target, toolId, this.host.type, this.host.frame.contentWindow, this.frameId); // Prevent reloading the toolbox when loading the tools in a tab (e.g. from about:debugging) if (!this.host.frame.contentWindow.location.href.startsWith("about:devtools-toolbox")) { this.host.frame.setAttribute("src", "about:devtools-toolbox"); } return toolbox; }), handleEvent(event) { switch(event.type) { case "message": this.onMessage(event); break; case "unload": // On unload, host iframe already lost its contentWindow attribute, so // we can only compare against locations. Here we filter two very // different cases: preliminary about:blank document as well as iframes // like tool iframes. if (!event.target.location.href.startsWith("about:devtools-toolbox")) { break; } // Don't destroy the host during unload event (esp., don't remove the // iframe from DOM!). Otherwise the unload event for the toolbox // document doesn't fire within the toolbox *document*! This is // the unload event that fires on the toolbox *iframe*. DevToolsUtils.executeSoon(() => { this.destroy(); }); break; } }, onMessage(event) { if (!event.data) { return; } // Toolbox document is still chrome and disallow identifying message // origin via event.source as it is null. So use a custom id. if (event.data.frameId != this.frameId) { return; } switch (event.data.name) { case "switch-host": this.switchHost(event.data.hostType); break; case "maximize-host": this.host.maximize(); break; case "raise-host": this.host.raise(); break; case "toggle-minimize-mode": this.host.toggleMinimizeMode(event.data.toolbarHeight); break; case "set-host-title": this.host.setTitle(event.data.title); break; } }, postMessage(data) { let window = this.host.frame.contentWindow; window.postMessage(data, "*"); }, destroy() { this.destroyHost(); this.host = null; this.hostType = null; this.target = 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); } let newHost = new Hosts[hostType](this.target.tab, options); // Update the label and icon when the state changes. newHost.on("minimized", this.onHostMinimized); newHost.on("maximized", this.onHostMaximized); return newHost; }, onHostMinimized() { this.postMessage({ name: "host-minimized" }); }, onHostMaximized() { this.postMessage({ name: "host-maximized" }); }, switchHost: Task.async(function* (hostType) { 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 side if not. if (hostType === this.hostType) { if (hostType === Toolbox.HostType.BOTTOM) { hostType = Toolbox.HostType.SIDE; } else { hostType = Toolbox.HostType.BOTTOM; } } } let iframe = this.host.frame; let newHost = this.createHost(hostType); let newIframe = yield newHost.create(); // change toolbox document's parent to the new host newIframe.swapFrameLoaders(iframe); this.destroyHost(); if (this.hostType != Toolbox.HostType.CUSTOM) { Services.prefs.setCharPref(PREVIOUS_HOST, this.hostType); } this.host = newHost; this.hostType = hostType; this.host.setTitle(this.host.frame.contentWindow.document.title); this.host.frame.ownerDocument.defaultView.addEventListener("message", this); this.host.frame.addEventListener("unload", this, true); if (hostType != Toolbox.HostType.CUSTOM) { Services.prefs.setCharPref(LAST_HOST, hostType); } // Tell the toolbox the host changed this.postMessage({ name: "switched-host", hostType }); }), /** * Destroy the current host, and remove event listeners from its frame. * * @return {promise} to be resolved when the host is destroyed. */ destroyHost() { // When Firefox toplevel is closed, the frame may already be detached and // the top level document gone if (this.host.frame.ownerDocument.defaultView) { this.host.frame.ownerDocument.defaultView.removeEventListener("message", this); } this.host.frame.removeEventListener("unload", this, true); this.host.off("minimized", this.onHostMinimized); this.host.off("maximized", this.onHostMaximized); return this.host.destroy(); } }; exports.ToolboxHostManager = ToolboxHostManager;