summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/toolbox.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework/toolbox.js')
-rw-r--r--devtools/client/framework/toolbox.js2417
1 files changed, 2417 insertions, 0 deletions
diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js
new file mode 100644
index 000000000..82d5d2915
--- /dev/null
+++ b/devtools/client/framework/toolbox.js
@@ -0,0 +1,2417 @@
+/* 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 MAX_ORDINAL = 99;
+const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
+const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
+const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER";
+const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER";
+const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST";
+const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const { SourceMapService } = require("./source-map-service");
+
+var {Ci, Cu} = require("chrome");
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var Services = require("Services");
+var {Task} = require("devtools/shared/task");
+var {gDevTools} = require("devtools/client/framework/devtools");
+var EventEmitter = require("devtools/shared/event-emitter");
+var Telemetry = require("devtools/client/shared/telemetry");
+var HUDService = require("devtools/client/webconsole/hudservice");
+var viewSource = require("devtools/client/shared/view-source");
+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");
+
+const { BrowserLoader } =
+ Cu.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, "CommandUtils",
+ "devtools/client/shared/developer-toolbar", true);
+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, "showDoorhanger",
+ "devtools/client/shared/doorhanger", true);
+loader.lazyRequireGetter(this, "createPerformanceFront",
+ "devtools/shared/fronts/performance", true);
+loader.lazyRequireGetter(this, "system",
+ "devtools/shared/system");
+loader.lazyRequireGetter(this, "getPreferenceFront",
+ "devtools/shared/fronts/preference", true);
+loader.lazyRequireGetter(this, "KeyShortcuts",
+ "devtools/client/shared/key-shortcuts", true);
+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.lazyGetter(this, "registerHarOverlay", () => {
+ return require("devtools/client/netmonitor/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
+ */
+function Toolbox(target, selectedTool, hostType, contentWindow, frameId) {
+ this._target = target;
+ this._win = contentWindow;
+ this.frameId = frameId;
+
+ this._toolPanels = new Map();
+ this._telemetry = new Telemetry();
+ if (Services.prefs.getBoolPref("devtools.sourcemap.locations.enabled")) {
+ this._sourceMapService = new SourceMapService(this._target);
+ }
+
+ this._initInspector = null;
+ this._inspector = 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._refreshHostTitle = this._refreshHostTitle.bind(this);
+ this._toggleAutohide = this._toggleAutohide.bind(this);
+ this.showFramesMenu = this.showFramesMenu.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._prefChanged = this._prefChanged.bind(this);
+ this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+ this._onBrowserMessage = this._onBrowserMessage.bind(this);
+ this._showDevEditionPromo = this._showDevEditionPromo.bind(this);
+ this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this);
+ this._onBottomHostMinimized = this._onBottomHostMinimized.bind(this);
+ this._onBottomHostMaximized = this._onBottomHostMaximized.bind(this);
+ this._onToolSelectWhileMinimized = this._onToolSelectWhileMinimized.bind(this);
+ this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
+ this._onBottomHostWillChange = this._onBottomHostWillChange.bind(this);
+ this._toggleMinimizeMode = this._toggleMinimizeMode.bind(this);
+ this._onTabbarFocus = this._onTabbarFocus.bind(this);
+ this._onTabbarArrowKeypress = this._onTabbarArrowKeypress.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._target.on("close", this.destroy);
+
+ if (!selectedTool) {
+ selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
+ }
+ this._defaultToolId = selectedTool;
+
+ this._hostType = hostType;
+
+ EventEmitter.decorate(this);
+
+ this._target.on("navigate", this._refreshHostTitle);
+ this._target.on("frame-update", this._updateFrames);
+
+ this.on("host-changed", this._refreshHostTitle);
+ this.on("select", this._refreshHostTitle);
+
+ this.on("ready", this._showDevEditionPromo);
+
+ gDevTools.on("tool-registered", this._toolRegistered);
+ gDevTools.on("tool-unregistered", this._toolUnregistered);
+
+ this.on("picker-started", this._onPickerStarted);
+ this.on("picker-stopped", this._onPickerStopped);
+}
+exports.Toolbox = Toolbox;
+
+/**
+ * The toolbox can be 'hosted' either embedded in a browser window
+ * or in a separate window.
+ */
+Toolbox.HostType = {
+ BOTTOM: "bottom",
+ SIDE: "side",
+ WINDOW: "window",
+ CUSTOM: "custom"
+};
+
+Toolbox.prototype = {
+ _URL: "about:devtools-toolbox",
+
+ _prefs: {
+ LAST_TOOL: "devtools.toolbox.selectedTool",
+ SIDE_ENABLED: "devtools.toolbox.sideEnabled",
+ },
+
+ currentToolId: null,
+ 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) {
+ let deferred = defer();
+ let panel = this.getPanel(id);
+ if (panel) {
+ deferred.resolve(panel);
+ } else {
+ this.on(id + "-ready", (e, initializedPanel) => {
+ deferred.resolve(initializedPanel);
+ });
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * 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;
+ }
+ let focusedWin = Services.focus.focusedWindow;
+ return focusedWin && focusedWin ===
+ this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow;
+ },
+
+ /**
+ * Open the toolbox
+ */
+ open: function () {
+ return Task.spawn(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;
+ }
+
+ let domReady = defer();
+ let domHelper = new DOMHelpers(this.win);
+ domHelper.onceDOMReady(() => {
+ domReady.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
+ yield this._target.makeRemote();
+
+ // Attach the thread
+ this._threadClient = yield attachThread(this);
+ yield domReady.promise;
+
+ this.isReady = true;
+ let framesPromise = this._listFrames();
+
+ this.closeButton = this.doc.getElementById("toolbox-close");
+ this.closeButton.addEventListener("click", this.destroy, true);
+
+ gDevTools.on("pref-changed", this._prefChanged);
+
+ let framesMenu = this.doc.getElementById("command-button-frames");
+ framesMenu.addEventListener("click", this.showFramesMenu, false);
+
+ let noautohideMenu = this.doc.getElementById("command-button-noautohide");
+ noautohideMenu.addEventListener("click", this._toggleAutohide, true);
+
+ this.textBoxContextMenuPopup =
+ this.doc.getElementById("toolbox-textbox-context-popup");
+ this.textBoxContextMenuPopup.addEventListener("popupshowing",
+ this._updateTextBoxMenuItems, true);
+
+ this.shortcuts = new KeyShortcuts({
+ window: this.doc.defaultView
+ });
+ this._buildDockButtons();
+ 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.tabbar = this.doc.querySelector(".devtools-tabbar");
+ this.tabbar.addEventListener("focus", this._onTabbarFocus, true);
+ this.tabbar.addEventListener("click", this._onTabbarFocus, true);
+ this.tabbar.addEventListener("keypress", this._onTabbarArrowKeypress);
+
+ this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
+ this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
+ this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight);
+
+ let buttonsPromise = 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).
+ let toolDef = gDevTools.getToolDefinition(this._defaultToolId);
+ if (!toolDef || !toolDef.isTargetSupported(this._target)) {
+ this._defaultToolId = "webconsole";
+ }
+
+ yield this.selectTool(this._defaultToolId);
+
+ // 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();
+ }
+
+ yield promise.all([
+ splitConsolePromise,
+ buttonsPromise,
+ 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.
+ let 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) {
+ yield performanceFrontConnection;
+ }
+
+ this.emit("ready");
+ }.bind(this)).then(null, console.error.bind(console));
+ },
+
+ /**
+ * 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");
+ },
+
+ // Return HostType id for telemetry
+ _getTelemetryHostId: function () {
+ switch (this.hostType) {
+ case Toolbox.HostType.BOTTOM: return 0;
+ case Toolbox.HostType.SIDE: return 1;
+ case Toolbox.HostType.WINDOW: return 2;
+ case Toolbox.HostType.CUSTOM: return 3;
+ default: return 9;
+ }
+ },
+
+ _pingTelemetry: function () {
+ this._telemetry.toolOpened("toolbox");
+
+ this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
+ this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS,
+ Services.appinfo.is64Bit ? 1 : 0);
+ this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM,
+ system.getScreenDimensions());
+ this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
+ },
+
+ /**
+ * Because our panels are lazy loaded this is a good place to watch for
+ * "pref-changed" events.
+ * @param {String} event
+ * The event type, "pref-changed".
+ * @param {Object} data
+ * {
+ * newValue: The new value
+ * oldValue: The old value
+ * pref: The name of the preference that has changed
+ * }
+ */
+ _prefChanged: function (event, data) {
+ switch (data.pref) {
+ case "devtools.cache.disabled":
+ this._applyCacheSettings();
+ break;
+ case "devtools.serviceWorkers.testing.enabled":
+ this._applyServiceWorkersTestingSettings();
+ break;
+ }
+ },
+
+ _buildOptions: function () {
+ let selectOptions = (name, event) => {
+ // Flip back to the last used panel if we are already
+ // on the options panel.
+ if (this.currentToolId === "options" &&
+ gDevTools.getToolDefinition(this.lastUsedToolId)) {
+ this.selectTool(this.lastUsedToolId);
+ } else {
+ this.selectTool("options");
+ }
+ // Prevent the opening of bookmarks window on toolbox.options.key
+ event.preventDefault();
+ };
+ this.shortcuts.on(L10N.getStr("toolbox.options.key"), selectOptions);
+ this.shortcuts.on(L10N.getStr("toolbox.help.key"), selectOptions);
+ },
+
+ _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, (name, event) => {
+ if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) {
+ handler();
+ event.preventDefault();
+ }
+ });
+ },
+
+ _addReloadKeys: function () {
+ [
+ ["reload", false],
+ ["reload2", false],
+ ["forceReload", true],
+ ["forceReload2", true]
+ ].forEach(([id, force]) => {
+ let key = L10N.getStr("toolbox." + id + ".key");
+ this.shortcuts.on(key, (name, event) => {
+ this.reloadTarget(force);
+
+ // Prevent Firefox shortcuts from reloading the page
+ event.preventDefault();
+ });
+ });
+ },
+
+ _addHostListeners: function () {
+ this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"),
+ (name, event) => {
+ this.selectNextTool();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"),
+ (name, event) => {
+ this.selectPreviousTool();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.minimize.key"),
+ (name, event) => {
+ this._toggleMinimizeMode();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"),
+ (name, event) => {
+ this.switchToPreviousHost();
+ event.preventDefault();
+ });
+
+ this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false);
+ 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, false);
+ 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) {
+ return;
+ }
+ switch (event.data.name) {
+ case "switched-host":
+ this._onSwitchedHost(event.data);
+ break;
+ case "host-minimized":
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ this._onBottomHostMinimized();
+ }
+ break;
+ case "host-maximized":
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ this._onBottomHostMaximized();
+ }
+ break;
+ }
+ },
+
+ _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 () {
+ let deck = this.doc.getElementById("toolbox-deck");
+ let webconsolePanel = this.webconsolePanel;
+ let splitter = this.doc.getElementById("toolbox-console-splitter");
+ let 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;
+ }
+
+ let doc = this.win.parent.document;
+
+ for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) {
+ // Prevent multiple entries for the same tool.
+ if (!toolDefinition.key || doc.getElementById("key_" + id)) {
+ continue;
+ }
+
+ let toolId = id;
+ let key = doc.createElement("key");
+
+ key.id = "key_" + toolId;
+
+ if (toolDefinition.key.startsWith("VK_")) {
+ key.setAttribute("keycode", toolDefinition.key);
+ } else {
+ key.setAttribute("key", toolDefinition.key);
+ }
+
+ key.setAttribute("modifiers", toolDefinition.modifiers);
+ // needed. See bug 371900
+ key.setAttribute("oncommand", "void(0);");
+ key.addEventListener("command", () => {
+ this.selectTool(toolId).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")) {
+ let 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) {
+ let 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/notification-box");
+
+ NotificationBox = this.React.createFactory(NotificationBox);
+
+ // Render NotificationBox and assign priority levels to it.
+ let box = this.doc.getElementById("toolbox-notificationbox");
+ this._notificationBox = Object.assign(
+ this.ReactDOM.render(NotificationBox({}), box),
+ PriorityLevels);
+ }
+ return this._notificationBox;
+ },
+
+ /**
+ * Build the buttons for changing hosts. Called every time
+ * the host changes.
+ */
+ _buildDockButtons: function () {
+ let dockBox = this.doc.getElementById("toolbox-dock-buttons");
+
+ while (dockBox.firstChild) {
+ dockBox.removeChild(dockBox.firstChild);
+ }
+
+ if (!this._target.isLocalTab) {
+ return;
+ }
+
+ // Bottom-type host can be minimized, add a button for this.
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ let minimizeBtn = this.doc.createElementNS(HTML_NS, "button");
+ minimizeBtn.id = "toolbox-dock-bottom-minimize";
+ minimizeBtn.className = "devtools-button";
+ /* Bug 1177463 - The minimize button is currently hidden until we agree on
+ the UI for it, and until bug 1173849 is fixed too. */
+ minimizeBtn.setAttribute("hidden", "true");
+
+ minimizeBtn.addEventListener("click", this._toggleMinimizeMode);
+ dockBox.appendChild(minimizeBtn);
+ // Show the button in its maximized state.
+ this._onBottomHostMaximized();
+
+ // Maximize again when a tool gets selected.
+ this.on("before-select", this._onToolSelectWhileMinimized);
+ // Maximize and stop listening before the host type changes.
+ this.once("host-will-change", this._onBottomHostWillChange);
+ }
+
+ if (this.hostType == Toolbox.HostType.WINDOW) {
+ this.closeButton.setAttribute("hidden", "true");
+ } else {
+ this.closeButton.removeAttribute("hidden");
+ }
+
+ let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
+
+ for (let type in Toolbox.HostType) {
+ let position = Toolbox.HostType[type];
+ if (position == this.hostType ||
+ position == Toolbox.HostType.CUSTOM ||
+ (!sideEnabled && position == Toolbox.HostType.SIDE)) {
+ continue;
+ }
+
+ let button = this.doc.createElementNS(HTML_NS, "button");
+ button.id = "toolbox-dock-" + position;
+ button.className = "toolbox-dock-button devtools-button";
+ button.setAttribute("title", L10N.getStr("toolboxDockButtons." +
+ position + ".tooltip"));
+ button.addEventListener("click", this.switchHost.bind(this, position));
+
+ dockBox.appendChild(button);
+ }
+ },
+
+ _getMinimizeButtonShortcutTooltip: function () {
+ let str = L10N.getStr("toolbox.minimize.key");
+ let key = KeyShortcuts.parseElectronKey(this.win, str);
+ return "(" + KeyShortcuts.stringify(key) + ")";
+ },
+
+ _onBottomHostMinimized: function () {
+ let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
+ btn.className = "minimized";
+
+ btn.setAttribute("title",
+ L10N.getStr("toolboxDockButtons.bottom.maximize") + " " +
+ this._getMinimizeButtonShortcutTooltip());
+ },
+
+ _onBottomHostMaximized: function () {
+ let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
+ btn.className = "maximized";
+
+ btn.setAttribute("title",
+ L10N.getStr("toolboxDockButtons.bottom.minimize") + " " +
+ this._getMinimizeButtonShortcutTooltip());
+ },
+
+ _onToolSelectWhileMinimized: function () {
+ this.postMessage({
+ name: "maximize-host"
+ });
+ },
+
+ 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, "*");
+ }
+ },
+
+ _onBottomHostWillChange: function () {
+ this.postMessage({
+ name: "maximize-host"
+ });
+
+ this.off("before-select", this._onToolSelectWhileMinimized);
+ },
+
+ _toggleMinimizeMode: function () {
+ if (this.hostType !== Toolbox.HostType.BOTTOM) {
+ return;
+ }
+
+ // Calculate the height to which the host should be minimized so the
+ // tabbar is still visible.
+ let toolbarHeight = this.tabbar.getBoxQuads({box: "content"})[0].bounds
+ .height;
+ this.postMessage({
+ name: "toggle-minimize-mode",
+ toolbarHeight
+ });
+ },
+
+ /**
+ * Add tabs to the toolbox UI for registered tools
+ */
+ _buildTabs: function () {
+ for (let definition of gDevTools.getToolDefinitionArray()) {
+ this._buildTabForTool(definition);
+ }
+ },
+
+ /**
+ * Get all dev tools tab bar focusable elements. These are visible elements
+ * such as buttons or elements with tabindex.
+ */
+ get tabbarFocusableElms() {
+ return [...this.tabbar.querySelectorAll(
+ "[tabindex]:not([hidden]), button:not([hidden])")];
+ },
+
+ /**
+ * Reset tabindex attributes across all focusable elements inside the tabbar.
+ * Only have one element with tabindex=0 at a time to make sure that tabbing
+ * results in navigating away from the tabbar container.
+ * @param {FocusEvent} event
+ */
+ _onTabbarFocus: function (event) {
+ this.tabbarFocusableElms.forEach(elm =>
+ elm.setAttribute("tabindex", event.target === elm ? "0" : "-1"));
+ },
+
+ /**
+ * On left/right arrow press, attempt to move the focus inside the tabbar to
+ * the previous/next focusable element.
+ * @param {KeyboardEvent} event
+ */
+ _onTabbarArrowKeypress: function (event) {
+ let { 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;
+ }
+
+ let focusableElms = this.tabbarFocusableElms;
+ let curIndex = focusableElms.indexOf(target);
+
+ if (curIndex === -1) {
+ console.warn(target + " is not found among Developer Tools tab bar " +
+ "focusable elements. It needs to either be a button or have " +
+ "tabindex. If it is intended to be hidden, 'hidden' attribute must " +
+ "be used.");
+ return;
+ }
+
+ let newTarget;
+
+ if (key === "ArrowLeft") {
+ // Do nothing if already at the beginning.
+ if (curIndex === 0) {
+ return;
+ }
+ newTarget = focusableElms[curIndex - 1];
+ } else if (key === "ArrowRight") {
+ // Do nothing if already at the end.
+ if (curIndex === focusableElms.length - 1) {
+ return;
+ }
+ newTarget = focusableElms[curIndex + 1];
+ } else {
+ return;
+ }
+
+ focusableElms.forEach(elm =>
+ elm.setAttribute("tabindex", newTarget === elm ? "0" : "-1"));
+ newTarget.focus();
+
+ event.preventDefault();
+ event.stopPropagation();
+ },
+
+ /**
+ * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
+ */
+ _buildButtons: function () {
+ if (this.target.getTrait("highlightable")) {
+ this._buildPickerButton();
+ }
+
+ this.setToolboxButtonsVisibility();
+
+ // Old servers don't have a GCLI Actor, so just return
+ if (!this.target.hasActor("gcli")) {
+ return promise.resolve();
+ }
+ // Disable gcli in browser toolbox until there is usages of it
+ if (this.target.chrome) {
+ return promise.resolve();
+ }
+
+ const options = {
+ environment: CommandUtils.createEnvironment(this, "_target")
+ };
+ return CommandUtils.createRequisition(this.target, options).then(requisition => {
+ this._requisition = requisition;
+
+ const spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
+ return CommandUtils.createButtons(spec, this.target, this.doc, requisition)
+ .then(buttons => {
+ let container = this.doc.getElementById("toolbox-buttons");
+ buttons.forEach(button => {
+ if (button) {
+ container.appendChild(button);
+ }
+ });
+ this.setToolboxButtonsVisibility();
+ });
+ });
+ },
+
+ /**
+ * Adding the element picker button is done here unlike the other buttons
+ * since we want it to work for remote targets too
+ */
+ _buildPickerButton: function () {
+ this._pickerButton = this.doc.createElementNS(HTML_NS, "button");
+ this._pickerButton.id = "command-button-pick";
+ this._pickerButton.className =
+ "command-button command-button-invertable devtools-button";
+ this._pickerButton.setAttribute("title", L10N.getStr("pickButton.tooltip"));
+
+ let container = this.doc.querySelector("#toolbox-picker-container");
+ container.appendChild(this._pickerButton);
+
+ this._pickerButton.addEventListener("click", this._onPickerClick, false);
+ },
+
+ /**
+ * 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.
+ */
+ _onPickerClick: function () {
+ let focus = this.hostType === Toolbox.HostType.BOTTOM ||
+ this.hostType === Toolbox.HostType.SIDE;
+ 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) {
+ 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);
+ },
+
+ /**
+ * Apply the current cache setting from devtools.cache.disabled to this
+ * toolbox's tab.
+ */
+ _applyCacheSettings: function () {
+ let pref = "devtools.cache.disabled";
+ let 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 () {
+ let pref = "devtools.serviceWorkers.testing.enabled";
+ let serviceWorkersTestingEnabled =
+ Services.prefs.getBoolPref(pref) || false;
+
+ if (this.target.activeTab) {
+ this.target.activeTab.reconfigure({
+ "serviceWorkersTestingEnabled": serviceWorkersTestingEnabled
+ });
+ }
+ },
+
+ /**
+ * Setter for the checked state of the picker button in the toolbar
+ * @param {Boolean} isChecked
+ */
+ set pickerButtonChecked(isChecked) {
+ if (isChecked) {
+ this._pickerButton.setAttribute("checked", "true");
+ } else {
+ this._pickerButton.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Return all toolbox buttons (command buttons, plus any others that were
+ * added manually).
+ */
+ get toolboxButtons() {
+ return ToolboxButtons.map(options => {
+ let button = this.doc.getElementById(options.id);
+ // Some buttons may not exist inside of Browser Toolbox
+ if (!button) {
+ return false;
+ }
+
+ return {
+ id: options.id,
+ button: button,
+ label: button.getAttribute("title"),
+ visibilityswitch: "devtools." + options.id + ".enabled",
+ isTargetSupported: options.isTargetSupported
+ ? options.isTargetSupported
+ : target => target.isLocalTab,
+ };
+ }).filter(button=>button);
+ },
+
+ /**
+ * Ensure the visibility of each toolbox button matches the
+ * preference value. Simply hide buttons that are preffed off.
+ */
+ setToolboxButtonsVisibility: function () {
+ this.toolboxButtons.forEach(buttonSpec => {
+ let { visibilityswitch, button, isTargetSupported } = buttonSpec;
+ let on = true;
+ try {
+ on = Services.prefs.getBoolPref(visibilityswitch);
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ on = on && isTargetSupported(this.target);
+
+ if (button) {
+ if (on) {
+ button.removeAttribute("hidden");
+ } else {
+ button.setAttribute("hidden", "true");
+ }
+ }
+ });
+
+ this._updateNoautohideButton();
+ },
+
+ /**
+ * Build a tab for one tool definition and add to the toolbox
+ *
+ * @param {string} toolDefinition
+ * Tool definition of the tool to build a tab for.
+ */
+ _buildTabForTool: function (toolDefinition) {
+ if (!toolDefinition.isTargetSupported(this._target)) {
+ return;
+ }
+
+ let tabs = this.doc.getElementById("toolbox-tabs");
+ let deck = this.doc.getElementById("toolbox-deck");
+
+ let id = toolDefinition.id;
+
+ if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
+ toolDefinition.ordinal = MAX_ORDINAL;
+ }
+
+ let radio = this.doc.createElement("radio");
+ // The radio element is not being used in the conventional way, thus
+ // the devtools-tab class replaces the radio XBL binding with its base
+ // binding (the control-item binding).
+ radio.className = "devtools-tab";
+ radio.id = "toolbox-tab-" + id;
+ radio.setAttribute("toolid", id);
+ radio.setAttribute("tabindex", "0");
+ radio.setAttribute("ordinal", toolDefinition.ordinal);
+ radio.setAttribute("tooltiptext", toolDefinition.tooltip);
+ if (toolDefinition.invertIconForLightTheme) {
+ radio.setAttribute("icon-invertable", "light-theme");
+ } else if (toolDefinition.invertIconForDarkTheme) {
+ radio.setAttribute("icon-invertable", "dark-theme");
+ }
+
+ radio.addEventListener("command", this.selectTool.bind(this, id));
+
+ // spacer lets us center the image and label, while allowing cropping
+ let spacer = this.doc.createElement("spacer");
+ spacer.setAttribute("flex", "1");
+ radio.appendChild(spacer);
+
+ if (toolDefinition.icon) {
+ let image = this.doc.createElement("image");
+ image.className = "default-icon";
+ image.setAttribute("src",
+ toolDefinition.icon || toolDefinition.highlightedicon);
+ radio.appendChild(image);
+ // Adding the highlighted icon image
+ image = this.doc.createElement("image");
+ image.className = "highlighted-icon";
+ image.setAttribute("src",
+ toolDefinition.highlightedicon || toolDefinition.icon);
+ radio.appendChild(image);
+ }
+
+ if (toolDefinition.label && !toolDefinition.iconOnly) {
+ let label = this.doc.createElement("label");
+ label.setAttribute("value", toolDefinition.label);
+ label.setAttribute("crop", "end");
+ label.setAttribute("flex", "1");
+ radio.appendChild(label);
+ }
+
+ if (!toolDefinition.bgTheme) {
+ toolDefinition.bgTheme = "theme-toolbar";
+ }
+ let vbox = this.doc.createElement("vbox");
+ vbox.className = "toolbox-panel " + toolDefinition.bgTheme;
+
+ // There is already a container for the webconsole frame.
+ if (!this.doc.getElementById("toolbox-panel-" + id)) {
+ vbox.id = "toolbox-panel-" + id;
+ }
+
+ if (id === "options") {
+ // Options panel is special. It doesn't belong in the same container as
+ // the other tabs.
+ radio.setAttribute("role", "button");
+ let optionTabContainer = this.doc.getElementById("toolbox-option-container");
+ optionTabContainer.appendChild(radio);
+ deck.appendChild(vbox);
+ } else {
+ radio.setAttribute("role", "tab");
+
+ // If there is no tab yet, or the ordinal to be added is the largest one.
+ if (tabs.childNodes.length == 0 ||
+ tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) {
+ tabs.appendChild(radio);
+ deck.appendChild(vbox);
+ } else {
+ // else, iterate over all the tabs to get the correct location.
+ Array.some(tabs.childNodes, (node, i) => {
+ if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
+ tabs.insertBefore(radio, node);
+ deck.insertBefore(vbox, deck.childNodes[i]);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+
+ this._addKeysToWindow();
+ },
+
+ /**
+ * 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(() => {
+ return this.loadTool(id);
+ });
+ }
+
+ let deferred = defer();
+ let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
+
+ if (iframe) {
+ let panel = this._toolPanels.get(id);
+ if (panel) {
+ deferred.resolve(panel);
+ } else {
+ this.once(id + "-ready", initializedPanel => {
+ deferred.resolve(initializedPanel);
+ });
+ }
+ return deferred.promise;
+ }
+
+ let definition = gDevTools.getToolDefinition(id);
+ if (!definition) {
+ deferred.reject(new Error("no such tool id " + id));
+ return deferred.promise;
+ }
+
+ 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) {
+ let vbox = this.doc.getElementById("toolbox-panel-" + id);
+ vbox.appendChild(iframe);
+ }
+
+ let 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")) {
+ let 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 {
+ let buildDeferred = defer();
+ buildDeferred.resolve(panel);
+ built = buildDeferred.promise;
+ }
+ }
+
+ // 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);
+
+ deferred.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) {
+ let domHelper = new DOMHelpers(iframe.contentWindow);
+ domHelper.onceDOMReady(onLoad);
+ } else {
+ let callback = () => {
+ iframe.removeEventListener("DOMContentLoaded", callback);
+ onLoad();
+ };
+
+ iframe.addEventListener("DOMContentLoaded", callback);
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Set the dir attribute on the content document element of the provided iframe.
+ *
+ * @param {IFrameElement} iframe
+ */
+ setIframeDocumentDir: function (iframe) {
+ let 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.
+ let top = this.win.top;
+ let topDocEl = top.document.documentElement;
+ let isRtl = top.getComputedStyle(topDocEl).direction === "rtl";
+ docEl.setAttribute("dir", isRtl ? "rtl" : "ltr");
+ }
+ },
+
+ /**
+ * 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");
+ }
+ });
+ },
+
+ /**
+ * Switch to the tool with the given id
+ *
+ * @param {string} id
+ * The id of the tool to switch to
+ */
+ selectTool: function (id) {
+ this.emit("before-select", id);
+
+ let tabs = this.doc.querySelectorAll(".devtools-tab");
+ this.selectSingleNode(tabs, "toolbox-tab-" + id);
+
+ // If options is selected, the separator between it and the
+ // command buttons should be hidden.
+ let sep = this.doc.getElementById("toolbox-controls-separator");
+ if (id === "options") {
+ sep.setAttribute("invisible", "true");
+ } else {
+ sep.removeAttribute("invisible");
+ }
+
+ if (this.currentToolId == id) {
+ let panel = this._toolPanels.get(id);
+ if (panel) {
+ // We have a panel instance, so the tool is already fully loaded.
+
+ // re-focus tool to get key events again
+ this.focusTool(id);
+
+ // Return the existing panel in order to have a consistent return value.
+ return promise.resolve(panel);
+ }
+ // Otherwise, if there is no panel instance, it is still loading,
+ // so we are racing another call to selectTool with the same id.
+ return this.once("select").then(() => promise.resolve(this._toolPanels.get(id)));
+ }
+
+ if (!this.isReady) {
+ throw new Error("Can't select tool, wait for toolbox 'ready' event");
+ }
+
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+
+ if (tab) {
+ if (this.currentToolId) {
+ this._telemetry.toolClosed(this.currentToolId);
+ }
+ this._telemetry.toolOpened(id);
+ } else {
+ throw new Error("No tool found");
+ }
+
+ let tabstrip = this.doc.getElementById("toolbox-tabs");
+
+ // select the right tab, making 0th index the default tab if right tab not
+ // found.
+ tabstrip.selectedItem = tab || tabstrip.childNodes[0];
+
+ // and select the right iframe
+ let toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
+ this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
+
+ this.lastUsedToolId = this.currentToolId;
+ this.currentToolId = id;
+ this._refreshConsoleDisplay();
+ if (id != "options") {
+ Services.prefs.setCharPref(this._prefs.LAST_TOOL, id);
+ }
+
+ return this.loadTool(id).then(panel => {
+ // focus the tool's frame to start receiving key events
+ this.focusTool(id);
+
+ this.emit("select", id);
+ this.emit(id + "-selected", panel);
+ return panel;
+ });
+ },
+
+ /**
+ * Focus a tool's panel by id
+ * @param {string} id
+ * The id of tool to focus
+ */
+ focusTool: function (id, state = true) {
+ let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
+
+ if (state) {
+ iframe.focus();
+ } else {
+ iframe.blur();
+ }
+ },
+
+ /**
+ * Focus split console's input line
+ */
+ focusConsoleInput: function () {
+ let consolePanel = this.getPanel("webconsole");
+ if (consolePanel) {
+ consolePanel.focusInput();
+ }
+ },
+
+ /**
+ * If the console is split and we are focusing an element outside
+ * of the console, then store the newly focused element, so that
+ * it can be restored once the split console closes.
+ */
+ _onFocus: function ({originalTarget}) {
+ // Ignore any non element nodes, or any elements contained
+ // within the webconsole frame.
+ let webconsoleURL = gDevTools.getToolDefinition("webconsole").url;
+ if (originalTarget.nodeType !== 1 ||
+ originalTarget.baseURI === webconsoleURL) {
+ return;
+ }
+
+ this._lastFocusedElement = originalTarget;
+ },
+
+ /**
+ * Opens the split console.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * loaded and focused.
+ */
+ openSplitConsole: function () {
+ this._splitConsole = true;
+ Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, true);
+ this._refreshConsoleDisplay();
+ this.emit("split-console");
+
+ return this.loadTool("webconsole").then(() => {
+ this.focusConsoleInput();
+ });
+ },
+
+ /**
+ * Closes the split console.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * closed.
+ */
+ closeSplitConsole: function () {
+ this._splitConsole = false;
+ Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, false);
+ this._refreshConsoleDisplay();
+ this.emit("split-console");
+
+ if (this._lastFocusedElement) {
+ this._lastFocusedElement.focus();
+ }
+ return promise.resolve();
+ },
+
+ /**
+ * Toggles the split state of the webconsole. If the webconsole panel
+ * is already selected then this command is ignored.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * opened or closed.
+ */
+ toggleSplitConsole: function () {
+ if (this.currentToolId !== "webconsole") {
+ return this.splitConsole ?
+ this.closeSplitConsole() :
+ this.openSplitConsole();
+ }
+
+ return promise.resolve();
+ },
+
+ /**
+ * Tells the target tab to reload.
+ */
+ reloadTarget: function (force) {
+ this.target.activeTab.reload({ force: force });
+ },
+
+ /**
+ * Loads the tool next to the currently selected tool.
+ */
+ selectNextTool: function () {
+ let tools = this.doc.querySelectorAll(".devtools-tab");
+ let selected = this.doc.querySelector(".devtools-tab[selected]");
+ let nextIndex = [...tools].indexOf(selected) + 1;
+ let next = tools[nextIndex] || tools[0];
+ let tool = next.getAttribute("toolid");
+ return this.selectTool(tool);
+ },
+
+ /**
+ * Loads the tool just left to the currently selected tool.
+ */
+ selectPreviousTool: function () {
+ let tools = this.doc.querySelectorAll(".devtools-tab");
+ let selected = this.doc.querySelector(".devtools-tab[selected]");
+ let prevIndex = [...tools].indexOf(selected) - 1;
+ let prev = tools[prevIndex] || tools[tools.length - 1];
+ let tool = prev.getAttribute("toolid");
+ return this.selectTool(tool);
+ },
+
+ /**
+ * Highlights the tool's tab if it is not the currently selected tool.
+ *
+ * @param {string} id
+ * The id of the tool to highlight
+ */
+ highlightTool: function (id) {
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+ tab && tab.setAttribute("highlighted", "true");
+ },
+
+ /**
+ * De-highlights the tool's tab.
+ *
+ * @param {string} id
+ * The id of the tool to unhighlight
+ */
+ unhighlightTool: function (id) {
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+ tab && tab.removeAttribute("highlighted");
+ },
+
+ /**
+ * Raise the toolbox host.
+ */
+ raise: function () {
+ this.postMessage({
+ name: "raise-host"
+ });
+ },
+
+ /**
+ * Refresh the host's title.
+ */
+ _refreshHostTitle: function () {
+ let title;
+ if (this.target.name && this.target.name != this.target.url) {
+ title = L10N.getFormatStr("toolbox.titleTemplate2", this.target.name,
+ this.target.url);
+ } else {
+ title = L10N.getFormatStr("toolbox.titleTemplate1", this.target.url);
+ }
+ this.postMessage({
+ name: "set-host-title",
+ title
+ });
+ },
+
+ // Returns an instance of the preference actor
+ get _preferenceFront() {
+ return this.target.root.then(rootForm => {
+ return getPreferenceFront(this.target.client, rootForm);
+ });
+ },
+
+ _toggleAutohide: Task.async(function* () {
+ let prefName = "ui.popup.disable_autohide";
+ let front = yield this._preferenceFront;
+ let current = yield front.getBoolPref(prefName);
+ yield front.setBoolPref(prefName, !current);
+
+ this._updateNoautohideButton();
+ }),
+
+ _updateNoautohideButton: Task.async(function* () {
+ let menu = this.doc.getElementById("command-button-noautohide");
+ if (menu.getAttribute("hidden") === "true") {
+ return;
+ }
+ if (!this.target.root) {
+ return;
+ }
+ let prefName = "ui.popup.disable_autohide";
+ let front = yield this._preferenceFront;
+ let current = yield front.getBoolPref(prefName);
+ if (current) {
+ menu.setAttribute("checked", "true");
+ } else {
+ menu.removeAttribute("checked");
+ }
+ }),
+
+ _listFrames: function (event) {
+ if (!this._target.activeTab || !this._target.activeTab.traits.frames) {
+ // We are not targetting a regular TabActor
+ // it can be either an addon or browser toolbox actor
+ return promise.resolve();
+ }
+ let packet = {
+ to: this._target.form.actor,
+ type: "listFrames"
+ };
+ return this._target.client.request(packet, resp => {
+ this._updateFrames(null, { frames: resp.frames });
+ });
+ },
+
+ /**
+ * Show a drop down menu that allows the user to switch frames.
+ */
+ showFramesMenu: function (event) {
+ let menu = new Menu();
+ let target = event.target;
+
+ // Generate list of menu items from the list of frames.
+ this.frameMap.forEach(frame => {
+ // A frame is checked if it's the selected one.
+ let checked = frame.id == this.selectedFrameId;
+
+ // Create menu item.
+ menu.append(new MenuItem({
+ label: frame.url,
+ type: "radio",
+ checked,
+ click: () => {
+ this.onSelectFrame(frame.id);
+ }
+ }));
+ });
+
+ menu.once("open").then(() => {
+ target.setAttribute("open", "true");
+ });
+
+ menu.once("close").then(() => {
+ target.removeAttribute("open");
+ });
+
+ // Show a drop down menu with frames.
+ // XXX Missing menu API for specifying target (anchor)
+ // and relative position to it. See also:
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
+ let rect = target.getBoundingClientRect();
+ let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
+ let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
+ menu.popup(rect.left + screenX, rect.bottom + screenY, this);
+
+ return menu;
+ },
+
+ /**
+ * Select a frame by sending 'switchToFrame' packet to the backend.
+ */
+ onSelectFrame: function (frameId) {
+ // Send packet to the backend to select specified frame and
+ // wait for 'frameUpdate' event packet to update the UI.
+ let packet = {
+ to: this._target.form.actor,
+ type: "switchToFrame",
+ windowId: frameId
+ };
+ this._target.client.request(packet);
+ },
+
+ /**
+ * A handler for 'frameUpdate' packets received from the backend.
+ * Following properties might be set on the packet:
+ *
+ * destroyAll {Boolean}: All frames have been destroyed.
+ * selected {Number}: A frame has been selected
+ * frames {Array}: list of frames. Every frame can have:
+ * id {Number}: frame ID
+ * url {String}: frame URL
+ * title {String}: frame title
+ * destroy {Boolean}: Set to true if destroyed
+ * parentID {Number}: ID of the parent frame (not set
+ * for top level window)
+ */
+ _updateFrames: function (event, data) {
+ if (!Services.prefs.getBoolPref("devtools.command-button-frames.enabled")) {
+ return;
+ }
+
+ // We may receive this event before the toolbox is ready.
+ if (!this.isReady) {
+ return;
+ }
+
+ // Store (synchronize) data about all existing frames on the backend
+ if (data.destroyAll) {
+ this.frameMap.clear();
+ this.selectedFrameId = null;
+ } else if (data.selected) {
+ this.selectedFrameId = data.selected;
+ } else if (data.frames) {
+ data.frames.forEach(frame => {
+ if (frame.destroy) {
+ this.frameMap.delete(frame.id);
+
+ // Reset the currently selected frame if it's destroyed.
+ if (this.selectedFrameId == frame.id) {
+ this.selectedFrameId = null;
+ }
+ } else {
+ this.frameMap.set(frame.id, frame);
+ }
+ });
+ }
+
+ // If there is no selected frame select the first top level
+ // frame by default. Note that there might be more top level
+ // frames in case of the BrowserToolbox.
+ if (!this.selectedFrameId) {
+ let frames = [...this.frameMap.values()];
+ let topFrames = frames.filter(frame => !frame.parentID);
+ this.selectedFrameId = topFrames.length ? topFrames[0].id : null;
+ }
+
+ // Check out whether top frame is currently selected.
+ // Note that only child frame has parentID.
+ let frame = this.frameMap.get(this.selectedFrameId);
+ let topFrameSelected = frame ? !frame.parentID : false;
+ let button = this.doc.getElementById("command-button-frames");
+ button.removeAttribute("checked");
+
+ // If non-top level frame is selected the toolbar button is
+ // marked as 'checked' indicating that a child frame is active.
+ if (!topFrameSelected && this.selectedFrameId) {
+ button.setAttribute("checked", "true");
+ }
+ },
+
+ /**
+ * Switch to the last used host for the toolbox UI.
+ */
+ switchToPreviousHost: function () {
+ return this.switchHost("previous");
+ },
+
+ /**
+ * Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window,
+ * and focus the window when done.
+ *
+ * @param {string} hostType
+ * The host type of the new host object
+ */
+ switchHost: function (hostType) {
+ if (hostType == this.hostType || !this._target.isLocalTab) {
+ return null;
+ }
+
+ this.emit("host-will-change", hostType);
+
+ // ToolboxHostManager is going to call swapFrameLoaders which mess up with
+ // focus. We have to blur before calling it in order to be able to restore
+ // the focus after, in _onSwitchedHost.
+ this.focusTool(this.currentToolId, false);
+
+ // Host code on the chrome side will send back a message once the host
+ // switched
+ this.postMessage({
+ name: "switch-host",
+ hostType
+ });
+
+ return this.once("host-changed");
+ },
+
+ _onSwitchedHost: function ({ hostType }) {
+ this._hostType = hostType;
+
+ this._buildDockButtons();
+ this._addKeysToWindow();
+
+ // We blurred the tools at start of switchHost, but also when clicking on
+ // host switching button. We now have to restore the focus.
+ this.focusTool(this.currentToolId, true);
+
+ this.emit("host-changed");
+ this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
+ },
+
+ /**
+ * Return if the tool is available as a tab (i.e. if it's checked
+ * in the options panel). This is different from Toolbox.getPanel -
+ * a tool could be registered but not yet opened in which case
+ * isToolRegistered would return true but getPanel would return false.
+ */
+ isToolRegistered: function (toolId) {
+ return gDevTools.getToolDefinitionMap().has(toolId);
+ },
+
+ /**
+ * Handler for the tool-registered event.
+ * @param {string} event
+ * Name of the event ("tool-registered")
+ * @param {string} toolId
+ * Id of the tool that was registered
+ */
+ _toolRegistered: function (event, toolId) {
+ let tool = gDevTools.getToolDefinition(toolId);
+ this._buildTabForTool(tool);
+ // Emit the event so tools can listen to it from the toolbox level
+ // instead of gDevTools
+ this.emit("tool-registered", toolId);
+ },
+
+ /**
+ * Handler for the tool-unregistered event.
+ * @param {string} event
+ * Name of the event ("tool-unregistered")
+ * @param {string|object} toolId
+ * Definition or id of the tool that was unregistered. Passing the
+ * tool id should be avoided as it is a temporary measure.
+ */
+ _toolUnregistered: function (event, toolId) {
+ if (typeof toolId != "string") {
+ toolId = toolId.id;
+ }
+
+ if (this._toolPanels.has(toolId)) {
+ let instance = this._toolPanels.get(toolId);
+ instance.destroy();
+ this._toolPanels.delete(toolId);
+ }
+
+ let radio = this.doc.getElementById("toolbox-tab-" + toolId);
+ let panel = this.doc.getElementById("toolbox-panel-" + toolId);
+
+ if (radio) {
+ if (this.currentToolId == toolId) {
+ let nextToolName = null;
+ if (radio.nextSibling) {
+ nextToolName = radio.nextSibling.getAttribute("toolid");
+ }
+ if (radio.previousSibling) {
+ nextToolName = radio.previousSibling.getAttribute("toolid");
+ }
+ if (nextToolName) {
+ this.selectTool(nextToolName);
+ }
+ }
+ radio.parentNode.removeChild(radio);
+ }
+
+ if (panel) {
+ panel.parentNode.removeChild(panel);
+ }
+
+ if (this.hostType == Toolbox.HostType.WINDOW) {
+ let doc = this.win.parent.document;
+ let key = doc.getElementById("key_" + toolId);
+ if (key) {
+ key.parentNode.removeChild(key);
+ }
+ }
+ // Emit the event so tools can listen to it from the toolbox level
+ // instead of gDevTools
+ this.emit("tool-unregistered", toolId);
+ },
+
+ /**
+ * Initialize the inspector/walker/selection/highlighter fronts.
+ * Returns a promise that resolves when the fronts are initialized
+ */
+ initInspector: function () {
+ if (!this._initInspector) {
+ this._initInspector = Task.spawn(function* () {
+ this._inspector = InspectorFront(this._target.client, this._target.form);
+ let pref = "devtools.inspector.showAllAnonymousContent";
+ let showAllAnonymousContent = Services.prefs.getBoolPref(pref);
+ this._walker = yield this._inspector.getWalker({ showAllAnonymousContent });
+ this._selection = new Selection(this._walker);
+
+ if (this.highlighterUtils.isRemoteHighlightable()) {
+ this.walker.on("highlighter-ready", this._highlighterReady);
+ this.walker.on("highlighter-hide", this._highlighterHidden);
+
+ let autohide = !flags.testing;
+ this._highlighter = yield this._inspector.getHighlighter(autohide);
+ }
+ }.bind(this));
+ }
+ return this._initInspector;
+ },
+
+ /**
+ * Destroy the inspector/walker/selection fronts
+ * Returns a promise that resolves when the fronts are destroyed
+ */
+ destroyInspector: function () {
+ if (this._destroyingInspector) {
+ return this._destroyingInspector;
+ }
+
+ this._destroyingInspector = Task.spawn(function* () {
+ if (!this._inspector) {
+ return;
+ }
+
+ // Releasing the walker (if it has been created)
+ // This can fail, but in any case, we want to continue destroying the
+ // inspector/highlighter/selection
+ // FF42+: Inspector actor starts managing Walker actor and auto destroy it.
+ if (this._walker && !this.walker.traits.autoReleased) {
+ try {
+ yield this._walker.release();
+ } catch (e) {
+ // Do nothing;
+ }
+ }
+
+ yield this.highlighterUtils.stopPicker();
+ yield this._inspector.destroy();
+ if (this._highlighter) {
+ // Note that if the toolbox is closed, this will work fine, but will fail
+ // in case the browser is closed and will trigger a noSuchActor message.
+ // We ignore the promise that |_hideBoxModel| returns, since we should still
+ // proceed with the rest of destruction if it fails.
+ // FF42+ now does the cleanup from the actor.
+ if (!this.highlighter.traits.autoHideOnDestroy) {
+ this.highlighterUtils.unhighlight();
+ }
+ yield this._highlighter.destroy();
+ }
+ if (this._selection) {
+ this._selection.destroy();
+ }
+
+ if (this.walker) {
+ this.walker.off("highlighter-ready", this._highlighterReady);
+ this.walker.off("highlighter-hide", this._highlighterHidden);
+ }
+
+ this._inspector = null;
+ this._highlighter = null;
+ this._selection = null;
+ this._walker = null;
+ }.bind(this));
+ return this._destroyingInspector;
+ },
+
+ /**
+ * Get the toolbox's notification component
+ *
+ * @return The notification box component.
+ */
+ getNotificationBox: function () {
+ return this.notificationBox;
+ },
+
+ /**
+ * Remove all UI elements, detach from target and clear up
+ */
+ destroy: function () {
+ // If several things call destroy then we give them all the same
+ // destruction promise so we're sure to destroy only once
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+ let deferred = defer();
+ this._destroyer = deferred.promise;
+
+ this.emit("destroy");
+
+ this._target.off("navigate", this._refreshHostTitle);
+ this._target.off("frame-update", this._updateFrames);
+ this.off("select", this._refreshHostTitle);
+ this.off("host-changed", this._refreshHostTitle);
+ this.off("ready", this._showDevEditionPromo);
+
+ gDevTools.off("tool-registered", this._toolRegistered);
+ gDevTools.off("tool-unregistered", this._toolUnregistered);
+
+ gDevTools.off("pref-changed", this._prefChanged);
+
+ this._lastFocusedElement = null;
+ if (this._sourceMapService) {
+ this._sourceMapService.destroy();
+ this._sourceMapService = null;
+ }
+
+ if (this.webconsolePanel) {
+ this._saveSplitConsoleHeight();
+ this.webconsolePanel.removeEventListener("resize",
+ this._saveSplitConsoleHeight);
+ this.webconsolePanel = null;
+ }
+ if (this.closeButton) {
+ this.closeButton.removeEventListener("click", this.destroy, true);
+ this.closeButton = null;
+ }
+ if (this.textBoxContextMenuPopup) {
+ this.textBoxContextMenuPopup.removeEventListener("popupshowing",
+ this._updateTextBoxMenuItems, true);
+ this.textBoxContextMenuPopup = null;
+ }
+ if (this.tabbar) {
+ this.tabbar.removeEventListener("focus", this._onTabbarFocus, true);
+ this.tabbar.removeEventListener("click", this._onTabbarFocus, true);
+ this.tabbar.removeEventListener("keypress", this._onTabbarArrowKeypress);
+ this.tabbar = null;
+ }
+
+ let outstanding = [];
+ for (let [id, panel] of this._toolPanels) {
+ try {
+ gDevTools.emit(id + "-destroy", this, panel);
+ this.emit(id + "-destroy", panel);
+
+ outstanding.push(panel.destroy());
+ } catch (e) {
+ // We don't want to stop here if any panel fail to close.
+ console.error("Panel " + id + ":", e);
+ }
+ }
+
+ this.browserRequire = null;
+
+ // Now that we are closing the toolbox we can re-enable the cache settings
+ // and disable the service workers testing settings for the current tab.
+ // FF41+ automatically cleans up state in actor on disconnect.
+ if (this.target.activeTab && !this.target.activeTab.traits.noTabReconfigureOnClose) {
+ this.target.activeTab.reconfigure({
+ "cacheDisabled": false,
+ "serviceWorkersTestingEnabled": false
+ });
+ }
+
+ // Destroying the walker and inspector fronts
+ outstanding.push(this.destroyInspector().then(() => {
+ // Removing buttons
+ if (this._pickerButton) {
+ this._pickerButton.removeEventListener("click", this._togglePicker, false);
+ this._pickerButton = null;
+ }
+ }));
+
+ // Destroy the profiler connection
+ outstanding.push(this.destroyPerformance());
+
+ // Detach the thread
+ detachThread(this._threadClient);
+ this._threadClient = null;
+
+ // We need to grab a reference to win before this._host is destroyed.
+ let win = this.win;
+
+ if (this._requisition) {
+ CommandUtils.destroyRequisition(this._requisition, this.target);
+ }
+ this._telemetry.toolClosed("toolbox");
+ this._telemetry.destroy();
+
+ // Finish all outstanding tasks (which means finish destroying panels and
+ // then destroying the host, successfully or not) before destroying the
+ // target.
+ deferred.resolve(settleAll(outstanding)
+ .catch(console.error)
+ .then(() => {
+ this._removeHostListeners();
+
+ // `location` may already be null if the toolbox document is already
+ // in process of destruction. Otherwise if it is still around, ensure
+ // releasing toolbox document and triggering cleanup thanks to unload
+ // event. We do that precisely here, before nullifying the target as
+ // various cleanup code depends on the target attribute to be still
+ // defined.
+ if (win.location) {
+ win.location.replace("about:blank");
+ }
+
+ // Targets need to be notified that the toolbox is being torn down.
+ // This is done after other destruction tasks since it may tear down
+ // fronts and the debugger transport which earlier destroy methods may
+ // require to complete.
+ if (!this._target) {
+ return null;
+ }
+ let target = this._target;
+ this._target = null;
+ this.highlighterUtils.release();
+ target.off("close", this.destroy);
+ return target.destroy();
+ }, console.error).then(() => {
+ this.emit("destroyed");
+
+ // Free _host after the call to destroyed in order to let a chance
+ // to destroyed listeners to still query toolbox attributes
+ this._host = null;
+ this._win = null;
+ this._toolPanels.clear();
+
+ // Force GC to prevent long GC pauses when running tests and to free up
+ // memory in general when the toolbox is closed.
+ if (flags.testing) {
+ win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .garbageCollect();
+ }
+ }).then(null, console.error));
+
+ let leakCheckObserver = ({wrappedJSObject: barrier}) => {
+ // Make the leak detector wait until this toolbox is properly destroyed.
+ barrier.client.addBlocker("DevTools: Wait until toolbox is destroyed",
+ this._destroyer);
+ };
+
+ let topic = "shutdown-leaks-before-check";
+ Services.obs.addObserver(leakCheckObserver, topic, false);
+ this._destroyer.then(() => {
+ Services.obs.removeObserver(leakCheckObserver, topic);
+ });
+
+ return this._destroyer;
+ },
+
+ _highlighterReady: function () {
+ this.emit("highlighter-ready");
+ },
+
+ _highlighterHidden: function () {
+ this.emit("highlighter-hide");
+ },
+
+ /**
+ * For displaying the promotional Doorhanger on first opening of
+ * the developer tools, promoting the Developer Edition.
+ */
+ _showDevEditionPromo: function () {
+ // Do not display in browser toolbox
+ if (this.target.chrome) {
+ return;
+ }
+ showDoorhanger({ window: this.win, type: "deveditionpromo" });
+ },
+
+ /**
+ * Enable / disable necessary textbox menu items using globalOverlay.js.
+ */
+ _updateTextBoxMenuItems: function () {
+ let window = this.win;
+ ["cmd_undo", "cmd_delete", "cmd_cut",
+ "cmd_copy", "cmd_paste", "cmd_selectAll"].forEach(window.goUpdateCommand);
+ },
+
+ /**
+ * Open the textbox context menu at given coordinates.
+ * Panels in the toolbox can call this on contextmenu events with event.screenX/Y
+ * instead of having to implement their own copy/paste/selectAll menu.
+ * @param {Number} x
+ * @param {Number} y
+ */
+ openTextBoxContextMenu: function (x, y) {
+ this.textBoxContextMenuPopup.openPopupAtScreen(x, y, true);
+ },
+
+ /**
+ * Connects to the SPS profiler when the developer tools are open. This is
+ * necessary because of the WebConsole's `profile` and `profileEnd` methods.
+ */
+ initPerformance: Task.async(function* () {
+ // If target does not have profiler actor (addons), do not
+ // even register the shared performance connection.
+ if (!this.target.hasActor("profiler")) {
+ return promise.resolve();
+ }
+
+ if (this._performanceFrontConnection) {
+ return this._performanceFrontConnection.promise;
+ }
+
+ this._performanceFrontConnection = defer();
+ this._performance = createPerformanceFront(this._target);
+ yield this.performance.connect();
+
+ // Emit an event when connected, but don't wait on startup for this.
+ this.emit("profiler-connected");
+
+ this.performance.on("*", this._onPerformanceFrontEvent);
+ this._performanceFrontConnection.resolve(this.performance);
+ return this._performanceFrontConnection.promise;
+ }),
+
+ /**
+ * Disconnects the underlying Performance actor. If the connection
+ * has not finished initializing, as opening a toolbox does not wait,
+ * the performance connection destroy method will wait for it on its own.
+ */
+ destroyPerformance: Task.async(function* () {
+ if (!this.performance) {
+ return;
+ }
+ // If still connecting to performance actor, allow the
+ // actor to resolve its connection before attempting to destroy.
+ if (this._performanceFrontConnection) {
+ yield this._performanceFrontConnection.promise;
+ }
+ this.performance.off("*", this._onPerformanceFrontEvent);
+ yield this.performance.destroy();
+ this._performance = null;
+ }),
+
+ /**
+ * Called when any event comes from the PerformanceFront. If the performance tool is
+ * already loaded when the first event comes in, immediately unbind this handler, as
+ * this is only used to queue up observed recordings before the performance tool can
+ * handle them, which will only occur when `console.profile()` recordings are started
+ * before the tool loads.
+ */
+ _onPerformanceFrontEvent: Task.async(function* (eventName, recording) {
+ if (this.getPanel("performance")) {
+ this.performance.off("*", this._onPerformanceFrontEvent);
+ return;
+ }
+
+ this._performanceQueuedRecordings = this._performanceQueuedRecordings || [];
+ let recordings = this._performanceQueuedRecordings;
+
+ // Before any console recordings, we'll get a `console-profile-start` event
+ // warning us that a recording will come later (via `recording-started`), so
+ // start to boot up the tool and populate the tool with any other recordings
+ // observed during that time.
+ if (eventName === "console-profile-start" && !this._performanceToolOpenedViaConsole) {
+ this._performanceToolOpenedViaConsole = this.loadTool("performance");
+ let panel = yield this._performanceToolOpenedViaConsole;
+ yield panel.open();
+
+ panel.panelWin.PerformanceController.populateWithRecordings(recordings);
+ this.performance.off("*", this._onPerformanceFrontEvent);
+ }
+
+ // Otherwise, if it's a recording-started event, we've already started loading
+ // the tool, so just store this recording in our array to be later populated
+ // once the tool loads.
+ if (eventName === "recording-started") {
+ recordings.push(recording);
+ }
+ }),
+
+ /**
+ * Returns gViewSourceUtils for viewing source.
+ */
+ get gViewSourceUtils() {
+ return this.win.gViewSourceUtils;
+ },
+
+ /**
+ * Opens source in style editor. Falls back to plain "view-source:".
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSourceInStyleEditor: function (sourceURL, sourceLine) {
+ return viewSource.viewSourceInStyleEditor(this, sourceURL, sourceLine);
+ },
+
+ /**
+ * Opens source in debugger. Falls back to plain "view-source:".
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSourceInDebugger: function (sourceURL, sourceLine) {
+ return viewSource.viewSourceInDebugger(this, sourceURL, sourceLine);
+ },
+
+ /**
+ * Opens source in scratchpad. Falls back to plain "view-source:".
+ * TODO The `sourceURL` for scratchpad instances are like `Scratchpad/1`.
+ * If instances are scoped one-per-browser-window, then we should be able
+ * to infer the URL from this toolbox, or use the built in scratchpad IN
+ * the toolbox.
+ *
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSourceInScratchpad: function (sourceURL, sourceLine) {
+ return viewSource.viewSourceInScratchpad(sourceURL, sourceLine);
+ },
+
+ /**
+ * Opens source in plain "view-source:".
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSource: function (sourceURL, sourceLine) {
+ return viewSource.viewSource(this, sourceURL, sourceLine);
+ },
+};