michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const MAX_ORDINAL = 99; michael@0: const ZOOM_PREF = "devtools.toolbox.zoomValue"; michael@0: const MIN_ZOOM = 0.5; michael@0: const MAX_ZOOM = 2; michael@0: michael@0: let {Cc, Ci, Cu} = require("chrome"); michael@0: let {Promise: promise} = require("resource://gre/modules/Promise.jsm"); michael@0: let EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: let Telemetry = require("devtools/shared/telemetry"); michael@0: let HUDService = require("devtools/webconsole/hudservice"); michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource:///modules/devtools/gDevTools.jsm"); michael@0: Cu.import("resource:///modules/devtools/scratchpad-manager.jsm"); michael@0: Cu.import("resource:///modules/devtools/DOMHelpers.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: michael@0: loader.lazyGetter(this, "Hosts", () => require("devtools/framework/toolbox-hosts").Hosts); michael@0: michael@0: loader.lazyImporter(this, "CommandUtils", "resource:///modules/devtools/DeveloperToolbar.jsm"); michael@0: michael@0: loader.lazyGetter(this, "toolboxStrings", () => { michael@0: let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties"); michael@0: return (name, ...args) => { michael@0: try { michael@0: if (!args.length) { michael@0: return bundle.GetStringFromName(name); michael@0: } michael@0: return bundle.formatStringFromName(name, args, args.length); michael@0: } catch (ex) { michael@0: Services.console.logStringMessage("Error reading '" + name + "'"); michael@0: return null; michael@0: } michael@0: }; michael@0: }); michael@0: michael@0: loader.lazyGetter(this, "Selection", () => require("devtools/framework/selection").Selection); michael@0: loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront); michael@0: michael@0: /** michael@0: * A "Toolbox" is the component that holds all the tools for one specific michael@0: * target. Visually, it's a document that includes the tools tabs and all michael@0: * the iframes where the tool panels will be living in. michael@0: * michael@0: * @param {object} target michael@0: * The object the toolbox is debugging. michael@0: * @param {string} selectedTool michael@0: * Tool to select initially michael@0: * @param {Toolbox.HostType} hostType michael@0: * Type of host that will host the toolbox (e.g. sidebar, window) michael@0: * @param {object} hostOptions michael@0: * Options for host specifically michael@0: */ michael@0: function Toolbox(target, selectedTool, hostType, hostOptions) { michael@0: this._target = target; michael@0: this._toolPanels = new Map(); michael@0: this._telemetry = new Telemetry(); michael@0: michael@0: this._toolRegistered = this._toolRegistered.bind(this); michael@0: this._toolUnregistered = this._toolUnregistered.bind(this); michael@0: this._refreshHostTitle = this._refreshHostTitle.bind(this); michael@0: this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this) michael@0: this.destroy = this.destroy.bind(this); michael@0: this.highlighterUtils = new ToolboxHighlighterUtils(this); michael@0: this._highlighterReady = this._highlighterReady.bind(this); michael@0: this._highlighterHidden = this._highlighterHidden.bind(this); michael@0: michael@0: this._target.on("close", this.destroy); michael@0: michael@0: if (!hostType) { michael@0: hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST); michael@0: } michael@0: if (!selectedTool) { michael@0: selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); michael@0: } michael@0: if (!gDevTools.getToolDefinition(selectedTool)) { michael@0: selectedTool = "webconsole"; michael@0: } michael@0: this._defaultToolId = selectedTool; michael@0: michael@0: this._host = this._createHost(hostType, hostOptions); michael@0: michael@0: EventEmitter.decorate(this); michael@0: michael@0: this._target.on("navigate", this._refreshHostTitle); michael@0: this.on("host-changed", this._refreshHostTitle); michael@0: this.on("select", this._refreshHostTitle); michael@0: michael@0: gDevTools.on("tool-registered", this._toolRegistered); michael@0: gDevTools.on("tool-unregistered", this._toolUnregistered); michael@0: } michael@0: exports.Toolbox = Toolbox; michael@0: michael@0: /** michael@0: * The toolbox can be 'hosted' either embedded in a browser window michael@0: * or in a separate window. michael@0: */ michael@0: Toolbox.HostType = { michael@0: BOTTOM: "bottom", michael@0: SIDE: "side", michael@0: WINDOW: "window", michael@0: CUSTOM: "custom" michael@0: }; michael@0: michael@0: Toolbox.prototype = { michael@0: _URL: "chrome://browser/content/devtools/framework/toolbox.xul", michael@0: michael@0: _prefs: { michael@0: LAST_HOST: "devtools.toolbox.host", michael@0: LAST_TOOL: "devtools.toolbox.selectedTool", michael@0: SIDE_ENABLED: "devtools.toolbox.sideEnabled" michael@0: }, michael@0: michael@0: currentToolId: null, michael@0: michael@0: /** michael@0: * Returns a *copy* of the _toolPanels collection. michael@0: * michael@0: * @return {Map} panels michael@0: * All the running panels in the toolbox michael@0: */ michael@0: getToolPanels: function() { michael@0: return new Map(this._toolPanels); michael@0: }, michael@0: michael@0: /** michael@0: * Access the panel for a given tool michael@0: */ michael@0: getPanel: function(id) { michael@0: return this._toolPanels.get(id); michael@0: }, michael@0: michael@0: /** michael@0: * This is a shortcut for getPanel(currentToolId) because it is much more michael@0: * likely that we're going to want to get the panel that we've just made michael@0: * visible michael@0: */ michael@0: getCurrentPanel: function() { michael@0: return this._toolPanels.get(this.currentToolId); michael@0: }, michael@0: michael@0: /** michael@0: * Get/alter the target of a Toolbox so we're debugging something different. michael@0: * See Target.jsm for more details. michael@0: * TODO: Do we allow |toolbox.target = null;| ? michael@0: */ michael@0: get target() { michael@0: return this._target; michael@0: }, michael@0: michael@0: /** michael@0: * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate michael@0: * tab. See HostType for more details. michael@0: */ michael@0: get hostType() { michael@0: return this._host.type; michael@0: }, michael@0: michael@0: /** michael@0: * Get the iframe containing the toolbox UI. michael@0: */ michael@0: get frame() { michael@0: return this._host.frame; michael@0: }, michael@0: michael@0: /** michael@0: * Shortcut to the document containing the toolbox UI michael@0: */ michael@0: get doc() { michael@0: return this.frame.contentDocument; michael@0: }, michael@0: michael@0: /** michael@0: * Get current zoom level of toolbox michael@0: */ michael@0: get zoomValue() { michael@0: return parseFloat(Services.prefs.getCharPref(ZOOM_PREF)); michael@0: }, michael@0: michael@0: /** michael@0: * Get the toolbox highlighter front. Note that it may not always have been michael@0: * initialized first. Use `initInspector()` if needed. michael@0: */ michael@0: get highlighter() { michael@0: if (this.highlighterUtils.isRemoteHighlightable) { michael@0: return this._highlighter; michael@0: } else { michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Get the toolbox's inspector front. Note that it may not always have been michael@0: * initialized first. Use `initInspector()` if needed. michael@0: */ michael@0: get inspector() { michael@0: return this._inspector; michael@0: }, michael@0: michael@0: /** michael@0: * Get the toolbox's walker front. Note that it may not always have been michael@0: * initialized first. Use `initInspector()` if needed. michael@0: */ michael@0: get walker() { michael@0: return this._walker; michael@0: }, michael@0: michael@0: /** michael@0: * Get the toolbox's node selection. Note that it may not always have been michael@0: * initialized first. Use `initInspector()` if needed. michael@0: */ michael@0: get selection() { michael@0: return this._selection; michael@0: }, michael@0: michael@0: /** michael@0: * Get the toggled state of the split console michael@0: */ michael@0: get splitConsole() { michael@0: return this._splitConsole; michael@0: }, michael@0: michael@0: /** michael@0: * Open the toolbox michael@0: */ michael@0: open: function() { michael@0: let deferred = promise.defer(); michael@0: michael@0: return this._host.create().then(iframe => { michael@0: let deferred = promise.defer(); michael@0: michael@0: let domReady = () => { michael@0: this.isReady = true; michael@0: michael@0: let closeButton = this.doc.getElementById("toolbox-close"); michael@0: closeButton.addEventListener("command", this.destroy, true); michael@0: michael@0: this._buildDockButtons(); michael@0: this._buildOptions(); michael@0: this._buildTabs(); michael@0: this._buildButtons(); michael@0: this._addKeysToWindow(); michael@0: this._addToolSwitchingKeys(); michael@0: this._addZoomKeys(); michael@0: this._loadInitialZoom(); michael@0: michael@0: this._telemetry.toolOpened("toolbox"); michael@0: michael@0: this.selectTool(this._defaultToolId).then(panel => { michael@0: this.emit("ready"); michael@0: deferred.resolve(); michael@0: }); michael@0: }; michael@0: michael@0: // Load the toolbox-level actor fronts and utilities now michael@0: this._target.makeRemote().then(() => { michael@0: iframe.setAttribute("src", this._URL); michael@0: let domHelper = new DOMHelpers(iframe.contentWindow); michael@0: domHelper.onceDOMReady(domReady); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }); michael@0: }, michael@0: michael@0: _buildOptions: function() { michael@0: let key = this.doc.getElementById("toolbox-options-key"); michael@0: key.addEventListener("command", () => { michael@0: this.selectTool("options"); michael@0: }, true); michael@0: }, michael@0: michael@0: _isResponsiveModeActive: function() { michael@0: let responsiveModeActive = false; michael@0: if (this.target.isLocalTab) { michael@0: let tab = this.target.tab; michael@0: let browserWindow = tab.ownerDocument.defaultView; michael@0: let responsiveUIManager = browserWindow.ResponsiveUI.ResponsiveUIManager; michael@0: responsiveModeActive = responsiveUIManager.isActiveForTab(tab); michael@0: } michael@0: return responsiveModeActive; michael@0: }, michael@0: michael@0: _splitConsoleOnKeypress: function(e) { michael@0: let responsiveModeActive = this._isResponsiveModeActive(); michael@0: if (e.keyCode === e.DOM_VK_ESCAPE && !responsiveModeActive) { michael@0: this.toggleSplitConsole(); michael@0: } michael@0: }, michael@0: michael@0: _addToolSwitchingKeys: function() { michael@0: let nextKey = this.doc.getElementById("toolbox-next-tool-key"); michael@0: nextKey.addEventListener("command", this.selectNextTool.bind(this), true); michael@0: let prevKey = this.doc.getElementById("toolbox-previous-tool-key"); michael@0: prevKey.addEventListener("command", this.selectPreviousTool.bind(this), true); michael@0: michael@0: // Split console uses keypress instead of command so the event can be michael@0: // cancelled with stopPropagation on the keypress, and not preventDefault. michael@0: this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false); michael@0: }, michael@0: michael@0: /** michael@0: * Make sure that the console is showing up properly based on all the michael@0: * possible conditions. michael@0: * 1) If the console tab is selected, then regardless of split state michael@0: * it should take up the full height of the deck, and we should michael@0: * hide the deck and splitter. michael@0: * 2) If the console tab is not selected and it is split, then we should michael@0: * show the splitter, deck, and console. michael@0: * 3) If the console tab is not selected and it is *not* split, michael@0: * then we should hide the console and splitter, and show the deck michael@0: * at full height. michael@0: */ michael@0: _refreshConsoleDisplay: function() { michael@0: let deck = this.doc.getElementById("toolbox-deck"); michael@0: let webconsolePanel = this.doc.getElementById("toolbox-panel-webconsole"); michael@0: let splitter = this.doc.getElementById("toolbox-console-splitter"); michael@0: let openedConsolePanel = this.currentToolId === "webconsole"; michael@0: michael@0: if (openedConsolePanel) { michael@0: deck.setAttribute("collapsed", "true"); michael@0: splitter.setAttribute("hidden", "true"); michael@0: webconsolePanel.removeAttribute("collapsed"); michael@0: } else { michael@0: deck.removeAttribute("collapsed"); michael@0: if (this._splitConsole) { michael@0: webconsolePanel.removeAttribute("collapsed"); michael@0: splitter.removeAttribute("hidden"); michael@0: } else { michael@0: webconsolePanel.setAttribute("collapsed", "true"); michael@0: splitter.setAttribute("hidden", "true"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Wire up the listeners for the zoom keys. michael@0: */ michael@0: _addZoomKeys: function() { michael@0: let inKey = this.doc.getElementById("toolbox-zoom-in-key"); michael@0: inKey.addEventListener("command", this.zoomIn.bind(this), true); michael@0: michael@0: let inKey2 = this.doc.getElementById("toolbox-zoom-in-key2"); michael@0: inKey2.addEventListener("command", this.zoomIn.bind(this), true); michael@0: michael@0: let outKey = this.doc.getElementById("toolbox-zoom-out-key"); michael@0: outKey.addEventListener("command", this.zoomOut.bind(this), true); michael@0: michael@0: let resetKey = this.doc.getElementById("toolbox-zoom-reset-key"); michael@0: resetKey.addEventListener("command", this.zoomReset.bind(this), true); michael@0: }, michael@0: michael@0: /** michael@0: * Set zoom on toolbox to whatever the last setting was. michael@0: */ michael@0: _loadInitialZoom: function() { michael@0: this.setZoom(this.zoomValue); michael@0: }, michael@0: michael@0: /** michael@0: * Increase zoom level of toolbox window - make things bigger. michael@0: */ michael@0: zoomIn: function() { michael@0: this.setZoom(this.zoomValue + 0.1); michael@0: }, michael@0: michael@0: /** michael@0: * Decrease zoom level of toolbox window - make things smaller. michael@0: */ michael@0: zoomOut: function() { michael@0: this.setZoom(this.zoomValue - 0.1); michael@0: }, michael@0: michael@0: /** michael@0: * Reset zoom level of the toolbox window. michael@0: */ michael@0: zoomReset: function() { michael@0: this.setZoom(1); michael@0: }, michael@0: michael@0: /** michael@0: * Set zoom level of the toolbox window. michael@0: * michael@0: * @param {number} zoomValue michael@0: * Zoom level e.g. 1.2 michael@0: */ michael@0: setZoom: function(zoomValue) { michael@0: // cap zoom value michael@0: zoomValue = Math.max(zoomValue, MIN_ZOOM); michael@0: zoomValue = Math.min(zoomValue, MAX_ZOOM); michael@0: michael@0: let contViewer = this.frame.docShell.contentViewer; michael@0: let docViewer = contViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); michael@0: michael@0: docViewer.fullZoom = zoomValue; michael@0: michael@0: Services.prefs.setCharPref(ZOOM_PREF, zoomValue); michael@0: }, michael@0: michael@0: /** michael@0: * Adds the keys and commands to the Toolbox Window in window mode. michael@0: */ michael@0: _addKeysToWindow: function() { michael@0: if (this.hostType != Toolbox.HostType.WINDOW) { michael@0: return; michael@0: } michael@0: michael@0: let doc = this.doc.defaultView.parent.document; michael@0: michael@0: for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) { michael@0: // Prevent multiple entries for the same tool. michael@0: if (!toolDefinition.key || doc.getElementById("key_" + id)) { michael@0: continue; michael@0: } michael@0: michael@0: let toolId = id; michael@0: let key = doc.createElement("key"); michael@0: michael@0: key.id = "key_" + toolId; michael@0: michael@0: if (toolDefinition.key.startsWith("VK_")) { michael@0: key.setAttribute("keycode", toolDefinition.key); michael@0: } else { michael@0: key.setAttribute("key", toolDefinition.key); michael@0: } michael@0: michael@0: key.setAttribute("modifiers", toolDefinition.modifiers); michael@0: key.setAttribute("oncommand", "void(0);"); // needed. See bug 371900 michael@0: key.addEventListener("command", () => { michael@0: this.selectTool(toolId).then(() => this.fireCustomKey(toolId)); michael@0: }, true); michael@0: doc.getElementById("toolbox-keyset").appendChild(key); michael@0: } michael@0: michael@0: // Add key for toggling the browser console from the detached window michael@0: if (!doc.getElementById("key_browserconsole")) { michael@0: let key = doc.createElement("key"); michael@0: key.id = "key_browserconsole"; michael@0: michael@0: key.setAttribute("key", toolboxStrings("browserConsoleCmd.commandkey")); michael@0: key.setAttribute("modifiers", "accel,shift"); michael@0: key.setAttribute("oncommand", "void(0)"); // needed. See bug 371900 michael@0: key.addEventListener("command", () => { michael@0: HUDService.toggleBrowserConsole(); michael@0: }, true); michael@0: doc.getElementById("toolbox-keyset").appendChild(key); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle any custom key events. Returns true if there was a custom key binding run michael@0: * @param {string} toolId michael@0: * Which tool to run the command on (skip if not current) michael@0: */ michael@0: fireCustomKey: function(toolId) { michael@0: let toolDefinition = gDevTools.getToolDefinition(toolId); michael@0: michael@0: if (toolDefinition.onkey && michael@0: ((this.currentToolId === toolId) || michael@0: (toolId == "webconsole" && this.splitConsole))) { michael@0: toolDefinition.onkey(this.getCurrentPanel(), this); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Build the buttons for changing hosts. Called every time michael@0: * the host changes. michael@0: */ michael@0: _buildDockButtons: function() { michael@0: let dockBox = this.doc.getElementById("toolbox-dock-buttons"); michael@0: michael@0: while (dockBox.firstChild) { michael@0: dockBox.removeChild(dockBox.firstChild); michael@0: } michael@0: michael@0: if (!this._target.isLocalTab) { michael@0: return; michael@0: } michael@0: michael@0: let closeButton = this.doc.getElementById("toolbox-close"); michael@0: if (this.hostType == Toolbox.HostType.WINDOW) { michael@0: closeButton.setAttribute("hidden", "true"); michael@0: } else { michael@0: closeButton.removeAttribute("hidden"); michael@0: } michael@0: michael@0: let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED); michael@0: michael@0: for (let type in Toolbox.HostType) { michael@0: let position = Toolbox.HostType[type]; michael@0: if (position == this.hostType || michael@0: position == Toolbox.HostType.CUSTOM || michael@0: (!sideEnabled && position == Toolbox.HostType.SIDE)) { michael@0: continue; michael@0: } michael@0: michael@0: let button = this.doc.createElement("toolbarbutton"); michael@0: button.id = "toolbox-dock-" + position; michael@0: button.className = "toolbox-dock-button"; michael@0: button.setAttribute("tooltiptext", toolboxStrings("toolboxDockButtons." + michael@0: position + ".tooltip")); michael@0: button.addEventListener("command", () => { michael@0: this.switchHost(position); michael@0: }); michael@0: michael@0: dockBox.appendChild(button); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add tabs to the toolbox UI for registered tools michael@0: */ michael@0: _buildTabs: function() { michael@0: for (let definition of gDevTools.getToolDefinitionArray()) { michael@0: this._buildTabForTool(definition); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref michael@0: */ michael@0: _buildButtons: function() { michael@0: this._buildPickerButton(); michael@0: michael@0: if (!this.target.isLocalTab) { michael@0: return; michael@0: } michael@0: michael@0: let spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec"); michael@0: let environment = CommandUtils.createEnvironment(this, '_target'); michael@0: this._requisition = CommandUtils.createRequisition(environment); michael@0: let buttons = CommandUtils.createButtons(spec, this._target, michael@0: this.doc, this._requisition); michael@0: let container = this.doc.getElementById("toolbox-buttons"); michael@0: buttons.forEach(container.appendChild.bind(container)); michael@0: this.setToolboxButtonsVisibility(); michael@0: }, michael@0: michael@0: /** michael@0: * Adding the element picker button is done here unlike the other buttons michael@0: * since we want it to work for remote targets too michael@0: */ michael@0: _buildPickerButton: function() { michael@0: this._pickerButton = this.doc.createElement("toolbarbutton"); michael@0: this._pickerButton.id = "command-button-pick"; michael@0: this._pickerButton.className = "command-button command-button-invertable"; michael@0: this._pickerButton.setAttribute("tooltiptext", toolboxStrings("pickButton.tooltip")); michael@0: michael@0: let container = this.doc.querySelector("#toolbox-buttons"); michael@0: container.appendChild(this._pickerButton); michael@0: michael@0: this._togglePicker = this.highlighterUtils.togglePicker.bind(this.highlighterUtils); michael@0: this._pickerButton.addEventListener("command", this._togglePicker, false); michael@0: }, michael@0: michael@0: /** michael@0: * Return all toolbox buttons (command buttons, plus any others that were michael@0: * added manually). michael@0: */ michael@0: get toolboxButtons() { michael@0: // White-list buttons that can be toggled to prevent adding prefs for michael@0: // addons that have manually inserted toolbarbuttons into DOM. michael@0: return [ michael@0: "command-button-pick", michael@0: "command-button-splitconsole", michael@0: "command-button-responsive", michael@0: "command-button-paintflashing", michael@0: "command-button-tilt", michael@0: "command-button-scratchpad", michael@0: "command-button-eyedropper" michael@0: ].map(id => { michael@0: let button = this.doc.getElementById(id); michael@0: // Some buttons may not exist inside of Browser Toolbox michael@0: if (!button) { michael@0: return false; michael@0: } michael@0: return { michael@0: id: id, michael@0: button: button, michael@0: label: button.getAttribute("tooltiptext"), michael@0: visibilityswitch: "devtools." + id + ".enabled" michael@0: } michael@0: }).filter(button=>button); michael@0: }, michael@0: michael@0: /** michael@0: * Ensure the visibility of each toolbox button matches the michael@0: * preference value. Simply hide buttons that are preffed off. michael@0: */ michael@0: setToolboxButtonsVisibility: function() { michael@0: this.toolboxButtons.forEach(buttonSpec => { michael@0: let {visibilityswitch, id, button}=buttonSpec; michael@0: let on = true; michael@0: try { michael@0: on = Services.prefs.getBoolPref(visibilityswitch); michael@0: } catch (ex) { } michael@0: michael@0: if (button) { michael@0: if (on) { michael@0: button.removeAttribute("hidden"); michael@0: } else { michael@0: button.setAttribute("hidden", "true"); michael@0: } michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Build a tab for one tool definition and add to the toolbox michael@0: * michael@0: * @param {string} toolDefinition michael@0: * Tool definition of the tool to build a tab for. michael@0: */ michael@0: _buildTabForTool: function(toolDefinition) { michael@0: if (!toolDefinition.isTargetSupported(this._target)) { michael@0: return; michael@0: } michael@0: michael@0: let tabs = this.doc.getElementById("toolbox-tabs"); michael@0: let deck = this.doc.getElementById("toolbox-deck"); michael@0: michael@0: let id = toolDefinition.id; michael@0: michael@0: if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) { michael@0: toolDefinition.ordinal = MAX_ORDINAL; michael@0: } michael@0: michael@0: let radio = this.doc.createElement("radio"); michael@0: // The radio element is not being used in the conventional way, thus michael@0: // the devtools-tab class replaces the radio XBL binding with its base michael@0: // binding (the control-item binding). michael@0: radio.className = "devtools-tab"; michael@0: radio.id = "toolbox-tab-" + id; michael@0: radio.setAttribute("toolid", id); michael@0: radio.setAttribute("ordinal", toolDefinition.ordinal); michael@0: radio.setAttribute("tooltiptext", toolDefinition.tooltip); michael@0: if (toolDefinition.invertIconForLightTheme) { michael@0: radio.setAttribute("icon-invertable", "true"); michael@0: } michael@0: michael@0: radio.addEventListener("command", () => { michael@0: this.selectTool(id); michael@0: }); michael@0: michael@0: // spacer lets us center the image and label, while allowing cropping michael@0: let spacer = this.doc.createElement("spacer"); michael@0: spacer.setAttribute("flex", "1"); michael@0: radio.appendChild(spacer); michael@0: michael@0: if (toolDefinition.icon) { michael@0: let image = this.doc.createElement("image"); michael@0: image.className = "default-icon"; michael@0: image.setAttribute("src", michael@0: toolDefinition.icon || toolDefinition.highlightedicon); michael@0: radio.appendChild(image); michael@0: // Adding the highlighted icon image michael@0: image = this.doc.createElement("image"); michael@0: image.className = "highlighted-icon"; michael@0: image.setAttribute("src", michael@0: toolDefinition.highlightedicon || toolDefinition.icon); michael@0: radio.appendChild(image); michael@0: } michael@0: michael@0: if (toolDefinition.label) { michael@0: let label = this.doc.createElement("label"); michael@0: label.setAttribute("value", toolDefinition.label) michael@0: label.setAttribute("crop", "end"); michael@0: label.setAttribute("flex", "1"); michael@0: radio.appendChild(label); michael@0: radio.setAttribute("flex", "1"); michael@0: } michael@0: michael@0: if (!toolDefinition.bgTheme) { michael@0: toolDefinition.bgTheme = "theme-toolbar"; michael@0: } michael@0: let vbox = this.doc.createElement("vbox"); michael@0: vbox.className = "toolbox-panel " + toolDefinition.bgTheme; michael@0: michael@0: // There is already a container for the webconsole frame. michael@0: if (!this.doc.getElementById("toolbox-panel-" + id)) { michael@0: vbox.id = "toolbox-panel-" + id; michael@0: } michael@0: michael@0: // If there is no tab yet, or the ordinal to be added is the largest one. michael@0: if (tabs.childNodes.length == 0 || michael@0: +tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) { michael@0: tabs.appendChild(radio); michael@0: deck.appendChild(vbox); michael@0: } else { michael@0: // else, iterate over all the tabs to get the correct location. michael@0: Array.some(tabs.childNodes, (node, i) => { michael@0: if (+node.getAttribute("ordinal") > toolDefinition.ordinal) { michael@0: tabs.insertBefore(radio, node); michael@0: deck.insertBefore(vbox, deck.childNodes[i]); michael@0: return true; michael@0: } michael@0: return false; michael@0: }); michael@0: } michael@0: michael@0: this._addKeysToWindow(); michael@0: }, michael@0: michael@0: /** michael@0: * Ensure the tool with the given id is loaded. michael@0: * michael@0: * @param {string} id michael@0: * The id of the tool to load. michael@0: */ michael@0: loadTool: function(id) { michael@0: if (id === "inspector" && !this._inspector) { michael@0: return this.initInspector().then(() => { michael@0: return this.loadTool(id); michael@0: }); michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); michael@0: michael@0: if (iframe) { michael@0: let panel = this._toolPanels.get(id); michael@0: if (panel) { michael@0: deferred.resolve(panel); michael@0: } else { michael@0: this.once(id + "-ready", panel => { michael@0: deferred.resolve(panel); michael@0: }); michael@0: } michael@0: return deferred.promise; michael@0: } michael@0: michael@0: let definition = gDevTools.getToolDefinition(id); michael@0: if (!definition) { michael@0: deferred.reject(new Error("no such tool id "+id)); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: iframe = this.doc.createElement("iframe"); michael@0: iframe.className = "toolbox-panel-iframe"; michael@0: iframe.id = "toolbox-panel-iframe-" + id; michael@0: iframe.setAttribute("flex", 1); michael@0: iframe.setAttribute("forceOwnRefreshDriver", ""); michael@0: iframe.tooltip = "aHTMLTooltip"; michael@0: iframe.style.visibility = "hidden"; michael@0: michael@0: let vbox = this.doc.getElementById("toolbox-panel-" + id); michael@0: vbox.appendChild(iframe); michael@0: michael@0: let onLoad = () => { michael@0: // Prevent flicker while loading by waiting to make visible until now. michael@0: iframe.style.visibility = "visible"; michael@0: michael@0: let built = definition.build(iframe.contentWindow, this); michael@0: promise.resolve(built).then((panel) => { michael@0: this._toolPanels.set(id, panel); michael@0: this.emit(id + "-ready", panel); michael@0: gDevTools.emit(id + "-ready", this, panel); michael@0: deferred.resolve(panel); michael@0: }, console.error); michael@0: }; michael@0: michael@0: iframe.setAttribute("src", definition.url); michael@0: michael@0: // Depending on the host, iframe.contentWindow is not always michael@0: // defined at this moment. If it is not defined, we use an michael@0: // event listener on the iframe DOM node. If it's defined, michael@0: // we use the chromeEventHandler. We can't use a listener michael@0: // on the DOM node every time because this won't work michael@0: // if the (xul chrome) iframe is loaded in a content docshell. michael@0: if (iframe.contentWindow) { michael@0: let domHelper = new DOMHelpers(iframe.contentWindow); michael@0: domHelper.onceDOMReady(onLoad); michael@0: } else { michael@0: let callback = () => { michael@0: iframe.removeEventListener("DOMContentLoaded", callback); michael@0: onLoad(); michael@0: } michael@0: iframe.addEventListener("DOMContentLoaded", callback); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Switch to the tool with the given id michael@0: * michael@0: * @param {string} id michael@0: * The id of the tool to switch to michael@0: */ michael@0: selectTool: function(id) { michael@0: let selected = this.doc.querySelector(".devtools-tab[selected]"); michael@0: if (selected) { michael@0: selected.removeAttribute("selected"); michael@0: } michael@0: michael@0: let tab = this.doc.getElementById("toolbox-tab-" + id); michael@0: tab.setAttribute("selected", "true"); michael@0: michael@0: if (this.currentToolId == id) { michael@0: // re-focus tool to get key events again michael@0: this.focusTool(id); michael@0: michael@0: // Return the existing panel in order to have a consistent return value. michael@0: return promise.resolve(this._toolPanels.get(id)); michael@0: } michael@0: michael@0: if (!this.isReady) { michael@0: throw new Error("Can't select tool, wait for toolbox 'ready' event"); michael@0: } michael@0: michael@0: tab = this.doc.getElementById("toolbox-tab-" + id); michael@0: michael@0: if (tab) { michael@0: if (this.currentToolId) { michael@0: this._telemetry.toolClosed(this.currentToolId); michael@0: } michael@0: this._telemetry.toolOpened(id); michael@0: } else { michael@0: throw new Error("No tool found"); michael@0: } michael@0: michael@0: let tabstrip = this.doc.getElementById("toolbox-tabs"); michael@0: michael@0: // select the right tab, making 0th index the default tab if right tab not michael@0: // found michael@0: let index = 0; michael@0: let tabs = tabstrip.childNodes; michael@0: for (let i = 0; i < tabs.length; i++) { michael@0: if (tabs[i] === tab) { michael@0: index = i; michael@0: break; michael@0: } michael@0: } michael@0: tabstrip.selectedItem = tab; michael@0: michael@0: // and select the right iframe michael@0: let deck = this.doc.getElementById("toolbox-deck"); michael@0: deck.selectedIndex = index; michael@0: michael@0: this.currentToolId = id; michael@0: this._refreshConsoleDisplay(); michael@0: if (id != "options") { michael@0: Services.prefs.setCharPref(this._prefs.LAST_TOOL, id); michael@0: } michael@0: michael@0: return this.loadTool(id).then(panel => { michael@0: // focus the tool's frame to start receiving key events michael@0: this.focusTool(id); michael@0: michael@0: this.emit("select", id); michael@0: this.emit(id + "-selected", panel); michael@0: return panel; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Focus a tool's panel by id michael@0: * @param {string} id michael@0: * The id of tool to focus michael@0: */ michael@0: focusTool: function(id) { michael@0: let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); michael@0: iframe.focus(); michael@0: }, michael@0: michael@0: /** michael@0: * Focus split console's input line michael@0: */ michael@0: focusConsoleInput: function() { michael@0: let hud = this.getPanel("webconsole").hud; michael@0: if (hud && hud.jsterm) { michael@0: hud.jsterm.inputNode.focus(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Toggles the split state of the webconsole. If the webconsole panel michael@0: * is already selected, then this command is ignored. michael@0: */ michael@0: toggleSplitConsole: function() { michael@0: let openedConsolePanel = this.currentToolId === "webconsole"; michael@0: michael@0: // Don't allow changes when console is open, since it could be confusing michael@0: if (!openedConsolePanel) { michael@0: this._splitConsole = !this._splitConsole; michael@0: this._refreshConsoleDisplay(); michael@0: this.emit("split-console"); michael@0: michael@0: if (this._splitConsole) { michael@0: this.loadTool("webconsole").then(() => { michael@0: this.focusConsoleInput(); michael@0: }); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Loads the tool next to the currently selected tool. michael@0: */ michael@0: selectNextTool: function() { michael@0: let selected = this.doc.querySelector(".devtools-tab[selected]"); michael@0: let next = selected.nextSibling || selected.parentNode.firstChild; michael@0: let tool = next.getAttribute("toolid"); michael@0: return this.selectTool(tool); michael@0: }, michael@0: michael@0: /** michael@0: * Loads the tool just left to the currently selected tool. michael@0: */ michael@0: selectPreviousTool: function() { michael@0: let selected = this.doc.querySelector(".devtools-tab[selected]"); michael@0: let previous = selected.previousSibling || selected.parentNode.lastChild; michael@0: let tool = previous.getAttribute("toolid"); michael@0: return this.selectTool(tool); michael@0: }, michael@0: michael@0: /** michael@0: * Highlights the tool's tab if it is not the currently selected tool. michael@0: * michael@0: * @param {string} id michael@0: * The id of the tool to highlight michael@0: */ michael@0: highlightTool: function(id) { michael@0: let tab = this.doc.getElementById("toolbox-tab-" + id); michael@0: tab && tab.setAttribute("highlighted", "true"); michael@0: }, michael@0: michael@0: /** michael@0: * De-highlights the tool's tab. michael@0: * michael@0: * @param {string} id michael@0: * The id of the tool to unhighlight michael@0: */ michael@0: unhighlightTool: function(id) { michael@0: let tab = this.doc.getElementById("toolbox-tab-" + id); michael@0: tab && tab.removeAttribute("highlighted"); michael@0: }, michael@0: michael@0: /** michael@0: * Raise the toolbox host. michael@0: */ michael@0: raise: function() { michael@0: this._host.raise(); michael@0: }, michael@0: michael@0: /** michael@0: * Refresh the host's title. michael@0: */ michael@0: _refreshHostTitle: function() { michael@0: let toolName; michael@0: let toolDef = gDevTools.getToolDefinition(this.currentToolId); michael@0: if (toolDef) { michael@0: toolName = toolDef.label; michael@0: } else { michael@0: // no tool is selected michael@0: toolName = toolboxStrings("toolbox.defaultTitle"); michael@0: } michael@0: let title = toolboxStrings("toolbox.titleTemplate", michael@0: toolName, this.target.url || this.target.name); michael@0: this._host.setTitle(title); michael@0: }, michael@0: michael@0: /** michael@0: * Create a host object based on the given host type. michael@0: * michael@0: * Warning: some hosts require that the toolbox target provides a reference to michael@0: * the attached tab. Not all Targets have a tab property - make sure you correctly michael@0: * mix and match hosts and targets. michael@0: * michael@0: * @param {string} hostType michael@0: * The host type of the new host object michael@0: * michael@0: * @return {Host} host michael@0: * The created host object michael@0: */ michael@0: _createHost: function(hostType, options) { michael@0: if (!Hosts[hostType]) { michael@0: throw new Error("Unknown hostType: " + hostType); michael@0: } michael@0: michael@0: // clean up the toolbox if its window is closed michael@0: let newHost = new Hosts[hostType](this.target.tab, options); michael@0: newHost.on("window-closed", this.destroy); michael@0: return newHost; michael@0: }, michael@0: michael@0: /** michael@0: * Switch to a new host for the toolbox UI. E.g. michael@0: * bottom, sidebar, separate window. michael@0: * michael@0: * @param {string} hostType michael@0: * The host type of the new host object michael@0: */ michael@0: switchHost: function(hostType) { michael@0: if (hostType == this._host.type || !this._target.isLocalTab) { michael@0: return null; michael@0: } michael@0: michael@0: let newHost = this._createHost(hostType); michael@0: return newHost.create().then(iframe => { michael@0: // change toolbox document's parent to the new host michael@0: iframe.QueryInterface(Ci.nsIFrameLoaderOwner); michael@0: iframe.swapFrameLoaders(this.frame); michael@0: michael@0: this._host.off("window-closed", this.destroy); michael@0: this.destroyHost(); michael@0: michael@0: this._host = newHost; michael@0: michael@0: if (this.hostType != Toolbox.HostType.CUSTOM) { michael@0: Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type); michael@0: } michael@0: michael@0: this._buildDockButtons(); michael@0: this._addKeysToWindow(); michael@0: michael@0: this.emit("host-changed"); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the tool-registered event. michael@0: * @param {string} event michael@0: * Name of the event ("tool-registered") michael@0: * @param {string} toolId michael@0: * Id of the tool that was registered michael@0: */ michael@0: _toolRegistered: function(event, toolId) { michael@0: let tool = gDevTools.getToolDefinition(toolId); michael@0: this._buildTabForTool(tool); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the tool-unregistered event. michael@0: * @param {string} event michael@0: * Name of the event ("tool-unregistered") michael@0: * @param {string|object} toolId michael@0: * Definition or id of the tool that was unregistered. Passing the michael@0: * tool id should be avoided as it is a temporary measure. michael@0: */ michael@0: _toolUnregistered: function(event, toolId) { michael@0: if (typeof toolId != "string") { michael@0: toolId = toolId.id; michael@0: } michael@0: michael@0: if (this._toolPanels.has(toolId)) { michael@0: let instance = this._toolPanels.get(toolId); michael@0: instance.destroy(); michael@0: this._toolPanels.delete(toolId); michael@0: } michael@0: michael@0: let radio = this.doc.getElementById("toolbox-tab-" + toolId); michael@0: let panel = this.doc.getElementById("toolbox-panel-" + toolId); michael@0: michael@0: if (radio) { michael@0: if (this.currentToolId == toolId) { michael@0: let nextToolName = null; michael@0: if (radio.nextSibling) { michael@0: nextToolName = radio.nextSibling.getAttribute("toolid"); michael@0: } michael@0: if (radio.previousSibling) { michael@0: nextToolName = radio.previousSibling.getAttribute("toolid"); michael@0: } michael@0: if (nextToolName) { michael@0: this.selectTool(nextToolName); michael@0: } michael@0: } michael@0: radio.parentNode.removeChild(radio); michael@0: } michael@0: michael@0: if (panel) { michael@0: panel.parentNode.removeChild(panel); michael@0: } michael@0: michael@0: if (this.hostType == Toolbox.HostType.WINDOW) { michael@0: let doc = this.doc.defaultView.parent.document; michael@0: let key = doc.getElementById("key_" + toolId); michael@0: if (key) { michael@0: key.parentNode.removeChild(key); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Initialize the inspector/walker/selection/highlighter fronts. michael@0: * Returns a promise that resolves when the fronts are initialized michael@0: */ michael@0: initInspector: function() { michael@0: if (!this._initInspector) { michael@0: this._initInspector = Task.spawn(function*() { michael@0: this._inspector = InspectorFront(this._target.client, this._target.form); michael@0: this._walker = yield this._inspector.getWalker(); michael@0: this._selection = new Selection(this._walker); michael@0: michael@0: if (this.highlighterUtils.isRemoteHighlightable) { michael@0: let autohide = !gDevTools.testing; michael@0: michael@0: this.walker.on("highlighter-ready", this._highlighterReady); michael@0: this.walker.on("highlighter-hide", this._highlighterHidden); michael@0: michael@0: this._highlighter = yield this._inspector.getHighlighter(autohide); michael@0: } michael@0: }.bind(this)); michael@0: } michael@0: return this._initInspector; michael@0: }, michael@0: michael@0: /** michael@0: * Destroy the inspector/walker/selection fronts michael@0: * Returns a promise that resolves when the fronts are destroyed michael@0: */ michael@0: destroyInspector: function() { michael@0: if (this._destroying) { michael@0: return this._destroying; michael@0: } michael@0: michael@0: if (!this._inspector) { michael@0: return promise.resolve(); michael@0: } michael@0: michael@0: let outstanding = () => { michael@0: return Task.spawn(function*() { michael@0: yield this.highlighterUtils.stopPicker(); michael@0: yield this._inspector.destroy(); michael@0: if (this._highlighter) { michael@0: yield this._highlighter.destroy(); michael@0: } michael@0: if (this._selection) { michael@0: this._selection.destroy(); michael@0: } michael@0: michael@0: if (this.walker) { michael@0: this.walker.off("highlighter-ready", this._highlighterReady); michael@0: this.walker.off("highlighter-hide", this._highlighterHidden); michael@0: } michael@0: michael@0: this._inspector = null; michael@0: this._highlighter = null; michael@0: this._selection = null; michael@0: this._walker = null; michael@0: }.bind(this)); michael@0: }; michael@0: michael@0: // Releasing the walker (if it has been created) michael@0: // This can fail, but in any case, we want to continue destroying the michael@0: // inspector/highlighter/selection michael@0: let walker = (this._destroying = this._walker) ? michael@0: this._walker.release() : michael@0: promise.resolve(); michael@0: return walker.then(outstanding, outstanding); michael@0: }, michael@0: michael@0: /** michael@0: * Get the toolbox's notification box michael@0: * michael@0: * @return The notification box element. michael@0: */ michael@0: getNotificationBox: function() { michael@0: return this.doc.getElementById("toolbox-notificationbox"); michael@0: }, michael@0: michael@0: /** michael@0: * Destroy the current host, and remove event listeners from its frame. michael@0: * michael@0: * @return {promise} to be resolved when the host is destroyed. michael@0: */ michael@0: destroyHost: function() { michael@0: this.doc.removeEventListener("keypress", michael@0: this._splitConsoleOnKeypress, false); michael@0: return this._host.destroy(); michael@0: }, michael@0: michael@0: /** michael@0: * Remove all UI elements, detach from target and clear up michael@0: */ michael@0: destroy: function() { michael@0: // If several things call destroy then we give them all the same michael@0: // destruction promise so we're sure to destroy only once michael@0: if (this._destroyer) { michael@0: return this._destroyer; michael@0: } michael@0: michael@0: this._target.off("navigate", this._refreshHostTitle); michael@0: this.off("select", this._refreshHostTitle); michael@0: this.off("host-changed", this._refreshHostTitle); michael@0: michael@0: gDevTools.off("tool-registered", this._toolRegistered); michael@0: gDevTools.off("tool-unregistered", this._toolUnregistered); michael@0: michael@0: let outstanding = []; michael@0: for (let [id, panel] of this._toolPanels) { michael@0: try { michael@0: outstanding.push(panel.destroy()); michael@0: } catch (e) { michael@0: // We don't want to stop here if any panel fail to close. michael@0: console.error("Panel " + id + ":", e); michael@0: } michael@0: } michael@0: michael@0: // Destroying the walker and inspector fronts michael@0: outstanding.push(this.destroyInspector()); michael@0: // Removing buttons michael@0: outstanding.push(() => { michael@0: this._pickerButton.removeEventListener("command", this._togglePicker, false); michael@0: this._pickerButton = null; michael@0: let container = this.doc.getElementById("toolbox-buttons"); michael@0: while (container.firstChild) { michael@0: container.removeChild(container.firstChild); michael@0: } michael@0: }); michael@0: // Remove the host UI michael@0: outstanding.push(this.destroyHost()); michael@0: michael@0: if (this.target.isLocalTab) { michael@0: this._requisition.destroy(); michael@0: } michael@0: this._telemetry.destroy(); michael@0: michael@0: return this._destroyer = promise.all(outstanding).then(() => { michael@0: // Targets need to be notified that the toolbox is being torn down. michael@0: // This is done after other destruction tasks since it may tear down michael@0: // fronts and the debugger transport which earlier destroy methods may michael@0: // require to complete. michael@0: if (!this._target) { michael@0: return null; michael@0: } michael@0: let target = this._target; michael@0: this._target = null; michael@0: target.off("close", this.destroy); michael@0: return target.destroy(); michael@0: }).then(() => { michael@0: this.emit("destroyed"); michael@0: // Free _host after the call to destroyed in order to let a chance michael@0: // to destroyed listeners to still query toolbox attributes michael@0: this._host = null; michael@0: this._toolPanels.clear(); michael@0: }).then(null, console.error); michael@0: }, michael@0: michael@0: _highlighterReady: function() { michael@0: this.emit("highlighter-ready"); michael@0: }, michael@0: michael@0: _highlighterHidden: function() { michael@0: this.emit("highlighter-hide"); michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * The ToolboxHighlighterUtils is what you should use for anything related to michael@0: * node highlighting and picking. michael@0: * It encapsulates the logic to connecting to the HighlighterActor. michael@0: */ michael@0: function ToolboxHighlighterUtils(toolbox) { michael@0: this.toolbox = toolbox; michael@0: this._onPickerNodeHovered = this._onPickerNodeHovered.bind(this); michael@0: this._onPickerNodePicked = this._onPickerNodePicked.bind(this); michael@0: this.stopPicker = this.stopPicker.bind(this); michael@0: } michael@0: michael@0: ToolboxHighlighterUtils.prototype = { michael@0: /** michael@0: * Indicates whether the highlighter actor exists on the server. michael@0: */ michael@0: get isRemoteHighlightable() { michael@0: return this.toolbox._target.client.traits.highlightable; michael@0: }, michael@0: michael@0: /** michael@0: * Start/stop the element picker on the debuggee target. michael@0: */ michael@0: togglePicker: function() { michael@0: if (this._isPicking) { michael@0: return this.stopPicker(); michael@0: } else { michael@0: return this.startPicker(); michael@0: } michael@0: }, michael@0: michael@0: _onPickerNodeHovered: function(res) { michael@0: this.toolbox.emit("picker-node-hovered", res.node); michael@0: }, michael@0: michael@0: _onPickerNodePicked: function(res) { michael@0: this.toolbox.selection.setNodeFront(res.node, "picker-node-picked"); michael@0: this.stopPicker(); michael@0: }, michael@0: michael@0: /** michael@0: * Start the element picker on the debuggee target. michael@0: * This will request the inspector actor to start listening for mouse/touch michael@0: * events on the target to highlight the hovered/picked element. michael@0: * Depending on the server-side capabilities, this may fire events when nodes michael@0: * are hovered. michael@0: * @return A promise that resolves when the picker has started or immediately michael@0: * if it is already started michael@0: */ michael@0: startPicker: function() { michael@0: if (this._isPicking) { michael@0: return promise.resolve(); michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: let done = () => { michael@0: this._isPicking = true; michael@0: this.toolbox.emit("picker-started"); michael@0: this.toolbox.on("select", this.stopPicker); michael@0: deferred.resolve(); michael@0: }; michael@0: michael@0: promise.all([ michael@0: this.toolbox.initInspector(), michael@0: this.toolbox.selectTool("inspector") michael@0: ]).then(() => { michael@0: this.toolbox._pickerButton.setAttribute("checked", "true"); michael@0: michael@0: if (this.isRemoteHighlightable) { michael@0: this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered); michael@0: this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked); michael@0: michael@0: this.toolbox.highlighter.pick().then(done); michael@0: } else { michael@0: return this.toolbox.walker.pick().then(node => { michael@0: this.toolbox.selection.setNodeFront(node, "picker-node-picked").then(() => { michael@0: this.stopPicker(); michael@0: done(); michael@0: }); michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Stop the element picker michael@0: * @return A promise that resolves when the picker has stopped or immediately michael@0: * if it is already stopped michael@0: */ michael@0: stopPicker: function() { michael@0: if (!this._isPicking) { michael@0: return promise.resolve(); michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: let done = () => { michael@0: this.toolbox.emit("picker-stopped"); michael@0: this.toolbox.off("select", this.stopPicker); michael@0: deferred.resolve(); michael@0: }; michael@0: michael@0: this.toolbox.initInspector().then(() => { michael@0: this._isPicking = false; michael@0: this.toolbox._pickerButton.removeAttribute("checked"); michael@0: if (this.isRemoteHighlightable) { michael@0: this.toolbox.highlighter.cancelPick().then(done); michael@0: this.toolbox.walker.off("picker-node-hovered", this._onPickerNodeHovered); michael@0: this.toolbox.walker.off("picker-node-picked", this._onPickerNodePicked); michael@0: } else { michael@0: this.toolbox.walker.cancelPick().then(done); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Show the box model highlighter on a node, given its NodeFront (this type michael@0: * of front is normally returned by the WalkerActor). michael@0: * @return a promise that resolves to the nodeFront when the node has been michael@0: * highlit michael@0: */ michael@0: highlightNodeFront: function(nodeFront, options={}) { michael@0: let deferred = promise.defer(); michael@0: michael@0: // If the remote highlighter exists on the target, use it michael@0: if (this.isRemoteHighlightable) { michael@0: this.toolbox.initInspector().then(() => { michael@0: this.toolbox.highlighter.showBoxModel(nodeFront, options).then(() => { michael@0: this.toolbox.emit("node-highlight", nodeFront); michael@0: deferred.resolve(nodeFront); michael@0: }); michael@0: }); michael@0: } michael@0: // Else, revert to the "older" version of the highlighter in the walker michael@0: // actor michael@0: else { michael@0: this.toolbox.walker.highlight(nodeFront).then(() => { michael@0: this.toolbox.emit("node-highlight", nodeFront); michael@0: deferred.resolve(nodeFront); michael@0: }); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * This is a convenience method in case you don't have a nodeFront but a michael@0: * valueGrip. This is often the case with VariablesView properties. michael@0: * This method will simply translate the grip into a nodeFront and call michael@0: * highlightNodeFront michael@0: * @return a promise that resolves to the nodeFront when the node has been michael@0: * highlit michael@0: */ michael@0: highlightDomValueGrip: function(valueGrip, options={}) { michael@0: return this._translateGripToNodeFront(valueGrip).then(nodeFront => { michael@0: if (nodeFront) { michael@0: return this.highlightNodeFront(nodeFront, options); michael@0: } else { michael@0: return promise.reject(); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: _translateGripToNodeFront: function(grip) { michael@0: return this.toolbox.initInspector().then(() => { michael@0: return this.toolbox.walker.getNodeActorFromObjectActor(grip.actor); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Hide the highlighter. michael@0: * @return a promise that resolves when the highlighter is hidden michael@0: */ michael@0: unhighlight: function(forceHide=false) { michael@0: let unhighlightPromise; michael@0: forceHide = forceHide || !gDevTools.testing; michael@0: michael@0: if (forceHide && this.isRemoteHighlightable && this.toolbox.highlighter) { michael@0: // If the remote highlighter exists on the target, use it michael@0: unhighlightPromise = this.toolbox.highlighter.hideBoxModel(); michael@0: } else { michael@0: // If not, no need to unhighlight as the older highlight method uses a michael@0: // setTimeout to hide itself michael@0: unhighlightPromise = promise.resolve(); michael@0: } michael@0: michael@0: return unhighlightPromise.then(() => { michael@0: this.toolbox.emit("node-unhighlight"); michael@0: }); michael@0: } michael@0: };