browser/devtools/framework/toolbox.js

changeset 0
6474c204b198
     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 +};

mercurial