diff options
Diffstat (limited to 'devtools/client/framework/toolbox-hosts.js')
-rw-r--r-- | devtools/client/framework/toolbox-hosts.js | 425 |
1 files changed, 425 insertions, 0 deletions
diff --git a/devtools/client/framework/toolbox-hosts.js b/devtools/client/framework/toolbox-hosts.js new file mode 100644 index 000000000..ea774549a --- /dev/null +++ b/devtools/client/framework/toolbox-hosts.js @@ -0,0 +1,425 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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("devtools/shared/event-emitter"); +const promise = require("promise"); +const defer = require("devtools/shared/defer"); +const Services = require("Services"); +const {DOMHelpers} = require("resource://devtools/client/shared/DOMHelpers.jsm"); + +loader.lazyRequireGetter(this, "system", "devtools/shared/system"); + +/* 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.height to be larger than that, since it can cause problems with + * resizing the toolbox and panel layout. */ +const MIN_PAGE_SIZE = 25; + +/** + * 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 and emit a 'ready' event when the UI is ready to use + * destroy() - destroy the host's UI + */ + +exports.Hosts = { + "bottom": BottomHost, + "side": SidebarHost, + "window": WindowHost, + "custom": CustomHost +}; + +/** + * 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. + */ + create: function () { + let deferred = defer(); + + let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser; + let ownerDocument = gBrowser.ownerDocument; + this._nbox = gBrowser.getNotificationBox(this.hostTab.linkedBrowser); + + this._splitter = ownerDocument.createElement("splitter"); + this._splitter.setAttribute("class", "devtools-horizontal-splitter"); + // Avoid resizing notification containers + this._splitter.setAttribute("resizebefore", "flex"); + + this.frame = ownerDocument.createElement("iframe"); + this.frame.className = "devtools-toolbox-bottom-iframe"; + this.frame.height = Math.min( + Services.prefs.getIntPref(this.heightPref), + this._nbox.clientHeight - MIN_PAGE_SIZE + ); + + this._nbox.appendChild(this._splitter); + this._nbox.appendChild(this.frame); + + let frameLoad = () => { + this.emit("ready", this.frame); + deferred.resolve(this.frame); + }; + + this.frame.tooltip = "aHTMLTooltip"; + + // we have to load something so we can switch documents if we have to + this.frame.setAttribute("src", "about:blank"); + + let domHelper = new DOMHelpers(this.frame.contentWindow); + domHelper.onceDOMReady(frameLoad); + + focusTab(this.hostTab); + + return deferred.promise; + }, + + /** + * Raise the host. + */ + raise: function () { + focusTab(this.hostTab); + }, + + /** + * Minimize this host so that only the toolbox tabbar remains visible. + * @param {Number} height The height to minimize to. Defaults to 0, which + * means that the toolbox won't be visible at all once minimized. + */ + minimize: function (height = 0) { + if (this.isMinimized) { + return; + } + this.isMinimized = true; + + let onTransitionEnd = event => { + if (event.propertyName !== "margin-bottom") { + // Ignore transitionend on unrelated properties. + return; + } + + this.frame.removeEventListener("transitionend", onTransitionEnd); + this.emit("minimized"); + }; + this.frame.addEventListener("transitionend", onTransitionEnd); + this.frame.style.marginBottom = -this.frame.height + height + "px"; + this._splitter.classList.add("disabled"); + }, + + /** + * If the host was minimized before, maximize it again (the host will be + * maximized to the height it previously had). + */ + maximize: function () { + if (!this.isMinimized) { + return; + } + this.isMinimized = false; + + let onTransitionEnd = event => { + if (event.propertyName !== "margin-bottom") { + // Ignore transitionend on unrelated properties. + return; + } + + this.frame.removeEventListener("transitionend", onTransitionEnd); + this.emit("maximized"); + }; + this.frame.addEventListener("transitionend", onTransitionEnd); + this.frame.style.marginBottom = "0"; + this._splitter.classList.remove("disabled"); + }, + + /** + * Toggle the minimize mode. + * @param {Number} minHeight The height to minimize to. + */ + toggleMinimizeMode: function (minHeight) { + this.isMinimized ? this.maximize() : this.minimize(minHeight); + }, + + /** + * Set the toolbox title. + * Nothing to do for this host type. + */ + setTitle: function () {}, + + /** + * Destroy the bottom dock. + */ + destroy: function () { + if (!this._destroyed) { + this._destroyed = true; + + Services.prefs.setIntPref(this.heightPref, this.frame.height); + this._nbox.removeChild(this._splitter); + this._nbox.removeChild(this.frame); + this.frame = null; + this._nbox = null; + this._splitter = null; + } + + return promise.resolve(null); + } +}; + +/** + * Host object for the in-browser sidebar + */ +function SidebarHost(hostTab) { + this.hostTab = hostTab; + + EventEmitter.decorate(this); +} + +SidebarHost.prototype = { + type: "side", + + widthPref: "devtools.toolbox.sidebar.width", + + /** + * Create a box in the sidebar of the host tab. + */ + create: function () { + let deferred = defer(); + + let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser; + let ownerDocument = gBrowser.ownerDocument; + this._sidebar = gBrowser.getSidebarContainer(this.hostTab.linkedBrowser); + + this._splitter = ownerDocument.createElement("splitter"); + this._splitter.setAttribute("class", "devtools-side-splitter"); + + this.frame = ownerDocument.createElement("iframe"); + this.frame.className = "devtools-toolbox-side-iframe"; + + this.frame.width = Math.min( + Services.prefs.getIntPref(this.widthPref), + this._sidebar.clientWidth - MIN_PAGE_SIZE + ); + + this._sidebar.appendChild(this._splitter); + this._sidebar.appendChild(this.frame); + + let frameLoad = () => { + this.emit("ready", this.frame); + deferred.resolve(this.frame); + }; + + this.frame.tooltip = "aHTMLTooltip"; + this.frame.setAttribute("src", "about:blank"); + + let domHelper = new DOMHelpers(this.frame.contentWindow); + domHelper.onceDOMReady(frameLoad); + + focusTab(this.hostTab); + + return deferred.promise; + }, + + /** + * Raise the host. + */ + raise: function () { + focusTab(this.hostTab); + }, + + /** + * Set the toolbox title. + * Nothing to do for this host type. + */ + setTitle: function () {}, + + /** + * Destroy the sidebar. + */ + destroy: function () { + if (!this._destroyed) { + this._destroyed = true; + + Services.prefs.setIntPref(this.widthPref, this.frame.width); + this._sidebar.removeChild(this._splitter); + this._sidebar.removeChild(this.frame); + } + + return promise.resolve(null); + } +}; + +/** + * Host object for the toolbox in a separate window + */ +function WindowHost() { + this._boundUnload = this._boundUnload.bind(this); + + EventEmitter.decorate(this); +} + +WindowHost.prototype = { + type: "window", + + WINDOW_URL: "chrome://devtools/content/framework/toolbox-window.xul", + + /** + * Create a new xul window to contain the toolbox. + */ + create: function () { + let deferred = defer(); + + let flags = "chrome,centerscreen,resizable,dialog=no"; + let win = Services.ww.openWindow(null, this.WINDOW_URL, "_blank", + flags, null); + + let frameLoad = () => { + win.removeEventListener("load", frameLoad, true); + win.focus(); + + let key; + if (system.constants.platform === "macosx") { + key = win.document.getElementById("toolbox-key-toggle-osx"); + } else { + key = win.document.getElementById("toolbox-key-toggle"); + } + key.removeAttribute("disabled"); + + this.frame = win.document.getElementById("toolbox-iframe"); + this.emit("ready", this.frame); + + deferred.resolve(this.frame); + }; + + win.addEventListener("load", frameLoad, true); + win.addEventListener("unload", this._boundUnload); + + this._window = win; + + return deferred.promise; + }, + + /** + * Catch the user closing the window. + */ + _boundUnload: function (event) { + if (event.target.location != this.WINDOW_URL) { + return; + } + this._window.removeEventListener("unload", this._boundUnload); + + this.emit("window-closed"); + }, + + /** + * Raise the host. + */ + raise: function () { + this._window.focus(); + }, + + /** + * Set the toolbox title. + */ + setTitle: function (title) { + this._window.document.title = title; + }, + + /** + * Destroy the window. + */ + destroy: function () { + if (!this._destroyed) { + this._destroyed = true; + + this._window.removeEventListener("unload", this._boundUnload); + this._window.close(); + } + + return promise.resolve(null); + } +}; + +/** + * Host object for the toolbox in its own tab + */ +function CustomHost(hostTab, options) { + this.frame = options.customIframe; + this.uid = options.uid; + EventEmitter.decorate(this); +} + +CustomHost.prototype = { + type: "custom", + + _sendMessageToTopWindow: function (msg, data) { + // It's up to the custom frame owner (parent window) to honor + // "close" or "raise" instructions. + let topWindow = this.frame.ownerDocument.defaultView; + if (!topWindow) { + return; + } + let json = {name: "toolbox-" + msg, uid: this.uid}; + if (data) { + json.data = data; + } + topWindow.postMessage(JSON.stringify(json), "*"); + }, + + /** + * Create a new xul window to contain the toolbox. + */ + create: function () { + return promise.resolve(this.frame); + }, + + /** + * Raise the host. + */ + raise: function () { + this._sendMessageToTopWindow("raise"); + }, + + /** + * Set the toolbox title. + */ + setTitle: function (title) { + this._sendMessageToTopWindow("title", { value: title }); + }, + + /** + * Destroy the window. + */ + destroy: function () { + if (!this._destroyed) { + this._destroyed = true; + this._sendMessageToTopWindow("close"); + } + return promise.resolve(null); + } +}; + +/** + * Switch to the given tab in a browser and focus the browser window + */ +function focusTab(tab) { + let browserWindow = tab.ownerDocument.defaultView; + browserWindow.focus(); + browserWindow.gBrowser.selectedTab = tab; +} |