/* 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 SOURCE_MAP_WORKER = "resource://devtools/client/shared/source-map/worker.js"; const MAX_ORDINAL = 99; const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled"; const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight"; const DISABLE_AUTOHIDE_PREF = "ui.popup.disable_autohide"; const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST"; const CURRENT_THEME_SCALAR = "devtools.current_theme"; const HTML_NS = "http://www.w3.org/1999/xhtml"; var {Ci, Cc} = require("chrome"); var promise = require("promise"); const { debounce } = require("devtools/shared/debounce"); var Services = require("Services"); var ChromeUtils = require("ChromeUtils"); var {gDevTools} = require("devtools/client/framework/devtools"); var EventEmitter = require("devtools/shared/event-emitter"); var Telemetry = require("devtools/client/shared/telemetry"); const { getUnicodeUrl } = require("devtools/client/shared/unicode-url"); var { attachThread, detachThread } = require("./attach-thread"); var Menu = require("devtools/client/framework/menu"); var MenuItem = require("devtools/client/framework/menu-item"); var { DOMHelpers } = require("resource://devtools/client/shared/DOMHelpers.jsm"); const { KeyCodes } = require("devtools/client/shared/keycodes"); var Startup = Cc["@mozilla.org/devtools/startup-clh;1"].getService(Ci.nsISupports) .wrappedJSObject; const { BrowserLoader } = ChromeUtils.import("resource://devtools/client/shared/browser-loader.js", {}); const {LocalizationHelper} = require("devtools/shared/l10n"); const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties"); loader.lazyRequireGetter(this, "getHighlighterUtils", "devtools/client/framework/toolbox-highlighter-utils", true); loader.lazyRequireGetter(this, "Selection", "devtools/client/framework/selection", true); loader.lazyRequireGetter(this, "InspectorFront", "devtools/shared/fronts/inspector", true); loader.lazyRequireGetter(this, "flags", "devtools/shared/flags"); loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts"); loader.lazyRequireGetter(this, "ZoomKeys", "devtools/client/shared/zoom-keys"); loader.lazyRequireGetter(this, "settleAll", "devtools/shared/ThreadSafeDevToolsUtils", true); loader.lazyRequireGetter(this, "ToolboxButtons", "devtools/client/definitions", true); loader.lazyRequireGetter(this, "SourceMapURLService", "devtools/client/framework/source-map-url-service", true); loader.lazyRequireGetter(this, "HUDService", "devtools/client/webconsole/hudservice", true); loader.lazyRequireGetter(this, "viewSource", "devtools/client/shared/view-source"); loader.lazyRequireGetter(this, "buildHarLog", "devtools/client/netmonitor/src/har/har-builder-utils", true); loader.lazyRequireGetter(this, "NetMonitorAPI", "devtools/client/netmonitor/src/api", true); loader.lazyRequireGetter(this, "sortPanelDefinitions", "devtools/client/framework/toolbox-tabs-order-manager", true); loader.lazyGetter(this, "domNodeConstants", () => { return require("devtools/shared/dom-node-constants"); }); loader.lazyGetter(this, "registerHarOverlay", () => { return require("devtools/client/netmonitor/src/har/toolbox-overlay").register; }); /** * A "Toolbox" is the component that holds all the tools for one specific * target. Visually, it's a document that includes the tools tabs and all * the iframes where the tool panels will be living in. * * @param {object} target * The object the toolbox is debugging. * @param {string} selectedTool * Tool to select initially * @param {Toolbox.HostType} hostType * Type of host that will host the toolbox (e.g. sidebar, window) * @param {DOMWindow} contentWindow * The window object of the toolbox document * @param {string} frameId * A unique identifier to differentiate toolbox documents from the * chrome codebase when passing DOM messages * @param {Number} msSinceProcessStart * the number of milliseconds since process start using monotonic * timestamps (unaffected by system clock changes). */ function Toolbox(target, selectedTool, hostType, contentWindow, frameId, msSinceProcessStart) { this._target = target; this._win = contentWindow; this.frameId = frameId; this.telemetry = new Telemetry(); // The session ID is used to determine which telemetry events belong to which // toolbox session. Because we use Amplitude to analyse the telemetry data we // must use the time since the system wide epoch as the session ID. this.sessionId = msSinceProcessStart; // Map of the available DevTools WebExtensions: // Map this._webExtensions = new Map(); this._toolPanels = new Map(); // Map of tool startup components for given tool id. this._toolStartups = new Map(); this._inspectorExtensionSidebars = new Map(); this._initInspector = null; this._inspector = null; this._netMonitorAPI = null; // Map of frames (id => frame-info) and currently selected frame id. this.frameMap = new Map(); this.selectedFrameId = null; this._toolRegistered = this._toolRegistered.bind(this); this._toolUnregistered = this._toolUnregistered.bind(this); this._onWillNavigate = this._onWillNavigate.bind(this); this._refreshHostTitle = this._refreshHostTitle.bind(this); this.toggleNoAutohide = this.toggleNoAutohide.bind(this); this.showFramesMenu = this.showFramesMenu.bind(this); this.handleKeyDownOnFramesButton = this.handleKeyDownOnFramesButton.bind(this); this.showFramesMenuOnKeyDown = this.showFramesMenuOnKeyDown.bind(this); this._updateFrames = this._updateFrames.bind(this); this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this); this.destroy = this.destroy.bind(this); this.highlighterUtils = getHighlighterUtils(this); this._highlighterReady = this._highlighterReady.bind(this); this._highlighterHidden = this._highlighterHidden.bind(this); this._applyCacheSettings = this._applyCacheSettings.bind(this); this._applyServiceWorkersTestingSettings = this._applyServiceWorkersTestingSettings.bind(this); this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this); this._onFocus = this._onFocus.bind(this); this._onBrowserMessage = this._onBrowserMessage.bind(this); this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this); this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this); this._onTabsOrderUpdated = this._onTabsOrderUpdated.bind(this); this._onToolbarFocus = this._onToolbarFocus.bind(this); this._onToolbarArrowKeypress = this._onToolbarArrowKeypress.bind(this); this._onPickerClick = this._onPickerClick.bind(this); this._onPickerKeypress = this._onPickerKeypress.bind(this); this._onPickerStarted = this._onPickerStarted.bind(this); this._onPickerStopped = this._onPickerStopped.bind(this); this._onInspectObject = this._onInspectObject.bind(this); this._onNewSelectedNodeFront = this._onNewSelectedNodeFront.bind(this); this._onToolSelected = this._onToolSelected.bind(this); this.updateToolboxButtonsVisibility = this.updateToolboxButtonsVisibility.bind(this); this.selectTool = this.selectTool.bind(this); this._pingTelemetrySelectTool = this._pingTelemetrySelectTool.bind(this); this.toggleSplitConsole = this.toggleSplitConsole.bind(this); this.toggleOptions = this.toggleOptions.bind(this); this._target.on("close", this.destroy); if (!selectedTool) { selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); } this._defaultToolId = selectedTool; this._hostType = hostType; this.isOpen = new Promise(function(resolve) { this._resolveIsOpen = resolve; }.bind(this)); EventEmitter.decorate(this); this._target.on("will-navigate", this._onWillNavigate); this._target.on("navigate", this._refreshHostTitle); this._target.on("frame-update", this._updateFrames); this._target.on("inspect-object", this._onInspectObject); this.on("host-changed", this._refreshHostTitle); this.on("select", this._onToolSelected); gDevTools.on("tool-registered", this._toolRegistered); gDevTools.on("tool-unregistered", this._toolUnregistered); this.on("picker-started", this._onPickerStarted); this.on("picker-stopped", this._onPickerStopped); /** * Get text direction for the current locale direction. * * `getComputedStyle` forces a synchronous reflow, so use a lazy getter in order to * call it only once. */ loader.lazyGetter(this, "direction", () => { // Get the direction from browser.xul document const top = this.win.top; const topDocEl = top.document.documentElement; const isRtl = top.getComputedStyle(topDocEl).direction === "rtl"; return isRtl ? "rtl" : "ltr"; }); } exports.Toolbox = Toolbox; /** * The toolbox can be 'hosted' either embedded in a browser window * or in a separate window. */ Toolbox.HostType = { BOTTOM: "bottom", RIGHT: "right", LEFT: "left", WINDOW: "window", CUSTOM: "custom" }; Toolbox.prototype = { _URL: "about:devtools-toolbox", _prefs: { LAST_TOOL: "devtools.toolbox.selectedTool", SIDE_ENABLED: "devtools.toolbox.sideEnabled", }, get currentToolId() { return this._currentToolId; }, set currentToolId(id) { this._currentToolId = id; this.component.setCurrentToolId(id); }, get defaultToolId() { return this._defaultToolId; }, get panelDefinitions() { return this._panelDefinitions; }, set panelDefinitions(definitions) { this._panelDefinitions = definitions; this._combineAndSortPanelDefinitions(); }, get visibleAdditionalTools() { if (!this._visibleAdditionalTools) { this._visibleAdditionalTools = []; } return this._visibleAdditionalTools; }, set visibleAdditionalTools(tools) { this._visibleAdditionalTools = tools; if (this.isReady) { this._combineAndSortPanelDefinitions(); } }, /** * Combines the built-in panel definitions and the additional tool definitions that * can be set by add-ons. */ _combineAndSortPanelDefinitions() { let definitions = [...this._panelDefinitions, ...this.getVisibleAdditionalTools()]; definitions = sortPanelDefinitions(definitions); this.component.setPanelDefinitions(definitions); }, lastUsedToolId: null, /** * Returns a *copy* of the _toolPanels collection. * * @return {Map} panels * All the running panels in the toolbox */ getToolPanels: function() { return new Map(this._toolPanels); }, /** * Access the panel for a given tool */ getPanel: function(id) { return this._toolPanels.get(id); }, /** * Get the panel instance for a given tool once it is ready. * If the tool is already opened, the promise will resolve immediately, * otherwise it will wait until the tool has been opened before resolving. * * Note that this does not open the tool, use selectTool if you'd * like to select the tool right away. * * @param {String} id * The id of the panel, for example "jsdebugger". * @returns Promise * A promise that resolves once the panel is ready. */ getPanelWhenReady: function(id) { const panel = this.getPanel(id); return new Promise(resolve => { if (panel) { resolve(panel); } else { this.on(id + "-ready", initializedPanel => { resolve(initializedPanel); }); } }); }, /** * This is a shortcut for getPanel(currentToolId) because it is much more * likely that we're going to want to get the panel that we've just made * visible */ getCurrentPanel: function() { return this._toolPanels.get(this.currentToolId); }, /** * Get/alter the target of a Toolbox so we're debugging something different. * See Target.jsm for more details. * TODO: Do we allow |toolbox.target = null;| ? */ get target() { return this._target; }, get threadClient() { return this._threadClient; }, /** * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate * tab. See HostType for more details. */ get hostType() { return this._hostType; }, /** * Shortcut to the window containing the toolbox UI */ get win() { return this._win; }, /** * Shortcut to the document containing the toolbox UI */ get doc() { return this.win.document; }, /** * Get the toolbox highlighter front. Note that it may not always have been * initialized first. Use `initInspector()` if needed. * Consider using highlighterUtils instead, it exposes the highlighter API in * a useful way for the toolbox panels */ get highlighter() { return this._highlighter; }, /** * Get the toolbox's performance front. Note that it may not always have been * initialized first. Use `initPerformance()` if needed. */ get performance() { return this._performance; }, /** * Get the toolbox's inspector front. Note that it may not always have been * initialized first. Use `initInspector()` if needed. */ get inspector() { return this._inspector; }, /** * Get the toolbox's walker front. Note that it may not always have been * initialized first. Use `initInspector()` if needed. */ get walker() { return this._walker; }, /** * Get the toolbox's node selection. Note that it may not always have been * initialized first. Use `initInspector()` if needed. */ get selection() { return this._selection; }, /** * Get the toggled state of the split console */ get splitConsole() { return this._splitConsole; }, /** * Get the focused state of the split console */ isSplitConsoleFocused: function() { if (!this._splitConsole) { return false; } const focusedWin = Services.focus.focusedWindow; return focusedWin && focusedWin === this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow; }, /** * Open the toolbox */ open: function() { return (async function() { this.browserRequire = BrowserLoader({ window: this.doc.defaultView, useOnlyShared: true }).require; if (this.win.location.href.startsWith(this._URL)) { // Update the URL so that onceDOMReady watch for the right url. this._URL = this.win.location.href; } const domHelper = new DOMHelpers(this.win); const domReady = new Promise(resolve => { domHelper.onceDOMReady(() => { resolve(); }, this._URL); }); // Optimization: fire up a few other things before waiting on // the iframe being ready (makes startup faster) // Load the toolbox-level actor fronts and utilities now await this._target.makeRemote(); // Start tracking network activity on toolbox open for targets such as tabs. // (Workers and potentially others don't manage the console client in the target.) if (this._target.activeConsole) { await this._target.activeConsole.startListeners([ "NetworkActivity", ]); } // Attach the thread this._threadClient = await attachThread(this); await domReady; this.isReady = true; const framesPromise = this._listFrames(); Services.prefs.addObserver("devtools.cache.disabled", this._applyCacheSettings); Services.prefs.addObserver("devtools.serviceWorkers.testing.enabled", this._applyServiceWorkersTestingSettings); this.textBoxContextMenuPopup = this.doc.getElementById("toolbox-textbox-context-popup"); this.textBoxContextMenuPopup.addEventListener("popupshowing", this._updateTextBoxMenuItems, true); this.doc.addEventListener("contextmenu", (e) => { if (e.originalTarget.closest("input[type=text]") || e.originalTarget.closest("input[type=search]") || e.originalTarget.closest("input:not([type])") || e.originalTarget.closest("textarea")) { e.stopPropagation(); e.preventDefault(); this.openTextBoxContextMenu(e.screenX, e.screenY); } }); this.shortcuts = new KeyShortcuts({ window: this.doc.defaultView }); // Get the DOM element to mount the ToolboxController to. this._componentMount = this.doc.getElementById("toolbox-toolbar-mount"); this._mountReactComponent(); this._buildDockOptions(); this._buildOptions(); this._buildTabs(); this._applyCacheSettings(); this._applyServiceWorkersTestingSettings(); this._addKeysToWindow(); this._addReloadKeys(); this._addHostListeners(); this._registerOverlays(); if (!this._hostOptions || this._hostOptions.zoom === true) { ZoomKeys.register(this.win); } this._componentMount.addEventListener("keypress", this._onToolbarArrowKeypress); this._componentMount.setAttribute("aria-label", L10N.getStr("toolbox.label")); this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole"); this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF); this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight); this._buildButtons(); this._pingTelemetry(); // The isTargetSupported check needs to happen after the target is // remoted, otherwise we could have done it in the toolbox constructor // (bug 1072764). const toolDef = gDevTools.getToolDefinition(this._defaultToolId); if (!toolDef || !toolDef.isTargetSupported(this._target)) { this._defaultToolId = "webconsole"; } // Start rendering the toolbox toolbar before selecting the tool, as the tools // can take a few hundred milliseconds seconds to start up. // // Delay React rendering as Toolbox.open is synchronous. // Even if this involve promises, it is synchronous. Toolbox.open already loads // react modules and freeze the event loop for a significant time. // requestIdleCallback allows releasing it to allow user events to be processed. // Use 16ms maximum delay to allow one frame to be rendered at 60FPS // (1000ms/60FPS=16ms) this.win.requestIdleCallback(() => { this.component.setCanRender(); }, {timeout: 16}); await this.selectTool(this._defaultToolId, "initial_panel"); // Wait until the original tool is selected so that the split // console input will receive focus. let splitConsolePromise = promise.resolve(); if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) { splitConsolePromise = this.openSplitConsole(); this.telemetry.addEventProperty( "devtools.main", "open", "tools", null, "splitconsole", true); } else { this.telemetry.addEventProperty( "devtools.main", "open", "tools", null, "splitconsole", false); } await promise.all([ splitConsolePromise, framesPromise ]); // Lazily connect to the profiler here and don't wait for it to complete, // used to intercept console.profile calls before the performance tools are open. const performanceFrontConnection = this.initPerformance(); // If in testing environment, wait for performance connection to finish, // so we don't have to explicitly wait for this in tests; ideally, all tests // will handle this on their own, but each have their own tear down function. if (flags.testing) { await performanceFrontConnection; } this.emit("ready"); this._resolveIsOpen(); }.bind(this))().catch(e => { console.error("Exception while opening the toolbox", String(e), e); // While the exception stack is correctly printed in the Browser console when // passing `e` to console.error, it is not on the stdout, so print it via dump. dump(e.stack + "\n"); }); }, /** * loading React modules when needed (to avoid performance penalties * during Firefox start up time). */ get React() { return this.browserRequire("devtools/client/shared/vendor/react"); }, get ReactDOM() { return this.browserRequire("devtools/client/shared/vendor/react-dom"); }, get ReactRedux() { return this.browserRequire("devtools/client/shared/vendor/react-redux"); }, get ToolboxController() { return this.browserRequire("devtools/client/framework/components/ToolboxController"); }, /** * Unconditionally create and get the source map service. */ _createSourceMapService: function() { if (this._sourceMapService) { return this._sourceMapService; } // Uses browser loader to access the `Worker` global. const service = this.browserRequire("devtools/client/shared/source-map/index"); // Provide a wrapper for the service that reports errors more nicely. this._sourceMapService = new Proxy(service, { get: (target, name) => { switch (name) { case "getOriginalURLs": return (urlInfo) => { return target.getOriginalURLs(urlInfo) .catch(text => { const message = L10N.getFormatStr("toolbox.sourceMapFailure", text, urlInfo.url, urlInfo.sourceMapURL); this.target.logWarningInPage(message, "source map"); // It's ok to swallow errors here, because a null // result just means that no source map was found. return null; }); }; case "getOriginalSourceText": return (originalSource) => { return target.getOriginalSourceText(originalSource) .catch(text => { const message = L10N.getFormatStr("toolbox.sourceMapSourceFailure", text, originalSource.url); this.target.logWarningInPage(message, "source map"); // Also replace the result with the error text. // Note that this result has to have the same form // as whatever the upstream getOriginalSourceText // returns. return { text: message, contentType: "text/plain", }; }); }; case "applySourceMap": return (generatedId, url, code, mappings) => { return target.applySourceMap(generatedId, url, code, mappings) .then(result => { // If a tool has changed or introduced a source map // (e.g, by pretty-printing a source), tell the // source map URL service about the change, so that // subscribers to that service can be updated as // well. if (this._sourceMapURLService) { this._sourceMapURLService.sourceMapChanged(generatedId, url); } return result; }); }; default: return target[name]; } }, }); this._sourceMapService.startSourceMapWorker(SOURCE_MAP_WORKER); return this._sourceMapService; }, /** * A common access point for the client-side mapping service for source maps that * any panel can use. This is a "low-level" API that connects to * the source map worker. */ get sourceMapService() { if (!Services.prefs.getBoolPref("devtools.source-map.client-service.enabled")) { return null; } return this._createSourceMapService(); }, /** * Clients wishing to use source maps but that want the toolbox to * track the source and style sheet actor mapping can use this * source map service. This is a higher-level service than the one * returned by |sourceMapService|, in that it automatically tracks * source and style sheet actor IDs. */ get sourceMapURLService() { if (this._sourceMapURLService) { return this._sourceMapURLService; } const sourceMaps = this._createSourceMapService(); this._sourceMapURLService = new SourceMapURLService(this, sourceMaps); return this._sourceMapURLService; }, // Return HostType id for telemetry _getTelemetryHostId: function() { switch (this.hostType) { case Toolbox.HostType.BOTTOM: return 0; case Toolbox.HostType.RIGHT: return 1; case Toolbox.HostType.WINDOW: return 2; case Toolbox.HostType.CUSTOM: return 3; case Toolbox.HostType.LEFT: return 4; default: return 9; } }, // Return HostType string for telemetry _getTelemetryHostString: function() { switch (this.hostType) { case Toolbox.HostType.BOTTOM: return "bottom"; case Toolbox.HostType.LEFT: return "left"; case Toolbox.HostType.RIGHT: return "right"; case Toolbox.HostType.WINDOW: return "window"; case Toolbox.HostType.CUSTOM: return "other"; default: return "bottom"; } }, _pingTelemetry: function() { this.telemetry.toolOpened("toolbox"); this.telemetry.getHistogramById(HOST_HISTOGRAM).add(this._getTelemetryHostId()); // Log current theme. The question we want to answer is: // "What proportion of users use which themes?" const currentTheme = Services.prefs.getCharPref("devtools.theme"); this.telemetry.keyedScalarAdd(CURRENT_THEME_SCALAR, currentTheme, 1); this.telemetry.preparePendingEvent("devtools.main", "open", "tools", null, [ "entrypoint", "first_panel", "host", "shortcut", "splitconsole", "width", "session_id" ]); this.telemetry.addEventProperty( "devtools.main", "open", "tools", null, "host", this._getTelemetryHostString() ); }, /** * Create a simple object to store the state of a toolbox button. The checked state of * a button can be updated arbitrarily outside of the scope of the toolbar and its * controllers. In order to simplify this interaction this object emits an * "updatechecked" event any time the isChecked value is updated, allowing any consuming * components to listen and respond to updates. * * @param {Object} options: * * @property {String} id - The id of the button or command. * @property {String} className - An optional additional className for the button. * @property {String} description - The value that will display as a tooltip and in * the options panel for enabling/disabling. * @property {Boolean} disabled - An optional disabled state for the button. * @property {Function} onClick - The function to run when the button is activated by * click or keyboard shortcut. First argument will be the 'click' * event, and second argument is the toolbox instance. * @property {Boolean} isInStartContainer - Buttons can either be placed at the start * of the toolbar, or at the end. * @property {Function} setup - Function run immediately to listen for events changing * whenever the button is checked or unchecked. The toolbox object * is passed as first argument and a callback is passed as second * argument, to be called whenever the checked state changes. * @property {Function} teardown - Function run on toolbox close to let a chance to * unregister listeners set when `setup` was called and avoid * memory leaks. The same arguments than `setup` function are * passed to `teardown`. * @property {Function} isTargetSupported - Function to automatically enable/disable * the button based on the target. If the target don't support * the button feature, this method should return false. * @property {Function} isCurrentlyVisible - Function to automatically * hide/show the button based on current state. * @property {Function} isChecked - Optional function called to known if the button * is toggled or not. The function should return true when * the button should be displayed as toggled on. */ _createButtonState: function(options) { let isCheckedValue = false; const { id, className, description, disabled, onClick, isInStartContainer, setup, teardown, isTargetSupported, isCurrentlyVisible, isChecked, onKeyDown } = options; const toolbox = this; const button = { id, className, description, disabled, onClick(event) { if (typeof onClick == "function") { onClick(event, toolbox); } }, onKeyDown(event) { if (typeof onKeyDown == "function") { onKeyDown(event, toolbox); } }, isTargetSupported, isCurrentlyVisible, get isChecked() { if (typeof isChecked == "function") { return isChecked(toolbox); } return isCheckedValue; }, set isChecked(value) { // Note that if options.isChecked is given, this is ignored isCheckedValue = value; this.emit("updatechecked"); }, // The preference for having this button visible. visibilityswitch: `devtools.${id}.enabled`, // The toolbar has a container at the start and end of the toolbar for // holding buttons. By default the buttons are placed in the end container. isInStartContainer: !!isInStartContainer }; if (typeof setup == "function") { const onChange = () => { button.emit("updatechecked"); }; setup(this, onChange); // Save a reference to the cleanup method that will unregister the onChange // callback. Immediately bind the function argument so that we don't have to // also save a reference to them. button.teardown = teardown.bind(options, this, onChange); } button.isVisible = this._commandIsVisible(button); EventEmitter.decorate(button); return button; }, _buildOptions: function() { this.shortcuts.on(L10N.getStr("toolbox.help.key"), this.toggleOptions); }, _splitConsoleOnKeypress: function(e) { if (e.keyCode === KeyCodes.DOM_VK_ESCAPE) { this.toggleSplitConsole(); // If the debugger is paused, don't let the ESC key stop any pending // navigation. if (this._threadClient.state == "paused") { e.preventDefault(); } } }, /** * Add a shortcut key that should work when a split console * has focus to the toolbox. * * @param {String} key * The electron key shortcut. * @param {Function} handler * The callback that should be called when the provided key shortcut is pressed. * @param {String} whichTool * The tool the key belongs to. The corresponding handler will only be triggered * if this tool is active. */ useKeyWithSplitConsole: function(key, handler, whichTool) { this.shortcuts.on(key, event => { if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) { handler(); event.preventDefault(); } }); }, _addReloadKeys: function() { [ ["reload", false], ["reload2", false], ["forceReload", true], ["forceReload2", true] ].forEach(([id, force]) => { const key = L10N.getStr("toolbox." + id + ".key"); this.shortcuts.on(key, event => { this.reloadTarget(force); // Prevent Firefox shortcuts from reloading the page event.preventDefault(); }); }); }, _addHostListeners: function() { this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"), event => { this.selectNextTool(); event.preventDefault(); }); this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"), event => { this.selectPreviousTool(); event.preventDefault(); }); this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"), event => { this.switchToPreviousHost(); event.preventDefault(); }); this.doc.addEventListener("keypress", this._splitConsoleOnKeypress); this.doc.addEventListener("focus", this._onFocus, true); this.win.addEventListener("unload", this.destroy); this.win.addEventListener("message", this._onBrowserMessage, true); }, _removeHostListeners: function() { // The host iframe's contentDocument may already be gone. if (this.doc) { this.doc.removeEventListener("keypress", this._splitConsoleOnKeypress); this.doc.removeEventListener("focus", this._onFocus, true); this.win.removeEventListener("unload", this.destroy); this.win.removeEventListener("message", this._onBrowserMessage, true); } }, // Called whenever the chrome send a message _onBrowserMessage: function(event) { if (event.data && event.data.name === "switched-host") { this._onSwitchedHost(event.data); } }, _registerOverlays: function() { registerHarOverlay(this); }, _saveSplitConsoleHeight: function() { Services.prefs.setIntPref(SPLITCONSOLE_HEIGHT_PREF, this.webconsolePanel.height); }, /** * Make sure that the console is showing up properly based on all the * possible conditions. * 1) If the console tab is selected, then regardless of split state * it should take up the full height of the deck, and we should * hide the deck and splitter. * 2) If the console tab is not selected and it is split, then we should * show the splitter, deck, and console. * 3) If the console tab is not selected and it is *not* split, * then we should hide the console and splitter, and show the deck * at full height. */ _refreshConsoleDisplay: function() { const deck = this.doc.getElementById("toolbox-deck"); const webconsolePanel = this.webconsolePanel; const splitter = this.doc.getElementById("toolbox-console-splitter"); const openedConsolePanel = this.currentToolId === "webconsole"; if (openedConsolePanel) { deck.setAttribute("collapsed", "true"); splitter.setAttribute("hidden", "true"); webconsolePanel.removeAttribute("collapsed"); } else { deck.removeAttribute("collapsed"); if (this.splitConsole) { webconsolePanel.removeAttribute("collapsed"); splitter.removeAttribute("hidden"); } else { webconsolePanel.setAttribute("collapsed", "true"); splitter.setAttribute("hidden", "true"); } } }, /** * Adds the keys and commands to the Toolbox Window in window mode. */ _addKeysToWindow: function() { if (this.hostType != Toolbox.HostType.WINDOW) { return; } const doc = this.win.parent.document; for (const item of Startup.KeyShortcuts) { // KeyShortcuts contain tool-specific and global key shortcuts, // here we only need to copy shortcut specific to each tool. if (!item.toolId) { continue; } const { toolId, shortcut, modifiers } = item; const key = doc.createElement("key"); key.id = "key_" + toolId; if (shortcut.startsWith("VK_")) { key.setAttribute("keycode", shortcut); } else { key.setAttribute("key", shortcut); } key.setAttribute("modifiers", modifiers); // needed. See bug 371900 key.setAttribute("oncommand", "void(0);"); key.addEventListener("command", () => { this.selectTool(toolId, "key_shortcut").then(() => this.fireCustomKey(toolId)); }, true); doc.getElementById("toolbox-keyset").appendChild(key); } // Add key for toggling the browser console from the detached window if (!doc.getElementById("key_browserconsole")) { const key = doc.createElement("key"); key.id = "key_browserconsole"; key.setAttribute("key", L10N.getStr("browserConsoleCmd.commandkey")); key.setAttribute("modifiers", "accel,shift"); // needed. See bug 371900 key.setAttribute("oncommand", "void(0)"); key.addEventListener("command", () => { HUDService.toggleBrowserConsole(); }, true); doc.getElementById("toolbox-keyset").appendChild(key); } }, /** * Handle any custom key events. Returns true if there was a custom key * binding run. * @param {string} toolId Which tool to run the command on (skip if not * current) */ fireCustomKey: function(toolId) { const toolDefinition = gDevTools.getToolDefinition(toolId); if (toolDefinition.onkey && ((this.currentToolId === toolId) || (toolId == "webconsole" && this.splitConsole))) { toolDefinition.onkey(this.getCurrentPanel(), this); } }, /** * Build the notification box as soon as needed. */ get notificationBox() { if (!this._notificationBox) { let { NotificationBox, PriorityLevels } = this.browserRequire("devtools/client/shared/components/NotificationBox"); NotificationBox = this.React.createFactory(NotificationBox); // Render NotificationBox and assign priority levels to it. const box = this.doc.getElementById("toolbox-notificationbox"); this._notificationBox = Object.assign( this.ReactDOM.render(NotificationBox({}), box), PriorityLevels); } return this._notificationBox; }, /** * Build the options for changing hosts. Called every time * the host changes. */ _buildDockOptions: function() { if (!this._target.isLocalTab) { this.component.setDockOptionsEnabled(false); this.component.setCanCloseToolbox(false); return; } this.component.setDockOptionsEnabled(true); this.component.setCanCloseToolbox(this.hostType !== Toolbox.HostType.WINDOW); const sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED); const hostTypes = []; for (const type in Toolbox.HostType) { const position = Toolbox.HostType[type]; if (position == Toolbox.HostType.CUSTOM || (!sideEnabled && (position == Toolbox.HostType.LEFT || position == Toolbox.HostType.RIGHT))) { continue; } hostTypes.push({ position, switchHost: this.switchHost.bind(this, position) }); } this.component.setCurrentHostType(this.hostType); this.component.setHostTypes(hostTypes); }, postMessage: function(msg) { // We sometime try to send messages in middle of destroy(), where the // toolbox iframe may already be detached and no longer have a parent. if (this.win.parent) { // Toolbox document is still chrome and disallow identifying message // origin via event.source as it is null. So use a custom id. msg.frameId = this.frameId; this.win.parent.postMessage(msg, "*"); } }, /** * Initiate ToolboxTabs React component and all it's properties. Do the initial render. */ _buildTabs: async function() { // Get the initial list of tab definitions. This list can be amended at a later time // by tools registering themselves. const definitions = gDevTools.getToolDefinitionArray(); definitions.forEach(definition => this._buildPanelForTool(definition)); // Get the definitions that will only affect the main tab area. this.panelDefinitions = definitions.filter(definition => definition.isTargetSupported(this._target) && definition.id !== "options"); // Do async lookup of disable pop-up auto-hide state. if (this.disableAutohideAvailable) { const disable = await this._isDisableAutohideEnabled(); this.component.setDisableAutohide(disable); } }, _mountReactComponent: function() { // Ensure the toolbar doesn't try to render until the tool is ready. const element = this.React.createElement(this.ToolboxController, { L10N, currentToolId: this.currentToolId, selectTool: this.selectTool, toggleOptions: this.toggleOptions, toggleSplitConsole: this.toggleSplitConsole, toggleNoAutohide: this.toggleNoAutohide, closeToolbox: this.destroy, focusButton: this._onToolbarFocus, toolbox: this, onTabsOrderUpdated: this._onTabsOrderUpdated, }); this.component = this.ReactDOM.render(element, this._componentMount); }, /** * Reset tabindex attributes across all focusable elements inside the toolbar. * Only have one element with tabindex=0 at a time to make sure that tabbing * results in navigating away from the toolbar container. * @param {FocusEvent} event */ _onToolbarFocus: function(id) { this.component.setFocusedButton(id); }, /** * On left/right arrow press, attempt to move the focus inside the toolbar to * the previous/next focusable element. This is not in the React component * as it is difficult to coordinate between different component elements. * The components are responsible for setting the correct tabindex value * for if they are the focused element. * @param {KeyboardEvent} event */ _onToolbarArrowKeypress: function(event) { const { key, target, ctrlKey, shiftKey, altKey, metaKey } = event; // If any of the modifier keys are pressed do not attempt navigation as it // might conflict with global shortcuts (Bug 1327972). if (ctrlKey || shiftKey || altKey || metaKey) { return; } const buttons = [...this._componentMount.querySelectorAll("button")]; const curIndex = buttons.indexOf(target); if (curIndex === -1) { console.warn(target + " is not found among Developer Tools tab bar " + "focusable elements."); return; } let newTarget; if (key === "ArrowLeft") { // Do nothing if already at the beginning. if (curIndex === 0) { return; } newTarget = buttons[curIndex - 1]; } else if (key === "ArrowRight") { // Do nothing if already at the end. if (curIndex === buttons.length - 1) { return; } newTarget = buttons[curIndex + 1]; } else { return; } newTarget.focus(); event.preventDefault(); event.stopPropagation(); }, /** * Add buttons to the UI as specified in devtools/client/definitions.js */ _buildButtons() { // Beyond the normal preference filtering this.toolbarButtons = [ this._buildPickerButton(), this._buildFrameButton(), ]; ToolboxButtons.forEach(definition => { const button = this._createButtonState(definition); this.toolbarButtons.push(button); }); this.component.setToolboxButtons(this.toolbarButtons); }, /** * Button to select a frame for the inspector to target. */ _buildFrameButton() { this.frameButton = this._createButtonState({ id: "command-button-frames", description: L10N.getStr("toolbox.frames.tooltip"), onClick: this.showFramesMenu, isTargetSupported: target => { return target.activeTab && target.activeTab.traits.frames; }, isCurrentlyVisible: () => { const hasFrames = this.frameMap.size > 1; const isOnOptionsPanel = this.currentToolId === "options"; return hasFrames || isOnOptionsPanel; }, onKeyDown: this.handleKeyDownOnFramesButton }); return this.frameButton; }, /** * Toggle the picker, but also decide whether or not the highlighter should * focus the window. This is only desirable when the toolbox is mounted to the * window. When devtools is free floating, then the target window should not * pop in front of the viewer when the picker is clicked. * * Note: Toggle picker can be overwritten by panel other than the inspector to * allow for custom picker behaviour. */ _onPickerClick: function() { const focus = this.hostType === Toolbox.HostType.BOTTOM || this.hostType === Toolbox.HostType.LEFT || this.hostType === Toolbox.HostType.RIGHT; const currentPanel = this.getCurrentPanel(); if (currentPanel.togglePicker) { currentPanel.togglePicker(focus); } else { this.highlighterUtils.togglePicker(focus); } }, /** * If the picker is activated, then allow the Escape key to deactivate the * functionality instead of the default behavior of toggling the console. */ _onPickerKeypress: function(event) { if (event.keyCode === KeyCodes.DOM_VK_ESCAPE) { const currentPanel = this.getCurrentPanel(); if (currentPanel.cancelPicker) { currentPanel.cancelPicker(); } else { this.highlighterUtils.cancelPicker(); } // Stop the console from toggling. event.stopImmediatePropagation(); } }, _onPickerStarted: function() { this.doc.addEventListener("keypress", this._onPickerKeypress, true); }, _onPickerStopped: function() { this.doc.removeEventListener("keypress", this._onPickerKeypress, true); }, /** * The element picker button enables the ability to select a DOM node by clicking * it on the page. */ _buildPickerButton() { this.pickerButton = this._createButtonState({ id: "command-button-pick", description: L10N.getStr("pickButton.tooltip"), onClick: this._onPickerClick, isInStartContainer: true, isTargetSupported: target => { return target.activeTab && target.activeTab.traits.frames; } }); return this.pickerButton; }, /** * Apply the current cache setting from devtools.cache.disabled to this * toolbox's tab. */ _applyCacheSettings: function() { const pref = "devtools.cache.disabled"; const cacheDisabled = Services.prefs.getBoolPref(pref); if (this.target.activeTab) { this.target.activeTab.reconfigure({"cacheDisabled": cacheDisabled}); } }, /** * Apply the current service workers testing setting from * devtools.serviceWorkers.testing.enabled to this toolbox's tab. */ _applyServiceWorkersTestingSettings: function() { const pref = "devtools.serviceWorkers.testing.enabled"; const serviceWorkersTestingEnabled = Services.prefs.getBoolPref(pref) || false; if (this.target.activeTab) { this.target.activeTab.reconfigure({ "serviceWorkersTestingEnabled": serviceWorkersTestingEnabled }); } }, /** * Update the visibility of the buttons. */ updateToolboxButtonsVisibility() { this.toolbarButtons.forEach(button => { button.isVisible = this._commandIsVisible(button); }); this.component.setToolboxButtons(this.toolbarButtons); }, /** * Visually update picker button. * This function is called on every "select" event. Newly selected panel can * update the visual state of the picker button such as disabled state, * additional CSS classes (className), and tooltip (description). */ updatePickerButton() { const button = this.pickerButton; const currentPanel = this.getCurrentPanel(); if (currentPanel && currentPanel.updatePickerButton) { currentPanel.updatePickerButton(); } else { // If the current panel doesn't define a custom updatePickerButton, // revert the button to its default state button.description = L10N.getStr("pickButton.tooltip"); button.className = null; button.disabled = null; } }, /** * Update the visual state of the Frame picker button. */ updateFrameButton() { if (this.currentToolId === "options" && this.frameMap.size <= 1) { // If the button is only visible because the user is on the Options panel, disable // the button and set an appropriate description. this.frameButton.disabled = true; this.frameButton.description = L10N.getStr("toolbox.frames.disabled.tooltip"); } else { // Otherwise, enable the button and update the description. this.frameButton.disabled = false; this.frameButton.description = L10N.getStr("toolbox.frames.tooltip"); } this.frameButton.isVisible = this._commandIsVisible(this.frameButton); }, /** * Ensure the visibility of each toolbox button matches the preference value. */ _commandIsVisible: function(button) { const { isTargetSupported, isCurrentlyVisible, visibilityswitch } = button; if (!Services.prefs.getBoolPref(visibilityswitch, true)) { return false; } if (isTargetSupported && !isTargetSupported(this.target)) { return false; } if (isCurrentlyVisible && !isCurrentlyVisible()) { return false; } return true; }, /** * Build a panel for a tool definition. * * @param {string} toolDefinition * Tool definition of the tool to build a tab for. */ _buildPanelForTool: function(toolDefinition) { if (!toolDefinition.isTargetSupported(this._target)) { return; } const deck = this.doc.getElementById("toolbox-deck"); const id = toolDefinition.id; if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) { toolDefinition.ordinal = MAX_ORDINAL; } if (!toolDefinition.bgTheme) { toolDefinition.bgTheme = "theme-toolbar"; } const panel = this.doc.createElement("vbox"); panel.className = "toolbox-panel " + toolDefinition.bgTheme; // There is already a container for the webconsole frame. if (!this.doc.getElementById("toolbox-panel-" + id)) { panel.id = "toolbox-panel-" + id; } deck.appendChild(panel); if (toolDefinition.buildToolStartup && !this._toolStartups.has(id)) { this._toolStartups.set(id, toolDefinition.buildToolStartup(this)); } this._addKeysToWindow(); }, /** * Lazily created map of the additional tools registered to this toolbox. * * @returns {Map} * a map of the tools definitions registered to this * particular toolbox (the key is the toolId string, the value * is the tool definition plain javascript object). */ get additionalToolDefinitions() { if (!this._additionalToolDefinitions) { this._additionalToolDefinitions = new Map(); } return this._additionalToolDefinitions; }, /** * Retrieve the array of the additional tools registered to this toolbox. * * @return {Array} * the array of additional tool definitions registered on this toolbox. */ getAdditionalTools() { if (this._additionalToolDefinitions) { return Array.from(this.additionalToolDefinitions.values()); } return []; }, /** * Get the additional tools that have been registered and are visible. * * @return {Array} * the array of additional tool definitions registered on this toolbox. */ getVisibleAdditionalTools() { return this.visibleAdditionalTools .map(toolId => this.additionalToolDefinitions.get(toolId)); }, /** * Test the existence of a additional tools registered to this toolbox by tool id. * * @param {string} toolId * the id of the tool to test for existence. * * @return {boolean} * */ hasAdditionalTool(toolId) { return this.additionalToolDefinitions.has(toolId); }, /** * Register and load an additional tool on this particular toolbox. * * @param {object} definition * the additional tool definition to register and add to this toolbox. */ addAdditionalTool(definition) { if (!definition.id) { throw new Error("Tool definition id is missing"); } if (this.isToolRegistered(definition.id)) { throw new Error("Tool definition already registered: " + definition.id); } this.additionalToolDefinitions.set(definition.id, definition); this.visibleAdditionalTools = [...this.visibleAdditionalTools, definition.id]; const buildPanel = () => this._buildPanelForTool(definition); if (this.isReady) { buildPanel(); } else { this.once("ready", buildPanel); } }, /** * Retrieve the registered inspector extension sidebars * (used by the inspector panel during its deferred initialization). */ get inspectorExtensionSidebars() { return this._inspectorExtensionSidebars; }, /** * Register an extension sidebar for the inspector panel. * * @param {String} id * An unique sidebar id * @param {Object} options * @param {String} options.title * A title for the sidebar */ async registerInspectorExtensionSidebar(id, options) { this._inspectorExtensionSidebars.set(id, options); // Defer the extension sidebar creation if the inspector // has not been created yet (and do not create the inspector // only to register an extension sidebar). if (!this._inspector) { return; } const inspector = this.getPanel("inspector"); inspector.addExtensionSidebar(id, options); }, /** * Unregister an extension sidebar for the inspector panel. * * @param {String} id * An unique sidebar id */ unregisterInspectorExtensionSidebar(id) { const sidebarDef = this._inspectorExtensionSidebars.get(id); if (!sidebarDef) { return; } this._inspectorExtensionSidebars.delete(id); // Remove the created sidebar instance if the inspector panel // has been already created. if (!this._inspector) { return; } const inspector = this.getPanel("inspector"); inspector.removeExtensionSidebar(id); }, /** * Unregister and unload an additional tool from this particular toolbox. * * @param {string} toolId * the id of the additional tool to unregister and remove. */ removeAdditionalTool(toolId) { // Early exit if the toolbox is already destroying itself. if (this._destroyer) { return; } if (!this.hasAdditionalTool(toolId)) { throw new Error("Tool definition not registered to this toolbox: " + toolId); } this.additionalToolDefinitions.delete(toolId); this.visibleAdditionalTools = this.visibleAdditionalTools .filter(id => id !== toolId); this.unloadTool(toolId); }, /** * Ensure the tool with the given id is loaded. * * @param {string} id * The id of the tool to load. */ loadTool: function(id) { if (id === "inspector" && !this._inspector) { return this.initInspector().then(() => this.loadTool(id)); } let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); if (iframe) { const panel = this._toolPanels.get(id); return new Promise(resolve => { if (panel) { resolve(panel); } else { this.once(id + "-ready", initializedPanel => { resolve(initializedPanel); }); } }); } return new Promise((resolve, reject) => { // Retrieve the tool definition (from the global or the per-toolbox tool maps) const definition = this.getToolDefinition(id); if (!definition) { reject(new Error("no such tool id " + id)); return; } iframe = this.doc.createElement("iframe"); iframe.className = "toolbox-panel-iframe"; iframe.id = "toolbox-panel-iframe-" + id; iframe.setAttribute("flex", 1); iframe.setAttribute("forceOwnRefreshDriver", ""); iframe.tooltip = "aHTMLTooltip"; iframe.style.visibility = "hidden"; gDevTools.emit(id + "-init", this, iframe); this.emit(id + "-init", iframe); // If no parent yet, append the frame into default location. if (!iframe.parentNode) { const vbox = this.doc.getElementById("toolbox-panel-" + id); vbox.appendChild(iframe); vbox.visibility = "visible"; } const onLoad = () => { // Prevent flicker while loading by waiting to make visible until now. iframe.style.visibility = "visible"; // Try to set the dir attribute as early as possible. this.setIframeDocumentDir(iframe); // The build method should return a panel instance, so events can // be fired with the panel as an argument. However, in order to keep // backward compatibility with existing extensions do a check // for a promise return value. let built = definition.build(iframe.contentWindow, this); if (!(typeof built.then == "function")) { const panel = built; iframe.panel = panel; // The panel instance is expected to fire (and listen to) various // framework events, so make sure it's properly decorated with // appropriate API (on, off, once, emit). // In this case we decorate panel instances directly returned by // the tool definition 'build' method. if (typeof panel.emit == "undefined") { EventEmitter.decorate(panel); } gDevTools.emit(id + "-build", this, panel); this.emit(id + "-build", panel); // The panel can implement an 'open' method for asynchronous // initialization sequence. if (typeof panel.open == "function") { built = panel.open(); } else { built = new Promise(resolve => { resolve(panel); }); } } // Wait till the panel is fully ready and fire 'ready' events. promise.resolve(built).then((panel) => { this._toolPanels.set(id, panel); // Make sure to decorate panel object with event API also in case // where the tool definition 'build' method returns only a promise // and the actual panel instance is available as soon as the // promise is resolved. if (typeof panel.emit == "undefined") { EventEmitter.decorate(panel); } gDevTools.emit(id + "-ready", this, panel); this.emit(id + "-ready", panel); resolve(panel); }, console.error); }; iframe.setAttribute("src", definition.url); if (definition.panelLabel) { iframe.setAttribute("aria-label", definition.panelLabel); } // Depending on the host, iframe.contentWindow is not always // defined at this moment. If it is not defined, we use an // event listener on the iframe DOM node. If it's defined, // we use the chromeEventHandler. We can't use a listener // on the DOM node every time because this won't work // if the (xul chrome) iframe is loaded in a content docshell. if (iframe.contentWindow) { const domHelper = new DOMHelpers(iframe.contentWindow); domHelper.onceDOMReady(onLoad); } else { const callback = () => { iframe.removeEventListener("DOMContentLoaded", callback); onLoad(); }; iframe.addEventListener("DOMContentLoaded", callback); } }); }, /** * Set the dir attribute on the content document element of the provided iframe. * * @param {IFrameElement} iframe */ setIframeDocumentDir: function(iframe) { const docEl = iframe.contentWindow && iframe.contentWindow.document.documentElement; if (!docEl || docEl.namespaceURI !== HTML_NS) { // Bail out if the content window or document is not ready or if the document is not // HTML. return; } if (docEl.hasAttribute("dir")) { // Set the dir attribute value only if dir is already present on the document. docEl.setAttribute("dir", this.direction); } }, /** * Mark all in collection as unselected; and id as selected * @param {string} collection * DOM collection of items * @param {string} id * The Id of the item within the collection to select */ selectSingleNode: function(collection, id) { [...collection].forEach(node => { if (node.id === id) { node.setAttribute("selected", "true"); node.setAttribute("aria-selected", "true"); } else { node.removeAttribute("selected"); node.removeAttribute("aria-selected"); } // The webconsole panel is in a special location due to split console if (!node.id) { node = this.webconsolePanel; } const iframe = node.querySelector(".toolbox-panel-iframe"); if (iframe) { let visible = node.id == id; // Prevents hiding the split-console if it is currently enabled if (node == this.webconsolePanel && this.splitConsole) { visible = true; } this.setIframeVisible(iframe, visible); } }); }, /** * Make a privileged iframe visible/hidden. * * For now, XUL Iframes loading chrome documents (i.e.