summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/toolbox-hosts.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/framework/toolbox-hosts.js460
1 files changed, 460 insertions, 0 deletions
diff --git a/devtools/client/framework/toolbox-hosts.js b/devtools/client/framework/toolbox-hosts.js
new file mode 100644
index 0000000000..59d113848f
--- /dev/null
+++ b/devtools/client/framework/toolbox-hosts.js
@@ -0,0 +1,460 @@
+/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+loader.lazyRequireGetter(
+ this,
+ "gDevToolsBrowser",
+ "resource://devtools/client/framework/devtools-browser.js",
+ true
+);
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+/* A host should always allow this much space for the page to be displayed.
+ * There is also a min-height on the browser, but we still don't want to set
+ * frame.style.height to be larger than that, since it can cause problems with
+ * resizing the toolbox and panel layout. */
+const MIN_PAGE_SIZE = 25;
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * A toolbox host represents an object that contains a toolbox (e.g. the
+ * sidebar or a separate window). Any host object should implement the
+ * following functions:
+ *
+ * create() - create the UI
+ * destroy() - destroy the host's UI
+ */
+
+/**
+ * Host object for the dock on the bottom of the browser
+ */
+function BottomHost(hostTab) {
+ this.hostTab = hostTab;
+
+ EventEmitter.decorate(this);
+}
+
+BottomHost.prototype = {
+ type: "bottom",
+
+ heightPref: "devtools.toolbox.footer.height",
+
+ /**
+ * Create a box at the bottom of the host tab.
+ */
+ async create() {
+ await gDevToolsBrowser.loadBrowserStyleSheet(this.hostTab.ownerGlobal);
+
+ const gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
+ const ownerDocument = gBrowser.ownerDocument;
+ this._browserContainer = gBrowser.getBrowserContainer(
+ this.hostTab.linkedBrowser
+ );
+
+ this._splitter = ownerDocument.createXULElement("splitter");
+ this._splitter.setAttribute("class", "devtools-horizontal-splitter");
+ this._splitter.setAttribute("resizebefore", "none");
+ this._splitter.setAttribute("resizeafter", "sibling");
+
+ this.frame = createDevToolsFrame(
+ ownerDocument,
+ "devtools-toolbox-bottom-iframe"
+ );
+ this.frame.style.height =
+ Math.min(
+ Services.prefs.getIntPref(this.heightPref),
+ this._browserContainer.clientHeight - MIN_PAGE_SIZE
+ ) + "px";
+
+ this._browserContainer.appendChild(this._splitter);
+ this._browserContainer.appendChild(this.frame);
+
+ focusTab(this.hostTab);
+ return this.frame;
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise() {
+ focusTab(this.hostTab);
+ },
+
+ /**
+ * Set the toolbox title.
+ * Nothing to do for this host type.
+ */
+ setTitle() {},
+
+ /**
+ * Destroy the bottom dock.
+ */
+ destroy() {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ const height = parseInt(this.frame.style.height, 10);
+ if (!isNaN(height)) {
+ Services.prefs.setIntPref(this.heightPref, height);
+ }
+
+ this._browserContainer.removeChild(this._splitter);
+ this._browserContainer.removeChild(this.frame);
+ this.frame = null;
+ this._browserContainer = null;
+ this._splitter = null;
+ }
+
+ return Promise.resolve(null);
+ },
+};
+
+/**
+ * Base Host object for the in-browser sidebar
+ */
+class SidebarHost {
+ constructor(hostTab, type) {
+ this.hostTab = hostTab;
+ this.type = type;
+ this.widthPref = "devtools.toolbox.sidebar.width";
+
+ EventEmitter.decorate(this);
+ }
+
+ /**
+ * Create a box in the sidebar of the host tab.
+ */
+ async create() {
+ await gDevToolsBrowser.loadBrowserStyleSheet(this.hostTab.ownerGlobal);
+ const gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
+ const ownerDocument = gBrowser.ownerDocument;
+ this._browserContainer = gBrowser.getBrowserContainer(
+ this.hostTab.linkedBrowser
+ );
+ this._browserPanel = gBrowser.getPanel(this.hostTab.linkedBrowser);
+
+ this._splitter = ownerDocument.createXULElement("splitter");
+ this._splitter.setAttribute("class", "devtools-side-splitter");
+
+ this.frame = createDevToolsFrame(
+ ownerDocument,
+ "devtools-toolbox-side-iframe"
+ );
+ this.frame.style.width =
+ Math.min(
+ Services.prefs.getIntPref(this.widthPref),
+ this._browserPanel.clientWidth - MIN_PAGE_SIZE
+ ) + "px";
+
+ // We should consider the direction when changing the dock position.
+ const topWindow = this.hostTab.ownerDocument.defaultView.top;
+ const topDoc = topWindow.document.documentElement;
+ const isLTR = topWindow.getComputedStyle(topDoc).direction === "ltr";
+
+ this._splitter.setAttribute("resizebefore", "none");
+ this._splitter.setAttribute("resizeafter", "none");
+
+ if ((isLTR && this.type == "right") || (!isLTR && this.type == "left")) {
+ this._splitter.setAttribute("resizeafter", "sibling");
+ this._browserPanel.appendChild(this._splitter);
+ this._browserPanel.appendChild(this.frame);
+ } else {
+ this._splitter.setAttribute("resizebefore", "sibling");
+ this._browserPanel.insertBefore(this.frame, this._browserContainer);
+ this._browserPanel.insertBefore(this._splitter, this._browserContainer);
+ }
+
+ focusTab(this.hostTab);
+ return this.frame;
+ }
+
+ /**
+ * Raise the host.
+ */
+ raise() {
+ focusTab(this.hostTab);
+ }
+
+ /**
+ * Set the toolbox title.
+ * Nothing to do for this host type.
+ */
+ setTitle() {}
+
+ /**
+ * Destroy the sidebar.
+ */
+ destroy() {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ const width = parseInt(this.frame.style.width, 10);
+ if (!isNaN(width)) {
+ Services.prefs.setIntPref(this.widthPref, width);
+ }
+
+ this._browserPanel.removeChild(this._splitter);
+ this._browserPanel.removeChild(this.frame);
+ }
+
+ return Promise.resolve(null);
+ }
+}
+
+/**
+ * Host object for the in-browser left sidebar
+ */
+class LeftHost extends SidebarHost {
+ constructor(hostTab) {
+ super(hostTab, "left");
+ }
+}
+
+/**
+ * Host object for the in-browser right sidebar
+ */
+class RightHost extends SidebarHost {
+ constructor(hostTab) {
+ super(hostTab, "right");
+ }
+}
+
+/**
+ * Host object for the toolbox in a separate window
+ */
+function WindowHost(hostTab, options) {
+ this._boundUnload = this._boundUnload.bind(this);
+ this.hostTab = hostTab;
+ this.options = options;
+ EventEmitter.decorate(this);
+}
+
+WindowHost.prototype = {
+ type: "window",
+
+ WINDOW_URL: "chrome://devtools/content/framework/toolbox-window.xhtml",
+
+ /**
+ * Create a new xul window to contain the toolbox.
+ */
+ create() {
+ return new Promise(resolve => {
+ let flags = "chrome,centerscreen,resizable,dialog=no";
+
+ // If we are debugging a tab which is in a Private window, we must also
+ // set the private flag on the DevTools host window. Otherwise switching
+ // hosts between docked and window modes can fail due to incompatible
+ // docshell origin attributes. See 1581093.
+ const owner = this.hostTab?.ownerGlobal;
+ if (owner && lazy.PrivateBrowsingUtils.isWindowPrivate(owner)) {
+ flags += ",private";
+ }
+
+ // If the current window is a non-fission window, force the non-fission
+ // flag. Otherwise switching to window host from a non-fission window in
+ // a fission Firefox (!) will attempt to swapFrameLoaders between fission
+ // and non-fission frames. See Bug 1650963.
+ if (this.hostTab && !this.hostTab.ownerGlobal.gFissionBrowser) {
+ flags += ",non-fission";
+ }
+
+ // When debugging local Web Extension, the toolbox is opened in an
+ // always foremost top level window in order to be kept visible
+ // when interacting with the Firefox Window.
+ if (this.options?.alwaysOnTop) {
+ flags += ",alwaysontop";
+ }
+
+ const win = Services.ww.openWindow(
+ null,
+ this.WINDOW_URL,
+ "_blank",
+ flags,
+ null
+ );
+
+ const frameLoad = () => {
+ win.removeEventListener("load", frameLoad, true);
+ win.focus();
+
+ this.frame = createDevToolsFrame(
+ win.document,
+ "devtools-toolbox-window-iframe"
+ );
+ win.document
+ .getElementById("devtools-toolbox-window")
+ .appendChild(this.frame);
+
+ // The forceOwnRefreshDriver attribute is set to avoid Windows only issues with
+ // CSS transitions when switching from docked to window hosts.
+ // Added in Bug 832920, should be reviewed in Bug 1542468.
+ this.frame.setAttribute("forceOwnRefreshDriver", "");
+ resolve(this.frame);
+ };
+
+ win.addEventListener("load", frameLoad, true);
+ win.addEventListener("unload", this._boundUnload);
+
+ this._window = win;
+ });
+ },
+
+ /**
+ * Catch the user closing the window.
+ */
+ _boundUnload(event) {
+ if (event.target.location != this.WINDOW_URL) {
+ return;
+ }
+ this._window.removeEventListener("unload", this._boundUnload);
+
+ this.emit("window-closed");
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise() {
+ this._window.focus();
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle(title) {
+ this._window.document.title = title;
+ },
+
+ /**
+ * Destroy the window.
+ */
+ destroy() {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ this._window.removeEventListener("unload", this._boundUnload);
+ this._window.close();
+ }
+
+ return Promise.resolve(null);
+ },
+};
+
+/**
+ * Host object for the Browser Toolbox
+ */
+function BrowserToolboxHost(hostTab, options) {
+ this.doc = options.doc;
+ EventEmitter.decorate(this);
+}
+
+BrowserToolboxHost.prototype = {
+ type: "browsertoolbox",
+
+ async create() {
+ this.frame = createDevToolsFrame(
+ this.doc,
+ "devtools-toolbox-browsertoolbox-iframe"
+ );
+
+ this.doc.body.appendChild(this.frame);
+
+ return this.frame;
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise() {
+ this.doc.defaultView.focus();
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle(title) {
+ this.doc.title = title;
+ },
+
+ // Do nothing. The BrowserToolbox is destroyed by quitting the application.
+ destroy() {
+ return Promise.resolve(null);
+ },
+};
+
+/**
+ * Host object for the toolbox as a page.
+ * This is typically used by `about:debugging`, when opening toolbox in a new tab,
+ * via `about:devtools-toolbox` URLs.
+ * The `iframe` ends up being the tab's browser element.
+ */
+function PageHost(hostTab, options) {
+ this.frame = options.customIframe;
+}
+
+PageHost.prototype = {
+ type: "page",
+
+ create() {
+ return Promise.resolve(this.frame);
+ },
+
+ // Do nothing.
+ raise() {},
+
+ // Do nothing.
+ setTitle(title) {},
+
+ // Do nothing.
+ destroy() {
+ return Promise.resolve(null);
+ },
+};
+
+/**
+ * Switch to the given tab in a browser and focus the browser window
+ */
+function focusTab(tab) {
+ const browserWindow = tab.ownerDocument.defaultView;
+ browserWindow.focus();
+ browserWindow.gBrowser.selectedTab = tab;
+}
+
+/**
+ * Create an iframe that can be used to load DevTools via about:devtools-toolbox.
+ */
+function createDevToolsFrame(doc, className) {
+ const frame = doc.createXULElement("browser");
+ frame.setAttribute("type", "content");
+ frame.style.flex = "1 auto"; // Required to be able to shrink when the window shrinks
+ frame.className = className;
+
+ const inXULDocument = doc.documentElement.namespaceURI === XUL_NS;
+ if (inXULDocument) {
+ // When the toolbox frame is loaded in a XUL document, tooltips rely on a
+ // special XUL <tooltip id="aHTMLTooltip"> element.
+ // This attribute should not be set when the frame is loaded in a HTML
+ // document (for instance: Browser Toolbox).
+ frame.tooltip = "aHTMLTooltip";
+ }
+ return frame;
+}
+
+exports.Hosts = {
+ bottom: BottomHost,
+ left: LeftHost,
+ right: RightHost,
+ window: WindowHost,
+ browsertoolbox: BrowserToolboxHost,
+ page: PageHost,
+};