1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/framework/toolbox.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1451 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const MAX_ORDINAL = 99; 1.11 +const ZOOM_PREF = "devtools.toolbox.zoomValue"; 1.12 +const MIN_ZOOM = 0.5; 1.13 +const MAX_ZOOM = 2; 1.14 + 1.15 +let {Cc, Ci, Cu} = require("chrome"); 1.16 +let {Promise: promise} = require("resource://gre/modules/Promise.jsm"); 1.17 +let EventEmitter = require("devtools/toolkit/event-emitter"); 1.18 +let Telemetry = require("devtools/shared/telemetry"); 1.19 +let HUDService = require("devtools/webconsole/hudservice"); 1.20 + 1.21 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.22 +Cu.import("resource://gre/modules/Services.jsm"); 1.23 +Cu.import("resource:///modules/devtools/gDevTools.jsm"); 1.24 +Cu.import("resource:///modules/devtools/scratchpad-manager.jsm"); 1.25 +Cu.import("resource:///modules/devtools/DOMHelpers.jsm"); 1.26 +Cu.import("resource://gre/modules/Task.jsm"); 1.27 + 1.28 +loader.lazyGetter(this, "Hosts", () => require("devtools/framework/toolbox-hosts").Hosts); 1.29 + 1.30 +loader.lazyImporter(this, "CommandUtils", "resource:///modules/devtools/DeveloperToolbar.jsm"); 1.31 + 1.32 +loader.lazyGetter(this, "toolboxStrings", () => { 1.33 + let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties"); 1.34 + return (name, ...args) => { 1.35 + try { 1.36 + if (!args.length) { 1.37 + return bundle.GetStringFromName(name); 1.38 + } 1.39 + return bundle.formatStringFromName(name, args, args.length); 1.40 + } catch (ex) { 1.41 + Services.console.logStringMessage("Error reading '" + name + "'"); 1.42 + return null; 1.43 + } 1.44 + }; 1.45 +}); 1.46 + 1.47 +loader.lazyGetter(this, "Selection", () => require("devtools/framework/selection").Selection); 1.48 +loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront); 1.49 + 1.50 +/** 1.51 + * A "Toolbox" is the component that holds all the tools for one specific 1.52 + * target. Visually, it's a document that includes the tools tabs and all 1.53 + * the iframes where the tool panels will be living in. 1.54 + * 1.55 + * @param {object} target 1.56 + * The object the toolbox is debugging. 1.57 + * @param {string} selectedTool 1.58 + * Tool to select initially 1.59 + * @param {Toolbox.HostType} hostType 1.60 + * Type of host that will host the toolbox (e.g. sidebar, window) 1.61 + * @param {object} hostOptions 1.62 + * Options for host specifically 1.63 + */ 1.64 +function Toolbox(target, selectedTool, hostType, hostOptions) { 1.65 + this._target = target; 1.66 + this._toolPanels = new Map(); 1.67 + this._telemetry = new Telemetry(); 1.68 + 1.69 + this._toolRegistered = this._toolRegistered.bind(this); 1.70 + this._toolUnregistered = this._toolUnregistered.bind(this); 1.71 + this._refreshHostTitle = this._refreshHostTitle.bind(this); 1.72 + this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this) 1.73 + this.destroy = this.destroy.bind(this); 1.74 + this.highlighterUtils = new ToolboxHighlighterUtils(this); 1.75 + this._highlighterReady = this._highlighterReady.bind(this); 1.76 + this._highlighterHidden = this._highlighterHidden.bind(this); 1.77 + 1.78 + this._target.on("close", this.destroy); 1.79 + 1.80 + if (!hostType) { 1.81 + hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST); 1.82 + } 1.83 + if (!selectedTool) { 1.84 + selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); 1.85 + } 1.86 + if (!gDevTools.getToolDefinition(selectedTool)) { 1.87 + selectedTool = "webconsole"; 1.88 + } 1.89 + this._defaultToolId = selectedTool; 1.90 + 1.91 + this._host = this._createHost(hostType, hostOptions); 1.92 + 1.93 + EventEmitter.decorate(this); 1.94 + 1.95 + this._target.on("navigate", this._refreshHostTitle); 1.96 + this.on("host-changed", this._refreshHostTitle); 1.97 + this.on("select", this._refreshHostTitle); 1.98 + 1.99 + gDevTools.on("tool-registered", this._toolRegistered); 1.100 + gDevTools.on("tool-unregistered", this._toolUnregistered); 1.101 +} 1.102 +exports.Toolbox = Toolbox; 1.103 + 1.104 +/** 1.105 + * The toolbox can be 'hosted' either embedded in a browser window 1.106 + * or in a separate window. 1.107 + */ 1.108 +Toolbox.HostType = { 1.109 + BOTTOM: "bottom", 1.110 + SIDE: "side", 1.111 + WINDOW: "window", 1.112 + CUSTOM: "custom" 1.113 +}; 1.114 + 1.115 +Toolbox.prototype = { 1.116 + _URL: "chrome://browser/content/devtools/framework/toolbox.xul", 1.117 + 1.118 + _prefs: { 1.119 + LAST_HOST: "devtools.toolbox.host", 1.120 + LAST_TOOL: "devtools.toolbox.selectedTool", 1.121 + SIDE_ENABLED: "devtools.toolbox.sideEnabled" 1.122 + }, 1.123 + 1.124 + currentToolId: null, 1.125 + 1.126 + /** 1.127 + * Returns a *copy* of the _toolPanels collection. 1.128 + * 1.129 + * @return {Map} panels 1.130 + * All the running panels in the toolbox 1.131 + */ 1.132 + getToolPanels: function() { 1.133 + return new Map(this._toolPanels); 1.134 + }, 1.135 + 1.136 + /** 1.137 + * Access the panel for a given tool 1.138 + */ 1.139 + getPanel: function(id) { 1.140 + return this._toolPanels.get(id); 1.141 + }, 1.142 + 1.143 + /** 1.144 + * This is a shortcut for getPanel(currentToolId) because it is much more 1.145 + * likely that we're going to want to get the panel that we've just made 1.146 + * visible 1.147 + */ 1.148 + getCurrentPanel: function() { 1.149 + return this._toolPanels.get(this.currentToolId); 1.150 + }, 1.151 + 1.152 + /** 1.153 + * Get/alter the target of a Toolbox so we're debugging something different. 1.154 + * See Target.jsm for more details. 1.155 + * TODO: Do we allow |toolbox.target = null;| ? 1.156 + */ 1.157 + get target() { 1.158 + return this._target; 1.159 + }, 1.160 + 1.161 + /** 1.162 + * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate 1.163 + * tab. See HostType for more details. 1.164 + */ 1.165 + get hostType() { 1.166 + return this._host.type; 1.167 + }, 1.168 + 1.169 + /** 1.170 + * Get the iframe containing the toolbox UI. 1.171 + */ 1.172 + get frame() { 1.173 + return this._host.frame; 1.174 + }, 1.175 + 1.176 + /** 1.177 + * Shortcut to the document containing the toolbox UI 1.178 + */ 1.179 + get doc() { 1.180 + return this.frame.contentDocument; 1.181 + }, 1.182 + 1.183 + /** 1.184 + * Get current zoom level of toolbox 1.185 + */ 1.186 + get zoomValue() { 1.187 + return parseFloat(Services.prefs.getCharPref(ZOOM_PREF)); 1.188 + }, 1.189 + 1.190 + /** 1.191 + * Get the toolbox highlighter front. Note that it may not always have been 1.192 + * initialized first. Use `initInspector()` if needed. 1.193 + */ 1.194 + get highlighter() { 1.195 + if (this.highlighterUtils.isRemoteHighlightable) { 1.196 + return this._highlighter; 1.197 + } else { 1.198 + return null; 1.199 + } 1.200 + }, 1.201 + 1.202 + /** 1.203 + * Get the toolbox's inspector front. Note that it may not always have been 1.204 + * initialized first. Use `initInspector()` if needed. 1.205 + */ 1.206 + get inspector() { 1.207 + return this._inspector; 1.208 + }, 1.209 + 1.210 + /** 1.211 + * Get the toolbox's walker front. Note that it may not always have been 1.212 + * initialized first. Use `initInspector()` if needed. 1.213 + */ 1.214 + get walker() { 1.215 + return this._walker; 1.216 + }, 1.217 + 1.218 + /** 1.219 + * Get the toolbox's node selection. Note that it may not always have been 1.220 + * initialized first. Use `initInspector()` if needed. 1.221 + */ 1.222 + get selection() { 1.223 + return this._selection; 1.224 + }, 1.225 + 1.226 + /** 1.227 + * Get the toggled state of the split console 1.228 + */ 1.229 + get splitConsole() { 1.230 + return this._splitConsole; 1.231 + }, 1.232 + 1.233 + /** 1.234 + * Open the toolbox 1.235 + */ 1.236 + open: function() { 1.237 + let deferred = promise.defer(); 1.238 + 1.239 + return this._host.create().then(iframe => { 1.240 + let deferred = promise.defer(); 1.241 + 1.242 + let domReady = () => { 1.243 + this.isReady = true; 1.244 + 1.245 + let closeButton = this.doc.getElementById("toolbox-close"); 1.246 + closeButton.addEventListener("command", this.destroy, true); 1.247 + 1.248 + this._buildDockButtons(); 1.249 + this._buildOptions(); 1.250 + this._buildTabs(); 1.251 + this._buildButtons(); 1.252 + this._addKeysToWindow(); 1.253 + this._addToolSwitchingKeys(); 1.254 + this._addZoomKeys(); 1.255 + this._loadInitialZoom(); 1.256 + 1.257 + this._telemetry.toolOpened("toolbox"); 1.258 + 1.259 + this.selectTool(this._defaultToolId).then(panel => { 1.260 + this.emit("ready"); 1.261 + deferred.resolve(); 1.262 + }); 1.263 + }; 1.264 + 1.265 + // Load the toolbox-level actor fronts and utilities now 1.266 + this._target.makeRemote().then(() => { 1.267 + iframe.setAttribute("src", this._URL); 1.268 + let domHelper = new DOMHelpers(iframe.contentWindow); 1.269 + domHelper.onceDOMReady(domReady); 1.270 + }); 1.271 + 1.272 + return deferred.promise; 1.273 + }); 1.274 + }, 1.275 + 1.276 + _buildOptions: function() { 1.277 + let key = this.doc.getElementById("toolbox-options-key"); 1.278 + key.addEventListener("command", () => { 1.279 + this.selectTool("options"); 1.280 + }, true); 1.281 + }, 1.282 + 1.283 + _isResponsiveModeActive: function() { 1.284 + let responsiveModeActive = false; 1.285 + if (this.target.isLocalTab) { 1.286 + let tab = this.target.tab; 1.287 + let browserWindow = tab.ownerDocument.defaultView; 1.288 + let responsiveUIManager = browserWindow.ResponsiveUI.ResponsiveUIManager; 1.289 + responsiveModeActive = responsiveUIManager.isActiveForTab(tab); 1.290 + } 1.291 + return responsiveModeActive; 1.292 + }, 1.293 + 1.294 + _splitConsoleOnKeypress: function(e) { 1.295 + let responsiveModeActive = this._isResponsiveModeActive(); 1.296 + if (e.keyCode === e.DOM_VK_ESCAPE && !responsiveModeActive) { 1.297 + this.toggleSplitConsole(); 1.298 + } 1.299 + }, 1.300 + 1.301 + _addToolSwitchingKeys: function() { 1.302 + let nextKey = this.doc.getElementById("toolbox-next-tool-key"); 1.303 + nextKey.addEventListener("command", this.selectNextTool.bind(this), true); 1.304 + let prevKey = this.doc.getElementById("toolbox-previous-tool-key"); 1.305 + prevKey.addEventListener("command", this.selectPreviousTool.bind(this), true); 1.306 + 1.307 + // Split console uses keypress instead of command so the event can be 1.308 + // cancelled with stopPropagation on the keypress, and not preventDefault. 1.309 + this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false); 1.310 + }, 1.311 + 1.312 + /** 1.313 + * Make sure that the console is showing up properly based on all the 1.314 + * possible conditions. 1.315 + * 1) If the console tab is selected, then regardless of split state 1.316 + * it should take up the full height of the deck, and we should 1.317 + * hide the deck and splitter. 1.318 + * 2) If the console tab is not selected and it is split, then we should 1.319 + * show the splitter, deck, and console. 1.320 + * 3) If the console tab is not selected and it is *not* split, 1.321 + * then we should hide the console and splitter, and show the deck 1.322 + * at full height. 1.323 + */ 1.324 + _refreshConsoleDisplay: function() { 1.325 + let deck = this.doc.getElementById("toolbox-deck"); 1.326 + let webconsolePanel = this.doc.getElementById("toolbox-panel-webconsole"); 1.327 + let splitter = this.doc.getElementById("toolbox-console-splitter"); 1.328 + let openedConsolePanel = this.currentToolId === "webconsole"; 1.329 + 1.330 + if (openedConsolePanel) { 1.331 + deck.setAttribute("collapsed", "true"); 1.332 + splitter.setAttribute("hidden", "true"); 1.333 + webconsolePanel.removeAttribute("collapsed"); 1.334 + } else { 1.335 + deck.removeAttribute("collapsed"); 1.336 + if (this._splitConsole) { 1.337 + webconsolePanel.removeAttribute("collapsed"); 1.338 + splitter.removeAttribute("hidden"); 1.339 + } else { 1.340 + webconsolePanel.setAttribute("collapsed", "true"); 1.341 + splitter.setAttribute("hidden", "true"); 1.342 + } 1.343 + } 1.344 + }, 1.345 + 1.346 + /** 1.347 + * Wire up the listeners for the zoom keys. 1.348 + */ 1.349 + _addZoomKeys: function() { 1.350 + let inKey = this.doc.getElementById("toolbox-zoom-in-key"); 1.351 + inKey.addEventListener("command", this.zoomIn.bind(this), true); 1.352 + 1.353 + let inKey2 = this.doc.getElementById("toolbox-zoom-in-key2"); 1.354 + inKey2.addEventListener("command", this.zoomIn.bind(this), true); 1.355 + 1.356 + let outKey = this.doc.getElementById("toolbox-zoom-out-key"); 1.357 + outKey.addEventListener("command", this.zoomOut.bind(this), true); 1.358 + 1.359 + let resetKey = this.doc.getElementById("toolbox-zoom-reset-key"); 1.360 + resetKey.addEventListener("command", this.zoomReset.bind(this), true); 1.361 + }, 1.362 + 1.363 + /** 1.364 + * Set zoom on toolbox to whatever the last setting was. 1.365 + */ 1.366 + _loadInitialZoom: function() { 1.367 + this.setZoom(this.zoomValue); 1.368 + }, 1.369 + 1.370 + /** 1.371 + * Increase zoom level of toolbox window - make things bigger. 1.372 + */ 1.373 + zoomIn: function() { 1.374 + this.setZoom(this.zoomValue + 0.1); 1.375 + }, 1.376 + 1.377 + /** 1.378 + * Decrease zoom level of toolbox window - make things smaller. 1.379 + */ 1.380 + zoomOut: function() { 1.381 + this.setZoom(this.zoomValue - 0.1); 1.382 + }, 1.383 + 1.384 + /** 1.385 + * Reset zoom level of the toolbox window. 1.386 + */ 1.387 + zoomReset: function() { 1.388 + this.setZoom(1); 1.389 + }, 1.390 + 1.391 + /** 1.392 + * Set zoom level of the toolbox window. 1.393 + * 1.394 + * @param {number} zoomValue 1.395 + * Zoom level e.g. 1.2 1.396 + */ 1.397 + setZoom: function(zoomValue) { 1.398 + // cap zoom value 1.399 + zoomValue = Math.max(zoomValue, MIN_ZOOM); 1.400 + zoomValue = Math.min(zoomValue, MAX_ZOOM); 1.401 + 1.402 + let contViewer = this.frame.docShell.contentViewer; 1.403 + let docViewer = contViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); 1.404 + 1.405 + docViewer.fullZoom = zoomValue; 1.406 + 1.407 + Services.prefs.setCharPref(ZOOM_PREF, zoomValue); 1.408 + }, 1.409 + 1.410 + /** 1.411 + * Adds the keys and commands to the Toolbox Window in window mode. 1.412 + */ 1.413 + _addKeysToWindow: function() { 1.414 + if (this.hostType != Toolbox.HostType.WINDOW) { 1.415 + return; 1.416 + } 1.417 + 1.418 + let doc = this.doc.defaultView.parent.document; 1.419 + 1.420 + for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) { 1.421 + // Prevent multiple entries for the same tool. 1.422 + if (!toolDefinition.key || doc.getElementById("key_" + id)) { 1.423 + continue; 1.424 + } 1.425 + 1.426 + let toolId = id; 1.427 + let key = doc.createElement("key"); 1.428 + 1.429 + key.id = "key_" + toolId; 1.430 + 1.431 + if (toolDefinition.key.startsWith("VK_")) { 1.432 + key.setAttribute("keycode", toolDefinition.key); 1.433 + } else { 1.434 + key.setAttribute("key", toolDefinition.key); 1.435 + } 1.436 + 1.437 + key.setAttribute("modifiers", toolDefinition.modifiers); 1.438 + key.setAttribute("oncommand", "void(0);"); // needed. See bug 371900 1.439 + key.addEventListener("command", () => { 1.440 + this.selectTool(toolId).then(() => this.fireCustomKey(toolId)); 1.441 + }, true); 1.442 + doc.getElementById("toolbox-keyset").appendChild(key); 1.443 + } 1.444 + 1.445 + // Add key for toggling the browser console from the detached window 1.446 + if (!doc.getElementById("key_browserconsole")) { 1.447 + let key = doc.createElement("key"); 1.448 + key.id = "key_browserconsole"; 1.449 + 1.450 + key.setAttribute("key", toolboxStrings("browserConsoleCmd.commandkey")); 1.451 + key.setAttribute("modifiers", "accel,shift"); 1.452 + key.setAttribute("oncommand", "void(0)"); // needed. See bug 371900 1.453 + key.addEventListener("command", () => { 1.454 + HUDService.toggleBrowserConsole(); 1.455 + }, true); 1.456 + doc.getElementById("toolbox-keyset").appendChild(key); 1.457 + } 1.458 + }, 1.459 + 1.460 + /** 1.461 + * Handle any custom key events. Returns true if there was a custom key binding run 1.462 + * @param {string} toolId 1.463 + * Which tool to run the command on (skip if not current) 1.464 + */ 1.465 + fireCustomKey: function(toolId) { 1.466 + let toolDefinition = gDevTools.getToolDefinition(toolId); 1.467 + 1.468 + if (toolDefinition.onkey && 1.469 + ((this.currentToolId === toolId) || 1.470 + (toolId == "webconsole" && this.splitConsole))) { 1.471 + toolDefinition.onkey(this.getCurrentPanel(), this); 1.472 + } 1.473 + }, 1.474 + 1.475 + /** 1.476 + * Build the buttons for changing hosts. Called every time 1.477 + * the host changes. 1.478 + */ 1.479 + _buildDockButtons: function() { 1.480 + let dockBox = this.doc.getElementById("toolbox-dock-buttons"); 1.481 + 1.482 + while (dockBox.firstChild) { 1.483 + dockBox.removeChild(dockBox.firstChild); 1.484 + } 1.485 + 1.486 + if (!this._target.isLocalTab) { 1.487 + return; 1.488 + } 1.489 + 1.490 + let closeButton = this.doc.getElementById("toolbox-close"); 1.491 + if (this.hostType == Toolbox.HostType.WINDOW) { 1.492 + closeButton.setAttribute("hidden", "true"); 1.493 + } else { 1.494 + closeButton.removeAttribute("hidden"); 1.495 + } 1.496 + 1.497 + let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED); 1.498 + 1.499 + for (let type in Toolbox.HostType) { 1.500 + let position = Toolbox.HostType[type]; 1.501 + if (position == this.hostType || 1.502 + position == Toolbox.HostType.CUSTOM || 1.503 + (!sideEnabled && position == Toolbox.HostType.SIDE)) { 1.504 + continue; 1.505 + } 1.506 + 1.507 + let button = this.doc.createElement("toolbarbutton"); 1.508 + button.id = "toolbox-dock-" + position; 1.509 + button.className = "toolbox-dock-button"; 1.510 + button.setAttribute("tooltiptext", toolboxStrings("toolboxDockButtons." + 1.511 + position + ".tooltip")); 1.512 + button.addEventListener("command", () => { 1.513 + this.switchHost(position); 1.514 + }); 1.515 + 1.516 + dockBox.appendChild(button); 1.517 + } 1.518 + }, 1.519 + 1.520 + /** 1.521 + * Add tabs to the toolbox UI for registered tools 1.522 + */ 1.523 + _buildTabs: function() { 1.524 + for (let definition of gDevTools.getToolDefinitionArray()) { 1.525 + this._buildTabForTool(definition); 1.526 + } 1.527 + }, 1.528 + 1.529 + /** 1.530 + * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref 1.531 + */ 1.532 + _buildButtons: function() { 1.533 + this._buildPickerButton(); 1.534 + 1.535 + if (!this.target.isLocalTab) { 1.536 + return; 1.537 + } 1.538 + 1.539 + let spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec"); 1.540 + let environment = CommandUtils.createEnvironment(this, '_target'); 1.541 + this._requisition = CommandUtils.createRequisition(environment); 1.542 + let buttons = CommandUtils.createButtons(spec, this._target, 1.543 + this.doc, this._requisition); 1.544 + let container = this.doc.getElementById("toolbox-buttons"); 1.545 + buttons.forEach(container.appendChild.bind(container)); 1.546 + this.setToolboxButtonsVisibility(); 1.547 + }, 1.548 + 1.549 + /** 1.550 + * Adding the element picker button is done here unlike the other buttons 1.551 + * since we want it to work for remote targets too 1.552 + */ 1.553 + _buildPickerButton: function() { 1.554 + this._pickerButton = this.doc.createElement("toolbarbutton"); 1.555 + this._pickerButton.id = "command-button-pick"; 1.556 + this._pickerButton.className = "command-button command-button-invertable"; 1.557 + this._pickerButton.setAttribute("tooltiptext", toolboxStrings("pickButton.tooltip")); 1.558 + 1.559 + let container = this.doc.querySelector("#toolbox-buttons"); 1.560 + container.appendChild(this._pickerButton); 1.561 + 1.562 + this._togglePicker = this.highlighterUtils.togglePicker.bind(this.highlighterUtils); 1.563 + this._pickerButton.addEventListener("command", this._togglePicker, false); 1.564 + }, 1.565 + 1.566 + /** 1.567 + * Return all toolbox buttons (command buttons, plus any others that were 1.568 + * added manually). 1.569 + */ 1.570 + get toolboxButtons() { 1.571 + // White-list buttons that can be toggled to prevent adding prefs for 1.572 + // addons that have manually inserted toolbarbuttons into DOM. 1.573 + return [ 1.574 + "command-button-pick", 1.575 + "command-button-splitconsole", 1.576 + "command-button-responsive", 1.577 + "command-button-paintflashing", 1.578 + "command-button-tilt", 1.579 + "command-button-scratchpad", 1.580 + "command-button-eyedropper" 1.581 + ].map(id => { 1.582 + let button = this.doc.getElementById(id); 1.583 + // Some buttons may not exist inside of Browser Toolbox 1.584 + if (!button) { 1.585 + return false; 1.586 + } 1.587 + return { 1.588 + id: id, 1.589 + button: button, 1.590 + label: button.getAttribute("tooltiptext"), 1.591 + visibilityswitch: "devtools." + id + ".enabled" 1.592 + } 1.593 + }).filter(button=>button); 1.594 + }, 1.595 + 1.596 + /** 1.597 + * Ensure the visibility of each toolbox button matches the 1.598 + * preference value. Simply hide buttons that are preffed off. 1.599 + */ 1.600 + setToolboxButtonsVisibility: function() { 1.601 + this.toolboxButtons.forEach(buttonSpec => { 1.602 + let {visibilityswitch, id, button}=buttonSpec; 1.603 + let on = true; 1.604 + try { 1.605 + on = Services.prefs.getBoolPref(visibilityswitch); 1.606 + } catch (ex) { } 1.607 + 1.608 + if (button) { 1.609 + if (on) { 1.610 + button.removeAttribute("hidden"); 1.611 + } else { 1.612 + button.setAttribute("hidden", "true"); 1.613 + } 1.614 + } 1.615 + }); 1.616 + }, 1.617 + 1.618 + /** 1.619 + * Build a tab for one tool definition and add to the toolbox 1.620 + * 1.621 + * @param {string} toolDefinition 1.622 + * Tool definition of the tool to build a tab for. 1.623 + */ 1.624 + _buildTabForTool: function(toolDefinition) { 1.625 + if (!toolDefinition.isTargetSupported(this._target)) { 1.626 + return; 1.627 + } 1.628 + 1.629 + let tabs = this.doc.getElementById("toolbox-tabs"); 1.630 + let deck = this.doc.getElementById("toolbox-deck"); 1.631 + 1.632 + let id = toolDefinition.id; 1.633 + 1.634 + if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) { 1.635 + toolDefinition.ordinal = MAX_ORDINAL; 1.636 + } 1.637 + 1.638 + let radio = this.doc.createElement("radio"); 1.639 + // The radio element is not being used in the conventional way, thus 1.640 + // the devtools-tab class replaces the radio XBL binding with its base 1.641 + // binding (the control-item binding). 1.642 + radio.className = "devtools-tab"; 1.643 + radio.id = "toolbox-tab-" + id; 1.644 + radio.setAttribute("toolid", id); 1.645 + radio.setAttribute("ordinal", toolDefinition.ordinal); 1.646 + radio.setAttribute("tooltiptext", toolDefinition.tooltip); 1.647 + if (toolDefinition.invertIconForLightTheme) { 1.648 + radio.setAttribute("icon-invertable", "true"); 1.649 + } 1.650 + 1.651 + radio.addEventListener("command", () => { 1.652 + this.selectTool(id); 1.653 + }); 1.654 + 1.655 + // spacer lets us center the image and label, while allowing cropping 1.656 + let spacer = this.doc.createElement("spacer"); 1.657 + spacer.setAttribute("flex", "1"); 1.658 + radio.appendChild(spacer); 1.659 + 1.660 + if (toolDefinition.icon) { 1.661 + let image = this.doc.createElement("image"); 1.662 + image.className = "default-icon"; 1.663 + image.setAttribute("src", 1.664 + toolDefinition.icon || toolDefinition.highlightedicon); 1.665 + radio.appendChild(image); 1.666 + // Adding the highlighted icon image 1.667 + image = this.doc.createElement("image"); 1.668 + image.className = "highlighted-icon"; 1.669 + image.setAttribute("src", 1.670 + toolDefinition.highlightedicon || toolDefinition.icon); 1.671 + radio.appendChild(image); 1.672 + } 1.673 + 1.674 + if (toolDefinition.label) { 1.675 + let label = this.doc.createElement("label"); 1.676 + label.setAttribute("value", toolDefinition.label) 1.677 + label.setAttribute("crop", "end"); 1.678 + label.setAttribute("flex", "1"); 1.679 + radio.appendChild(label); 1.680 + radio.setAttribute("flex", "1"); 1.681 + } 1.682 + 1.683 + if (!toolDefinition.bgTheme) { 1.684 + toolDefinition.bgTheme = "theme-toolbar"; 1.685 + } 1.686 + let vbox = this.doc.createElement("vbox"); 1.687 + vbox.className = "toolbox-panel " + toolDefinition.bgTheme; 1.688 + 1.689 + // There is already a container for the webconsole frame. 1.690 + if (!this.doc.getElementById("toolbox-panel-" + id)) { 1.691 + vbox.id = "toolbox-panel-" + id; 1.692 + } 1.693 + 1.694 + // If there is no tab yet, or the ordinal to be added is the largest one. 1.695 + if (tabs.childNodes.length == 0 || 1.696 + +tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) { 1.697 + tabs.appendChild(radio); 1.698 + deck.appendChild(vbox); 1.699 + } else { 1.700 + // else, iterate over all the tabs to get the correct location. 1.701 + Array.some(tabs.childNodes, (node, i) => { 1.702 + if (+node.getAttribute("ordinal") > toolDefinition.ordinal) { 1.703 + tabs.insertBefore(radio, node); 1.704 + deck.insertBefore(vbox, deck.childNodes[i]); 1.705 + return true; 1.706 + } 1.707 + return false; 1.708 + }); 1.709 + } 1.710 + 1.711 + this._addKeysToWindow(); 1.712 + }, 1.713 + 1.714 + /** 1.715 + * Ensure the tool with the given id is loaded. 1.716 + * 1.717 + * @param {string} id 1.718 + * The id of the tool to load. 1.719 + */ 1.720 + loadTool: function(id) { 1.721 + if (id === "inspector" && !this._inspector) { 1.722 + return this.initInspector().then(() => { 1.723 + return this.loadTool(id); 1.724 + }); 1.725 + } 1.726 + 1.727 + let deferred = promise.defer(); 1.728 + let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); 1.729 + 1.730 + if (iframe) { 1.731 + let panel = this._toolPanels.get(id); 1.732 + if (panel) { 1.733 + deferred.resolve(panel); 1.734 + } else { 1.735 + this.once(id + "-ready", panel => { 1.736 + deferred.resolve(panel); 1.737 + }); 1.738 + } 1.739 + return deferred.promise; 1.740 + } 1.741 + 1.742 + let definition = gDevTools.getToolDefinition(id); 1.743 + if (!definition) { 1.744 + deferred.reject(new Error("no such tool id "+id)); 1.745 + return deferred.promise; 1.746 + } 1.747 + 1.748 + iframe = this.doc.createElement("iframe"); 1.749 + iframe.className = "toolbox-panel-iframe"; 1.750 + iframe.id = "toolbox-panel-iframe-" + id; 1.751 + iframe.setAttribute("flex", 1); 1.752 + iframe.setAttribute("forceOwnRefreshDriver", ""); 1.753 + iframe.tooltip = "aHTMLTooltip"; 1.754 + iframe.style.visibility = "hidden"; 1.755 + 1.756 + let vbox = this.doc.getElementById("toolbox-panel-" + id); 1.757 + vbox.appendChild(iframe); 1.758 + 1.759 + let onLoad = () => { 1.760 + // Prevent flicker while loading by waiting to make visible until now. 1.761 + iframe.style.visibility = "visible"; 1.762 + 1.763 + let built = definition.build(iframe.contentWindow, this); 1.764 + promise.resolve(built).then((panel) => { 1.765 + this._toolPanels.set(id, panel); 1.766 + this.emit(id + "-ready", panel); 1.767 + gDevTools.emit(id + "-ready", this, panel); 1.768 + deferred.resolve(panel); 1.769 + }, console.error); 1.770 + }; 1.771 + 1.772 + iframe.setAttribute("src", definition.url); 1.773 + 1.774 + // Depending on the host, iframe.contentWindow is not always 1.775 + // defined at this moment. If it is not defined, we use an 1.776 + // event listener on the iframe DOM node. If it's defined, 1.777 + // we use the chromeEventHandler. We can't use a listener 1.778 + // on the DOM node every time because this won't work 1.779 + // if the (xul chrome) iframe is loaded in a content docshell. 1.780 + if (iframe.contentWindow) { 1.781 + let domHelper = new DOMHelpers(iframe.contentWindow); 1.782 + domHelper.onceDOMReady(onLoad); 1.783 + } else { 1.784 + let callback = () => { 1.785 + iframe.removeEventListener("DOMContentLoaded", callback); 1.786 + onLoad(); 1.787 + } 1.788 + iframe.addEventListener("DOMContentLoaded", callback); 1.789 + } 1.790 + 1.791 + return deferred.promise; 1.792 + }, 1.793 + 1.794 + /** 1.795 + * Switch to the tool with the given id 1.796 + * 1.797 + * @param {string} id 1.798 + * The id of the tool to switch to 1.799 + */ 1.800 + selectTool: function(id) { 1.801 + let selected = this.doc.querySelector(".devtools-tab[selected]"); 1.802 + if (selected) { 1.803 + selected.removeAttribute("selected"); 1.804 + } 1.805 + 1.806 + let tab = this.doc.getElementById("toolbox-tab-" + id); 1.807 + tab.setAttribute("selected", "true"); 1.808 + 1.809 + if (this.currentToolId == id) { 1.810 + // re-focus tool to get key events again 1.811 + this.focusTool(id); 1.812 + 1.813 + // Return the existing panel in order to have a consistent return value. 1.814 + return promise.resolve(this._toolPanels.get(id)); 1.815 + } 1.816 + 1.817 + if (!this.isReady) { 1.818 + throw new Error("Can't select tool, wait for toolbox 'ready' event"); 1.819 + } 1.820 + 1.821 + tab = this.doc.getElementById("toolbox-tab-" + id); 1.822 + 1.823 + if (tab) { 1.824 + if (this.currentToolId) { 1.825 + this._telemetry.toolClosed(this.currentToolId); 1.826 + } 1.827 + this._telemetry.toolOpened(id); 1.828 + } else { 1.829 + throw new Error("No tool found"); 1.830 + } 1.831 + 1.832 + let tabstrip = this.doc.getElementById("toolbox-tabs"); 1.833 + 1.834 + // select the right tab, making 0th index the default tab if right tab not 1.835 + // found 1.836 + let index = 0; 1.837 + let tabs = tabstrip.childNodes; 1.838 + for (let i = 0; i < tabs.length; i++) { 1.839 + if (tabs[i] === tab) { 1.840 + index = i; 1.841 + break; 1.842 + } 1.843 + } 1.844 + tabstrip.selectedItem = tab; 1.845 + 1.846 + // and select the right iframe 1.847 + let deck = this.doc.getElementById("toolbox-deck"); 1.848 + deck.selectedIndex = index; 1.849 + 1.850 + this.currentToolId = id; 1.851 + this._refreshConsoleDisplay(); 1.852 + if (id != "options") { 1.853 + Services.prefs.setCharPref(this._prefs.LAST_TOOL, id); 1.854 + } 1.855 + 1.856 + return this.loadTool(id).then(panel => { 1.857 + // focus the tool's frame to start receiving key events 1.858 + this.focusTool(id); 1.859 + 1.860 + this.emit("select", id); 1.861 + this.emit(id + "-selected", panel); 1.862 + return panel; 1.863 + }); 1.864 + }, 1.865 + 1.866 + /** 1.867 + * Focus a tool's panel by id 1.868 + * @param {string} id 1.869 + * The id of tool to focus 1.870 + */ 1.871 + focusTool: function(id) { 1.872 + let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); 1.873 + iframe.focus(); 1.874 + }, 1.875 + 1.876 + /** 1.877 + * Focus split console's input line 1.878 + */ 1.879 + focusConsoleInput: function() { 1.880 + let hud = this.getPanel("webconsole").hud; 1.881 + if (hud && hud.jsterm) { 1.882 + hud.jsterm.inputNode.focus(); 1.883 + } 1.884 + }, 1.885 + 1.886 + /** 1.887 + * Toggles the split state of the webconsole. If the webconsole panel 1.888 + * is already selected, then this command is ignored. 1.889 + */ 1.890 + toggleSplitConsole: function() { 1.891 + let openedConsolePanel = this.currentToolId === "webconsole"; 1.892 + 1.893 + // Don't allow changes when console is open, since it could be confusing 1.894 + if (!openedConsolePanel) { 1.895 + this._splitConsole = !this._splitConsole; 1.896 + this._refreshConsoleDisplay(); 1.897 + this.emit("split-console"); 1.898 + 1.899 + if (this._splitConsole) { 1.900 + this.loadTool("webconsole").then(() => { 1.901 + this.focusConsoleInput(); 1.902 + }); 1.903 + } 1.904 + } 1.905 + }, 1.906 + 1.907 + /** 1.908 + * Loads the tool next to the currently selected tool. 1.909 + */ 1.910 + selectNextTool: function() { 1.911 + let selected = this.doc.querySelector(".devtools-tab[selected]"); 1.912 + let next = selected.nextSibling || selected.parentNode.firstChild; 1.913 + let tool = next.getAttribute("toolid"); 1.914 + return this.selectTool(tool); 1.915 + }, 1.916 + 1.917 + /** 1.918 + * Loads the tool just left to the currently selected tool. 1.919 + */ 1.920 + selectPreviousTool: function() { 1.921 + let selected = this.doc.querySelector(".devtools-tab[selected]"); 1.922 + let previous = selected.previousSibling || selected.parentNode.lastChild; 1.923 + let tool = previous.getAttribute("toolid"); 1.924 + return this.selectTool(tool); 1.925 + }, 1.926 + 1.927 + /** 1.928 + * Highlights the tool's tab if it is not the currently selected tool. 1.929 + * 1.930 + * @param {string} id 1.931 + * The id of the tool to highlight 1.932 + */ 1.933 + highlightTool: function(id) { 1.934 + let tab = this.doc.getElementById("toolbox-tab-" + id); 1.935 + tab && tab.setAttribute("highlighted", "true"); 1.936 + }, 1.937 + 1.938 + /** 1.939 + * De-highlights the tool's tab. 1.940 + * 1.941 + * @param {string} id 1.942 + * The id of the tool to unhighlight 1.943 + */ 1.944 + unhighlightTool: function(id) { 1.945 + let tab = this.doc.getElementById("toolbox-tab-" + id); 1.946 + tab && tab.removeAttribute("highlighted"); 1.947 + }, 1.948 + 1.949 + /** 1.950 + * Raise the toolbox host. 1.951 + */ 1.952 + raise: function() { 1.953 + this._host.raise(); 1.954 + }, 1.955 + 1.956 + /** 1.957 + * Refresh the host's title. 1.958 + */ 1.959 + _refreshHostTitle: function() { 1.960 + let toolName; 1.961 + let toolDef = gDevTools.getToolDefinition(this.currentToolId); 1.962 + if (toolDef) { 1.963 + toolName = toolDef.label; 1.964 + } else { 1.965 + // no tool is selected 1.966 + toolName = toolboxStrings("toolbox.defaultTitle"); 1.967 + } 1.968 + let title = toolboxStrings("toolbox.titleTemplate", 1.969 + toolName, this.target.url || this.target.name); 1.970 + this._host.setTitle(title); 1.971 + }, 1.972 + 1.973 + /** 1.974 + * Create a host object based on the given host type. 1.975 + * 1.976 + * Warning: some hosts require that the toolbox target provides a reference to 1.977 + * the attached tab. Not all Targets have a tab property - make sure you correctly 1.978 + * mix and match hosts and targets. 1.979 + * 1.980 + * @param {string} hostType 1.981 + * The host type of the new host object 1.982 + * 1.983 + * @return {Host} host 1.984 + * The created host object 1.985 + */ 1.986 + _createHost: function(hostType, options) { 1.987 + if (!Hosts[hostType]) { 1.988 + throw new Error("Unknown hostType: " + hostType); 1.989 + } 1.990 + 1.991 + // clean up the toolbox if its window is closed 1.992 + let newHost = new Hosts[hostType](this.target.tab, options); 1.993 + newHost.on("window-closed", this.destroy); 1.994 + return newHost; 1.995 + }, 1.996 + 1.997 + /** 1.998 + * Switch to a new host for the toolbox UI. E.g. 1.999 + * bottom, sidebar, separate window. 1.1000 + * 1.1001 + * @param {string} hostType 1.1002 + * The host type of the new host object 1.1003 + */ 1.1004 + switchHost: function(hostType) { 1.1005 + if (hostType == this._host.type || !this._target.isLocalTab) { 1.1006 + return null; 1.1007 + } 1.1008 + 1.1009 + let newHost = this._createHost(hostType); 1.1010 + return newHost.create().then(iframe => { 1.1011 + // change toolbox document's parent to the new host 1.1012 + iframe.QueryInterface(Ci.nsIFrameLoaderOwner); 1.1013 + iframe.swapFrameLoaders(this.frame); 1.1014 + 1.1015 + this._host.off("window-closed", this.destroy); 1.1016 + this.destroyHost(); 1.1017 + 1.1018 + this._host = newHost; 1.1019 + 1.1020 + if (this.hostType != Toolbox.HostType.CUSTOM) { 1.1021 + Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type); 1.1022 + } 1.1023 + 1.1024 + this._buildDockButtons(); 1.1025 + this._addKeysToWindow(); 1.1026 + 1.1027 + this.emit("host-changed"); 1.1028 + }); 1.1029 + }, 1.1030 + 1.1031 + /** 1.1032 + * Handler for the tool-registered event. 1.1033 + * @param {string} event 1.1034 + * Name of the event ("tool-registered") 1.1035 + * @param {string} toolId 1.1036 + * Id of the tool that was registered 1.1037 + */ 1.1038 + _toolRegistered: function(event, toolId) { 1.1039 + let tool = gDevTools.getToolDefinition(toolId); 1.1040 + this._buildTabForTool(tool); 1.1041 + }, 1.1042 + 1.1043 + /** 1.1044 + * Handler for the tool-unregistered event. 1.1045 + * @param {string} event 1.1046 + * Name of the event ("tool-unregistered") 1.1047 + * @param {string|object} toolId 1.1048 + * Definition or id of the tool that was unregistered. Passing the 1.1049 + * tool id should be avoided as it is a temporary measure. 1.1050 + */ 1.1051 + _toolUnregistered: function(event, toolId) { 1.1052 + if (typeof toolId != "string") { 1.1053 + toolId = toolId.id; 1.1054 + } 1.1055 + 1.1056 + if (this._toolPanels.has(toolId)) { 1.1057 + let instance = this._toolPanels.get(toolId); 1.1058 + instance.destroy(); 1.1059 + this._toolPanels.delete(toolId); 1.1060 + } 1.1061 + 1.1062 + let radio = this.doc.getElementById("toolbox-tab-" + toolId); 1.1063 + let panel = this.doc.getElementById("toolbox-panel-" + toolId); 1.1064 + 1.1065 + if (radio) { 1.1066 + if (this.currentToolId == toolId) { 1.1067 + let nextToolName = null; 1.1068 + if (radio.nextSibling) { 1.1069 + nextToolName = radio.nextSibling.getAttribute("toolid"); 1.1070 + } 1.1071 + if (radio.previousSibling) { 1.1072 + nextToolName = radio.previousSibling.getAttribute("toolid"); 1.1073 + } 1.1074 + if (nextToolName) { 1.1075 + this.selectTool(nextToolName); 1.1076 + } 1.1077 + } 1.1078 + radio.parentNode.removeChild(radio); 1.1079 + } 1.1080 + 1.1081 + if (panel) { 1.1082 + panel.parentNode.removeChild(panel); 1.1083 + } 1.1084 + 1.1085 + if (this.hostType == Toolbox.HostType.WINDOW) { 1.1086 + let doc = this.doc.defaultView.parent.document; 1.1087 + let key = doc.getElementById("key_" + toolId); 1.1088 + if (key) { 1.1089 + key.parentNode.removeChild(key); 1.1090 + } 1.1091 + } 1.1092 + }, 1.1093 + 1.1094 + /** 1.1095 + * Initialize the inspector/walker/selection/highlighter fronts. 1.1096 + * Returns a promise that resolves when the fronts are initialized 1.1097 + */ 1.1098 + initInspector: function() { 1.1099 + if (!this._initInspector) { 1.1100 + this._initInspector = Task.spawn(function*() { 1.1101 + this._inspector = InspectorFront(this._target.client, this._target.form); 1.1102 + this._walker = yield this._inspector.getWalker(); 1.1103 + this._selection = new Selection(this._walker); 1.1104 + 1.1105 + if (this.highlighterUtils.isRemoteHighlightable) { 1.1106 + let autohide = !gDevTools.testing; 1.1107 + 1.1108 + this.walker.on("highlighter-ready", this._highlighterReady); 1.1109 + this.walker.on("highlighter-hide", this._highlighterHidden); 1.1110 + 1.1111 + this._highlighter = yield this._inspector.getHighlighter(autohide); 1.1112 + } 1.1113 + }.bind(this)); 1.1114 + } 1.1115 + return this._initInspector; 1.1116 + }, 1.1117 + 1.1118 + /** 1.1119 + * Destroy the inspector/walker/selection fronts 1.1120 + * Returns a promise that resolves when the fronts are destroyed 1.1121 + */ 1.1122 + destroyInspector: function() { 1.1123 + if (this._destroying) { 1.1124 + return this._destroying; 1.1125 + } 1.1126 + 1.1127 + if (!this._inspector) { 1.1128 + return promise.resolve(); 1.1129 + } 1.1130 + 1.1131 + let outstanding = () => { 1.1132 + return Task.spawn(function*() { 1.1133 + yield this.highlighterUtils.stopPicker(); 1.1134 + yield this._inspector.destroy(); 1.1135 + if (this._highlighter) { 1.1136 + yield this._highlighter.destroy(); 1.1137 + } 1.1138 + if (this._selection) { 1.1139 + this._selection.destroy(); 1.1140 + } 1.1141 + 1.1142 + if (this.walker) { 1.1143 + this.walker.off("highlighter-ready", this._highlighterReady); 1.1144 + this.walker.off("highlighter-hide", this._highlighterHidden); 1.1145 + } 1.1146 + 1.1147 + this._inspector = null; 1.1148 + this._highlighter = null; 1.1149 + this._selection = null; 1.1150 + this._walker = null; 1.1151 + }.bind(this)); 1.1152 + }; 1.1153 + 1.1154 + // Releasing the walker (if it has been created) 1.1155 + // This can fail, but in any case, we want to continue destroying the 1.1156 + // inspector/highlighter/selection 1.1157 + let walker = (this._destroying = this._walker) ? 1.1158 + this._walker.release() : 1.1159 + promise.resolve(); 1.1160 + return walker.then(outstanding, outstanding); 1.1161 + }, 1.1162 + 1.1163 + /** 1.1164 + * Get the toolbox's notification box 1.1165 + * 1.1166 + * @return The notification box element. 1.1167 + */ 1.1168 + getNotificationBox: function() { 1.1169 + return this.doc.getElementById("toolbox-notificationbox"); 1.1170 + }, 1.1171 + 1.1172 + /** 1.1173 + * Destroy the current host, and remove event listeners from its frame. 1.1174 + * 1.1175 + * @return {promise} to be resolved when the host is destroyed. 1.1176 + */ 1.1177 + destroyHost: function() { 1.1178 + this.doc.removeEventListener("keypress", 1.1179 + this._splitConsoleOnKeypress, false); 1.1180 + return this._host.destroy(); 1.1181 + }, 1.1182 + 1.1183 + /** 1.1184 + * Remove all UI elements, detach from target and clear up 1.1185 + */ 1.1186 + destroy: function() { 1.1187 + // If several things call destroy then we give them all the same 1.1188 + // destruction promise so we're sure to destroy only once 1.1189 + if (this._destroyer) { 1.1190 + return this._destroyer; 1.1191 + } 1.1192 + 1.1193 + this._target.off("navigate", this._refreshHostTitle); 1.1194 + this.off("select", this._refreshHostTitle); 1.1195 + this.off("host-changed", this._refreshHostTitle); 1.1196 + 1.1197 + gDevTools.off("tool-registered", this._toolRegistered); 1.1198 + gDevTools.off("tool-unregistered", this._toolUnregistered); 1.1199 + 1.1200 + let outstanding = []; 1.1201 + for (let [id, panel] of this._toolPanels) { 1.1202 + try { 1.1203 + outstanding.push(panel.destroy()); 1.1204 + } catch (e) { 1.1205 + // We don't want to stop here if any panel fail to close. 1.1206 + console.error("Panel " + id + ":", e); 1.1207 + } 1.1208 + } 1.1209 + 1.1210 + // Destroying the walker and inspector fronts 1.1211 + outstanding.push(this.destroyInspector()); 1.1212 + // Removing buttons 1.1213 + outstanding.push(() => { 1.1214 + this._pickerButton.removeEventListener("command", this._togglePicker, false); 1.1215 + this._pickerButton = null; 1.1216 + let container = this.doc.getElementById("toolbox-buttons"); 1.1217 + while (container.firstChild) { 1.1218 + container.removeChild(container.firstChild); 1.1219 + } 1.1220 + }); 1.1221 + // Remove the host UI 1.1222 + outstanding.push(this.destroyHost()); 1.1223 + 1.1224 + if (this.target.isLocalTab) { 1.1225 + this._requisition.destroy(); 1.1226 + } 1.1227 + this._telemetry.destroy(); 1.1228 + 1.1229 + return this._destroyer = promise.all(outstanding).then(() => { 1.1230 + // Targets need to be notified that the toolbox is being torn down. 1.1231 + // This is done after other destruction tasks since it may tear down 1.1232 + // fronts and the debugger transport which earlier destroy methods may 1.1233 + // require to complete. 1.1234 + if (!this._target) { 1.1235 + return null; 1.1236 + } 1.1237 + let target = this._target; 1.1238 + this._target = null; 1.1239 + target.off("close", this.destroy); 1.1240 + return target.destroy(); 1.1241 + }).then(() => { 1.1242 + this.emit("destroyed"); 1.1243 + // Free _host after the call to destroyed in order to let a chance 1.1244 + // to destroyed listeners to still query toolbox attributes 1.1245 + this._host = null; 1.1246 + this._toolPanels.clear(); 1.1247 + }).then(null, console.error); 1.1248 + }, 1.1249 + 1.1250 + _highlighterReady: function() { 1.1251 + this.emit("highlighter-ready"); 1.1252 + }, 1.1253 + 1.1254 + _highlighterHidden: function() { 1.1255 + this.emit("highlighter-hide"); 1.1256 + }, 1.1257 +}; 1.1258 + 1.1259 +/** 1.1260 + * The ToolboxHighlighterUtils is what you should use for anything related to 1.1261 + * node highlighting and picking. 1.1262 + * It encapsulates the logic to connecting to the HighlighterActor. 1.1263 + */ 1.1264 +function ToolboxHighlighterUtils(toolbox) { 1.1265 + this.toolbox = toolbox; 1.1266 + this._onPickerNodeHovered = this._onPickerNodeHovered.bind(this); 1.1267 + this._onPickerNodePicked = this._onPickerNodePicked.bind(this); 1.1268 + this.stopPicker = this.stopPicker.bind(this); 1.1269 +} 1.1270 + 1.1271 +ToolboxHighlighterUtils.prototype = { 1.1272 + /** 1.1273 + * Indicates whether the highlighter actor exists on the server. 1.1274 + */ 1.1275 + get isRemoteHighlightable() { 1.1276 + return this.toolbox._target.client.traits.highlightable; 1.1277 + }, 1.1278 + 1.1279 + /** 1.1280 + * Start/stop the element picker on the debuggee target. 1.1281 + */ 1.1282 + togglePicker: function() { 1.1283 + if (this._isPicking) { 1.1284 + return this.stopPicker(); 1.1285 + } else { 1.1286 + return this.startPicker(); 1.1287 + } 1.1288 + }, 1.1289 + 1.1290 + _onPickerNodeHovered: function(res) { 1.1291 + this.toolbox.emit("picker-node-hovered", res.node); 1.1292 + }, 1.1293 + 1.1294 + _onPickerNodePicked: function(res) { 1.1295 + this.toolbox.selection.setNodeFront(res.node, "picker-node-picked"); 1.1296 + this.stopPicker(); 1.1297 + }, 1.1298 + 1.1299 + /** 1.1300 + * Start the element picker on the debuggee target. 1.1301 + * This will request the inspector actor to start listening for mouse/touch 1.1302 + * events on the target to highlight the hovered/picked element. 1.1303 + * Depending on the server-side capabilities, this may fire events when nodes 1.1304 + * are hovered. 1.1305 + * @return A promise that resolves when the picker has started or immediately 1.1306 + * if it is already started 1.1307 + */ 1.1308 + startPicker: function() { 1.1309 + if (this._isPicking) { 1.1310 + return promise.resolve(); 1.1311 + } 1.1312 + 1.1313 + let deferred = promise.defer(); 1.1314 + 1.1315 + let done = () => { 1.1316 + this._isPicking = true; 1.1317 + this.toolbox.emit("picker-started"); 1.1318 + this.toolbox.on("select", this.stopPicker); 1.1319 + deferred.resolve(); 1.1320 + }; 1.1321 + 1.1322 + promise.all([ 1.1323 + this.toolbox.initInspector(), 1.1324 + this.toolbox.selectTool("inspector") 1.1325 + ]).then(() => { 1.1326 + this.toolbox._pickerButton.setAttribute("checked", "true"); 1.1327 + 1.1328 + if (this.isRemoteHighlightable) { 1.1329 + this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered); 1.1330 + this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked); 1.1331 + 1.1332 + this.toolbox.highlighter.pick().then(done); 1.1333 + } else { 1.1334 + return this.toolbox.walker.pick().then(node => { 1.1335 + this.toolbox.selection.setNodeFront(node, "picker-node-picked").then(() => { 1.1336 + this.stopPicker(); 1.1337 + done(); 1.1338 + }); 1.1339 + }); 1.1340 + } 1.1341 + }); 1.1342 + 1.1343 + return deferred.promise; 1.1344 + }, 1.1345 + 1.1346 + /** 1.1347 + * Stop the element picker 1.1348 + * @return A promise that resolves when the picker has stopped or immediately 1.1349 + * if it is already stopped 1.1350 + */ 1.1351 + stopPicker: function() { 1.1352 + if (!this._isPicking) { 1.1353 + return promise.resolve(); 1.1354 + } 1.1355 + 1.1356 + let deferred = promise.defer(); 1.1357 + 1.1358 + let done = () => { 1.1359 + this.toolbox.emit("picker-stopped"); 1.1360 + this.toolbox.off("select", this.stopPicker); 1.1361 + deferred.resolve(); 1.1362 + }; 1.1363 + 1.1364 + this.toolbox.initInspector().then(() => { 1.1365 + this._isPicking = false; 1.1366 + this.toolbox._pickerButton.removeAttribute("checked"); 1.1367 + if (this.isRemoteHighlightable) { 1.1368 + this.toolbox.highlighter.cancelPick().then(done); 1.1369 + this.toolbox.walker.off("picker-node-hovered", this._onPickerNodeHovered); 1.1370 + this.toolbox.walker.off("picker-node-picked", this._onPickerNodePicked); 1.1371 + } else { 1.1372 + this.toolbox.walker.cancelPick().then(done); 1.1373 + } 1.1374 + }); 1.1375 + 1.1376 + return deferred.promise; 1.1377 + }, 1.1378 + 1.1379 + /** 1.1380 + * Show the box model highlighter on a node, given its NodeFront (this type 1.1381 + * of front is normally returned by the WalkerActor). 1.1382 + * @return a promise that resolves to the nodeFront when the node has been 1.1383 + * highlit 1.1384 + */ 1.1385 + highlightNodeFront: function(nodeFront, options={}) { 1.1386 + let deferred = promise.defer(); 1.1387 + 1.1388 + // If the remote highlighter exists on the target, use it 1.1389 + if (this.isRemoteHighlightable) { 1.1390 + this.toolbox.initInspector().then(() => { 1.1391 + this.toolbox.highlighter.showBoxModel(nodeFront, options).then(() => { 1.1392 + this.toolbox.emit("node-highlight", nodeFront); 1.1393 + deferred.resolve(nodeFront); 1.1394 + }); 1.1395 + }); 1.1396 + } 1.1397 + // Else, revert to the "older" version of the highlighter in the walker 1.1398 + // actor 1.1399 + else { 1.1400 + this.toolbox.walker.highlight(nodeFront).then(() => { 1.1401 + this.toolbox.emit("node-highlight", nodeFront); 1.1402 + deferred.resolve(nodeFront); 1.1403 + }); 1.1404 + } 1.1405 + 1.1406 + return deferred.promise; 1.1407 + }, 1.1408 + 1.1409 + /** 1.1410 + * This is a convenience method in case you don't have a nodeFront but a 1.1411 + * valueGrip. This is often the case with VariablesView properties. 1.1412 + * This method will simply translate the grip into a nodeFront and call 1.1413 + * highlightNodeFront 1.1414 + * @return a promise that resolves to the nodeFront when the node has been 1.1415 + * highlit 1.1416 + */ 1.1417 + highlightDomValueGrip: function(valueGrip, options={}) { 1.1418 + return this._translateGripToNodeFront(valueGrip).then(nodeFront => { 1.1419 + if (nodeFront) { 1.1420 + return this.highlightNodeFront(nodeFront, options); 1.1421 + } else { 1.1422 + return promise.reject(); 1.1423 + } 1.1424 + }); 1.1425 + }, 1.1426 + 1.1427 + _translateGripToNodeFront: function(grip) { 1.1428 + return this.toolbox.initInspector().then(() => { 1.1429 + return this.toolbox.walker.getNodeActorFromObjectActor(grip.actor); 1.1430 + }); 1.1431 + }, 1.1432 + 1.1433 + /** 1.1434 + * Hide the highlighter. 1.1435 + * @return a promise that resolves when the highlighter is hidden 1.1436 + */ 1.1437 + unhighlight: function(forceHide=false) { 1.1438 + let unhighlightPromise; 1.1439 + forceHide = forceHide || !gDevTools.testing; 1.1440 + 1.1441 + if (forceHide && this.isRemoteHighlightable && this.toolbox.highlighter) { 1.1442 + // If the remote highlighter exists on the target, use it 1.1443 + unhighlightPromise = this.toolbox.highlighter.hideBoxModel(); 1.1444 + } else { 1.1445 + // If not, no need to unhighlight as the older highlight method uses a 1.1446 + // setTimeout to hide itself 1.1447 + unhighlightPromise = promise.resolve(); 1.1448 + } 1.1449 + 1.1450 + return unhighlightPromise.then(() => { 1.1451 + this.toolbox.emit("node-unhighlight"); 1.1452 + }); 1.1453 + } 1.1454 +};