browser/devtools/framework/gDevTools.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/framework/gDevTools.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,927 @@
     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 +this.EXPORTED_SYMBOLS = [ "gDevTools", "DevTools", "gDevToolsBrowser" ];
    1.11 +
    1.12 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
    1.13 +
    1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.15 +Cu.import("resource://gre/modules/Services.jsm");
    1.16 +Cu.import("resource://gre/modules/devtools/event-emitter.js");
    1.17 +Cu.import("resource://gre/modules/devtools/Loader.jsm");
    1.18 +XPCOMUtils.defineLazyModuleGetter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
    1.19 +
    1.20 +const FORBIDDEN_IDS = new Set(["toolbox", ""]);
    1.21 +const MAX_ORDINAL = 99;
    1.22 +
    1.23 +
    1.24 +/**
    1.25 + * DevTools is a class that represents a set of developer tools, it holds a
    1.26 + * set of tools and keeps track of open toolboxes in the browser.
    1.27 + */
    1.28 +this.DevTools = function DevTools() {
    1.29 +  this._tools = new Map();     // Map<toolId, tool>
    1.30 +  this._toolboxes = new Map(); // Map<target, toolbox>
    1.31 +
    1.32 +  // destroy() is an observer's handler so we need to preserve context.
    1.33 +  this.destroy = this.destroy.bind(this);
    1.34 +  this._teardown = this._teardown.bind(this);
    1.35 +
    1.36 +  this._testing = false;
    1.37 +
    1.38 +  EventEmitter.decorate(this);
    1.39 +
    1.40 +  Services.obs.addObserver(this._teardown, "devtools-unloaded", false);
    1.41 +  Services.obs.addObserver(this.destroy, "quit-application", false);
    1.42 +}
    1.43 +
    1.44 +DevTools.prototype = {
    1.45 +  /**
    1.46 +   * When the testing flag is set we take appropriate action to prevent race
    1.47 +   * conditions in our testing environment. This means setting
    1.48 +   * dom.send_after_paint_to_content to false to prevent infinite MozAfterPaint
    1.49 +   * loops and not autohiding the highlighter.
    1.50 +   */
    1.51 +  get testing() {
    1.52 +    return this._testing;
    1.53 +  },
    1.54 +
    1.55 +  set testing(state) {
    1.56 +    this._testing = state;
    1.57 +
    1.58 +    if (state) {
    1.59 +      // dom.send_after_paint_to_content is set to true (non-default) in
    1.60 +      // testing/profiles/prefs_general.js so lets set it to the same as it is
    1.61 +      // in a default browser profile for the duration of the test.
    1.62 +      Services.prefs.setBoolPref("dom.send_after_paint_to_content", false);
    1.63 +    } else {
    1.64 +      Services.prefs.setBoolPref("dom.send_after_paint_to_content", true);
    1.65 +    }
    1.66 +  },
    1.67 +
    1.68 +  /**
    1.69 +   * Register a new developer tool.
    1.70 +   *
    1.71 +   * A definition is a light object that holds different information about a
    1.72 +   * developer tool. This object is not supposed to have any operational code.
    1.73 +   * See it as a "manifest".
    1.74 +   * The only actual code lives in the build() function, which will be used to
    1.75 +   * start an instance of this tool.
    1.76 +   *
    1.77 +   * Each toolDefinition has the following properties:
    1.78 +   * - id: Unique identifier for this tool (string|required)
    1.79 +   * - visibilityswitch: Property name to allow us to hide this tool from the
    1.80 +   *                     DevTools Toolbox.
    1.81 +   *                     A falsy value indicates that it cannot be hidden.
    1.82 +   * - icon: URL pointing to a graphic which will be used as the src for an
    1.83 +   *         16x16 img tag (string|required)
    1.84 +   * - invertIconForLightTheme: The icon can automatically have an inversion
    1.85 +   *         filter applied (default is false).  All builtin tools are true, but
    1.86 +   *         addons may omit this to prevent unwanted changes to the `icon`
    1.87 +   *         image. See browser/themes/shared/devtools/filters.svg#invert for
    1.88 +   *         the filter being applied to the images (boolean|optional)
    1.89 +   * - url: URL pointing to a XUL/XHTML document containing the user interface
    1.90 +   *        (string|required)
    1.91 +   * - label: Localized name for the tool to be displayed to the user
    1.92 +   *          (string|required)
    1.93 +   * - build: Function that takes an iframe, which has been populated with the
    1.94 +   *          markup from |url|, and also the toolbox containing the panel.
    1.95 +   *          And returns an instance of ToolPanel (function|required)
    1.96 +   */
    1.97 +  registerTool: function DT_registerTool(toolDefinition) {
    1.98 +    let toolId = toolDefinition.id;
    1.99 +
   1.100 +    if (!toolId || FORBIDDEN_IDS.has(toolId)) {
   1.101 +      throw new Error("Invalid definition.id");
   1.102 +    }
   1.103 +
   1.104 +    // Make sure that additional tools will always be able to be hidden.
   1.105 +    // When being called from main.js, defaultTools has not yet been exported.
   1.106 +    // But, we can assume that in this case, it is a default tool.
   1.107 +    if (devtools.defaultTools && devtools.defaultTools.indexOf(toolDefinition) == -1) {
   1.108 +      toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled";
   1.109 +    }
   1.110 +
   1.111 +    this._tools.set(toolId, toolDefinition);
   1.112 +
   1.113 +    this.emit("tool-registered", toolId);
   1.114 +  },
   1.115 +
   1.116 +  /**
   1.117 +   * Removes all tools that match the given |toolId|
   1.118 +   * Needed so that add-ons can remove themselves when they are deactivated
   1.119 +   *
   1.120 +   * @param {string|object} tool
   1.121 +   *        Definition or the id of the tool to unregister. Passing the
   1.122 +   *        tool id should be avoided as it is a temporary measure.
   1.123 +   * @param {boolean} isQuitApplication
   1.124 +   *        true to indicate that the call is due to app quit, so we should not
   1.125 +   *        cause a cascade of costly events
   1.126 +   */
   1.127 +  unregisterTool: function DT_unregisterTool(tool, isQuitApplication) {
   1.128 +    let toolId = null;
   1.129 +    if (typeof tool == "string") {
   1.130 +      toolId = tool;
   1.131 +      tool = this._tools.get(tool);
   1.132 +    }
   1.133 +    else {
   1.134 +      toolId = tool.id;
   1.135 +    }
   1.136 +    this._tools.delete(toolId);
   1.137 +
   1.138 +    if (!isQuitApplication) {
   1.139 +      this.emit("tool-unregistered", tool);
   1.140 +    }
   1.141 +  },
   1.142 +
   1.143 +  /**
   1.144 +   * Sorting function used for sorting tools based on their ordinals.
   1.145 +   */
   1.146 +  ordinalSort: function DT_ordinalSort(d1, d2) {
   1.147 +    let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL;
   1.148 +    let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL;
   1.149 +    return o1 - o2;
   1.150 +  },
   1.151 +
   1.152 +  getDefaultTools: function DT_getDefaultTools() {
   1.153 +    return devtools.defaultTools.sort(this.ordinalSort);
   1.154 +  },
   1.155 +
   1.156 +  getAdditionalTools: function DT_getAdditionalTools() {
   1.157 +    let tools = [];
   1.158 +    for (let [key, value] of this._tools) {
   1.159 +      if (devtools.defaultTools.indexOf(value) == -1) {
   1.160 +        tools.push(value);
   1.161 +      }
   1.162 +    }
   1.163 +    return tools.sort(this.ordinalSort);
   1.164 +  },
   1.165 +
   1.166 +  /**
   1.167 +   * Get a tool definition if it exists and is enabled.
   1.168 +   *
   1.169 +   * @param {string} toolId
   1.170 +   *        The id of the tool to show
   1.171 +   *
   1.172 +   * @return {ToolDefinition|null} tool
   1.173 +   *         The ToolDefinition for the id or null.
   1.174 +   */
   1.175 +  getToolDefinition: function DT_getToolDefinition(toolId) {
   1.176 +    let tool = this._tools.get(toolId);
   1.177 +    if (!tool) {
   1.178 +      return null;
   1.179 +    } else if (!tool.visibilityswitch) {
   1.180 +      return tool;
   1.181 +    }
   1.182 +
   1.183 +    let enabled;
   1.184 +    try {
   1.185 +      enabled = Services.prefs.getBoolPref(tool.visibilityswitch);
   1.186 +    } catch (e) {
   1.187 +      enabled = true;
   1.188 +    }
   1.189 +
   1.190 +    return enabled ? tool : null;
   1.191 +  },
   1.192 +
   1.193 +  /**
   1.194 +   * Allow ToolBoxes to get at the list of tools that they should populate
   1.195 +   * themselves with.
   1.196 +   *
   1.197 +   * @return {Map} tools
   1.198 +   *         A map of the the tool definitions registered in this instance
   1.199 +   */
   1.200 +  getToolDefinitionMap: function DT_getToolDefinitionMap() {
   1.201 +    let tools = new Map();
   1.202 +
   1.203 +    for (let [id, definition] of this._tools) {
   1.204 +      if (this.getToolDefinition(id)) {
   1.205 +        tools.set(id, definition);
   1.206 +      }
   1.207 +    }
   1.208 +
   1.209 +    return tools;
   1.210 +  },
   1.211 +
   1.212 +  /**
   1.213 +   * Tools have an inherent ordering that can't be represented in a Map so
   1.214 +   * getToolDefinitionArray provides an alternative representation of the
   1.215 +   * definitions sorted by ordinal value.
   1.216 +   *
   1.217 +   * @return {Array} tools
   1.218 +   *         A sorted array of the tool definitions registered in this instance
   1.219 +   */
   1.220 +  getToolDefinitionArray: function DT_getToolDefinitionArray() {
   1.221 +    let definitions = [];
   1.222 +
   1.223 +    for (let [id, definition] of this._tools) {
   1.224 +      if (this.getToolDefinition(id)) {
   1.225 +        definitions.push(definition);
   1.226 +      }
   1.227 +    }
   1.228 +
   1.229 +    return definitions.sort(this.ordinalSort);
   1.230 +  },
   1.231 +
   1.232 +  /**
   1.233 +   * Show a Toolbox for a target (either by creating a new one, or if a toolbox
   1.234 +   * already exists for the target, by bring to the front the existing one)
   1.235 +   * If |toolId| is specified then the displayed toolbox will have the
   1.236 +   * specified tool selected.
   1.237 +   * If |hostType| is specified then the toolbox will be displayed using the
   1.238 +   * specified HostType.
   1.239 +   *
   1.240 +   * @param {Target} target
   1.241 +   *         The target the toolbox will debug
   1.242 +   * @param {string} toolId
   1.243 +   *        The id of the tool to show
   1.244 +   * @param {Toolbox.HostType} hostType
   1.245 +   *        The type of host (bottom, window, side)
   1.246 +   * @param {object} hostOptions
   1.247 +   *        Options for host specifically
   1.248 +   *
   1.249 +   * @return {Toolbox} toolbox
   1.250 +   *        The toolbox that was opened
   1.251 +   */
   1.252 +  showToolbox: function(target, toolId, hostType, hostOptions) {
   1.253 +    let deferred = promise.defer();
   1.254 +
   1.255 +    let toolbox = this._toolboxes.get(target);
   1.256 +    if (toolbox) {
   1.257 +
   1.258 +      let hostPromise = (hostType != null && toolbox.hostType != hostType) ?
   1.259 +          toolbox.switchHost(hostType) :
   1.260 +          promise.resolve(null);
   1.261 +
   1.262 +      if (toolId != null && toolbox.currentToolId != toolId) {
   1.263 +        hostPromise = hostPromise.then(function() {
   1.264 +          return toolbox.selectTool(toolId);
   1.265 +        });
   1.266 +      }
   1.267 +
   1.268 +      return hostPromise.then(function() {
   1.269 +        toolbox.raise();
   1.270 +        return toolbox;
   1.271 +      });
   1.272 +    }
   1.273 +    else {
   1.274 +      // No toolbox for target, create one
   1.275 +      toolbox = new devtools.Toolbox(target, toolId, hostType, hostOptions);
   1.276 +
   1.277 +      this._toolboxes.set(target, toolbox);
   1.278 +
   1.279 +      toolbox.once("destroyed", function() {
   1.280 +        this._toolboxes.delete(target);
   1.281 +        this.emit("toolbox-destroyed", target);
   1.282 +      }.bind(this));
   1.283 +
   1.284 +      // If we were asked for a specific tool then we need to wait for the
   1.285 +      // tool to be ready, otherwise we can just wait for toolbox open
   1.286 +      if (toolId != null) {
   1.287 +        toolbox.once(toolId + "-ready", function(event, panel) {
   1.288 +          this.emit("toolbox-ready", toolbox);
   1.289 +          deferred.resolve(toolbox);
   1.290 +        }.bind(this));
   1.291 +        toolbox.open();
   1.292 +      }
   1.293 +      else {
   1.294 +        toolbox.open().then(function() {
   1.295 +          deferred.resolve(toolbox);
   1.296 +          this.emit("toolbox-ready", toolbox);
   1.297 +        }.bind(this));
   1.298 +      }
   1.299 +    }
   1.300 +
   1.301 +    return deferred.promise;
   1.302 +  },
   1.303 +
   1.304 +  /**
   1.305 +   * Return the toolbox for a given target.
   1.306 +   *
   1.307 +   * @param  {object} target
   1.308 +   *         Target value e.g. the target that owns this toolbox
   1.309 +   *
   1.310 +   * @return {Toolbox} toolbox
   1.311 +   *         The toobox that is debugging the given target
   1.312 +   */
   1.313 +  getToolbox: function DT_getToolbox(target) {
   1.314 +    return this._toolboxes.get(target);
   1.315 +  },
   1.316 +
   1.317 +  /**
   1.318 +   * Close the toolbox for a given target
   1.319 +   *
   1.320 +   * @return promise
   1.321 +   *         This promise will resolve to false if no toolbox was found
   1.322 +   *         associated to the target. true, if the toolbox was successfuly
   1.323 +   *         closed.
   1.324 +   */
   1.325 +  closeToolbox: function DT_closeToolbox(target) {
   1.326 +    let toolbox = this._toolboxes.get(target);
   1.327 +    if (toolbox == null) {
   1.328 +      return promise.resolve(false);
   1.329 +    }
   1.330 +    return toolbox.destroy().then(() => true);
   1.331 +  },
   1.332 +
   1.333 +  /**
   1.334 +   * Called to tear down a tools provider.
   1.335 +   */
   1.336 +  _teardown: function DT_teardown() {
   1.337 +    for (let [target, toolbox] of this._toolboxes) {
   1.338 +      toolbox.destroy();
   1.339 +    }
   1.340 +  },
   1.341 +
   1.342 +  /**
   1.343 +   * All browser windows have been closed, tidy up remaining objects.
   1.344 +   */
   1.345 +  destroy: function() {
   1.346 +    Services.obs.removeObserver(this.destroy, "quit-application");
   1.347 +    Services.obs.removeObserver(this._teardown, "devtools-unloaded");
   1.348 +
   1.349 +    for (let [key, tool] of this.getToolDefinitionMap()) {
   1.350 +      this.unregisterTool(key, true);
   1.351 +    }
   1.352 +
   1.353 +    // Cleaning down the toolboxes: i.e.
   1.354 +    //   for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
   1.355 +    // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
   1.356 +  },
   1.357 +
   1.358 +  /**
   1.359 +   * Iterator that yields each of the toolboxes.
   1.360 +   */
   1.361 +  '@@iterator': function*() {
   1.362 +    for (let toolbox of this._toolboxes) {
   1.363 +      yield toolbox;
   1.364 +    }
   1.365 +  }
   1.366 +};
   1.367 +
   1.368 +/**
   1.369 + * gDevTools is a singleton that controls the Firefox Developer Tools.
   1.370 + *
   1.371 + * It is an instance of a DevTools class that holds a set of tools. It has the
   1.372 + * same lifetime as the browser.
   1.373 + */
   1.374 +let gDevTools = new DevTools();
   1.375 +this.gDevTools = gDevTools;
   1.376 +
   1.377 +/**
   1.378 + * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
   1.379 + * Firefox instance.
   1.380 + */
   1.381 +let gDevToolsBrowser = {
   1.382 +  /**
   1.383 +   * A record of the windows whose menus we altered, so we can undo the changes
   1.384 +   * as the window is closed
   1.385 +   */
   1.386 +  _trackedBrowserWindows: new Set(),
   1.387 +
   1.388 +  /**
   1.389 +   * This function is for the benefit of Tools:DevToolbox in
   1.390 +   * browser/base/content/browser-sets.inc and should not be used outside
   1.391 +   * of there
   1.392 +   */
   1.393 +  toggleToolboxCommand: function(gBrowser) {
   1.394 +    let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
   1.395 +    let toolbox = gDevTools.getToolbox(target);
   1.396 +
   1.397 +    toolbox ? toolbox.destroy() : gDevTools.showToolbox(target);
   1.398 +  },
   1.399 +
   1.400 +  toggleBrowserToolboxCommand: function(gBrowser) {
   1.401 +    let target = devtools.TargetFactory.forWindow(gBrowser.ownerDocument.defaultView);
   1.402 +    let toolbox = gDevTools.getToolbox(target);
   1.403 +
   1.404 +    toolbox ? toolbox.destroy()
   1.405 +     : gDevTools.showToolbox(target, "inspector", Toolbox.HostType.WINDOW);
   1.406 +  },
   1.407 +
   1.408 +  /**
   1.409 +   * This function ensures the right commands are enabled in a window,
   1.410 +   * depending on their relevant prefs. It gets run when a window is registered,
   1.411 +   * or when any of the devtools prefs change.
   1.412 +   */
   1.413 +  updateCommandAvailability: function(win) {
   1.414 +    let doc = win.document;
   1.415 +
   1.416 +    function toggleCmd(id, isEnabled) {
   1.417 +      let cmd = doc.getElementById(id);
   1.418 +      if (isEnabled) {
   1.419 +        cmd.removeAttribute("disabled");
   1.420 +        cmd.removeAttribute("hidden");
   1.421 +      } else {
   1.422 +        cmd.setAttribute("disabled", "true");
   1.423 +        cmd.setAttribute("hidden", "true");
   1.424 +      }
   1.425 +    };
   1.426 +
   1.427 +    // Enable developer toolbar?
   1.428 +    let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled");
   1.429 +    toggleCmd("Tools:DevToolbar", devToolbarEnabled);
   1.430 +    let focusEl = doc.getElementById("Tools:DevToolbarFocus");
   1.431 +    if (devToolbarEnabled) {
   1.432 +      focusEl.removeAttribute("disabled");
   1.433 +    } else {
   1.434 +      focusEl.setAttribute("disabled", "true");
   1.435 +    }
   1.436 +    if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) {
   1.437 +      win.DeveloperToolbar.show(false);
   1.438 +    }
   1.439 +
   1.440 +    // Enable App Manager?
   1.441 +    let appMgrEnabled = Services.prefs.getBoolPref("devtools.appmanager.enabled");
   1.442 +    toggleCmd("Tools:DevAppMgr", appMgrEnabled);
   1.443 +
   1.444 +    // Enable Browser Toolbox?
   1.445 +    let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
   1.446 +    let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
   1.447 +    let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled &&
   1.448 +                        Services.prefs.getBoolPref("devtools.debugger.chrome-enabled");
   1.449 +    toggleCmd("Tools:BrowserToolbox", remoteEnabled);
   1.450 +
   1.451 +    // Enable Error Console?
   1.452 +    let consoleEnabled = Services.prefs.getBoolPref("devtools.errorconsole.enabled");
   1.453 +    toggleCmd("Tools:ErrorConsole", consoleEnabled);
   1.454 +
   1.455 +    // Enable DevTools connection screen, if the preference allows this.
   1.456 +    toggleCmd("Tools:DevToolsConnect", devtoolsRemoteEnabled);
   1.457 +  },
   1.458 +
   1.459 +  observe: function(subject, topic, prefName) {
   1.460 +    if (prefName.endsWith("enabled")) {
   1.461 +      for (let win of this._trackedBrowserWindows) {
   1.462 +        this.updateCommandAvailability(win);
   1.463 +      }
   1.464 +    }
   1.465 +  },
   1.466 +
   1.467 +  _prefObserverRegistered: false,
   1.468 +
   1.469 +  ensurePrefObserver: function() {
   1.470 +    if (!this._prefObserverRegistered) {
   1.471 +      this._prefObserverRegistered = true;
   1.472 +      Services.prefs.addObserver("devtools.", this, false);
   1.473 +    }
   1.474 +  },
   1.475 +
   1.476 +
   1.477 +  /**
   1.478 +   * This function is for the benefit of Tools:{toolId} commands,
   1.479 +   * triggered from the WebDeveloper menu and keyboard shortcuts.
   1.480 +   *
   1.481 +   * selectToolCommand's behavior:
   1.482 +   * - if the toolbox is closed,
   1.483 +   *   we open the toolbox and select the tool
   1.484 +   * - if the toolbox is open, and the targetted tool is not selected,
   1.485 +   *   we select it
   1.486 +   * - if the toolbox is open, and the targetted tool is selected,
   1.487 +   *   and the host is NOT a window, we close the toolbox
   1.488 +   * - if the toolbox is open, and the targetted tool is selected,
   1.489 +   *   and the host is a window, we raise the toolbox window
   1.490 +   */
   1.491 +  selectToolCommand: function(gBrowser, toolId) {
   1.492 +    let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
   1.493 +    let toolbox = gDevTools.getToolbox(target);
   1.494 +    let toolDefinition = gDevTools.getToolDefinition(toolId);
   1.495 +
   1.496 +    if (toolbox &&
   1.497 +        (toolbox.currentToolId == toolId ||
   1.498 +          (toolId == "webconsole" && toolbox.splitConsole)))
   1.499 +    {
   1.500 +      toolbox.fireCustomKey(toolId);
   1.501 +
   1.502 +      if (toolDefinition.preventClosingOnKey || toolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
   1.503 +        toolbox.raise();
   1.504 +      } else {
   1.505 +        toolbox.destroy();
   1.506 +      }
   1.507 +    } else {
   1.508 +      gDevTools.showToolbox(target, toolId).then(() => {
   1.509 +        let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
   1.510 +        let toolbox = gDevTools.getToolbox(target);
   1.511 +
   1.512 +        toolbox.fireCustomKey(toolId);
   1.513 +      });
   1.514 +    }
   1.515 +  },
   1.516 +
   1.517 +  /**
   1.518 +   * Open a tab to allow connects to a remote browser
   1.519 +   */
   1.520 +  openConnectScreen: function(gBrowser) {
   1.521 +    gBrowser.selectedTab = gBrowser.addTab("chrome://browser/content/devtools/connect.xhtml");
   1.522 +  },
   1.523 +
   1.524 +  /**
   1.525 +   * Open the App Manager
   1.526 +   */
   1.527 +  openAppManager: function(gBrowser) {
   1.528 +    gBrowser.selectedTab = gBrowser.addTab("about:app-manager");
   1.529 +  },
   1.530 +
   1.531 +  /**
   1.532 +   * Add this DevTools's presence to a browser window's document
   1.533 +   *
   1.534 +   * @param {XULDocument} doc
   1.535 +   *        The document to which menuitems and handlers are to be added
   1.536 +   */
   1.537 +  registerBrowserWindow: function DT_registerBrowserWindow(win) {
   1.538 +    this.updateCommandAvailability(win);
   1.539 +    this.ensurePrefObserver();
   1.540 +    gDevToolsBrowser._trackedBrowserWindows.add(win);
   1.541 +    gDevToolsBrowser._addAllToolsToMenu(win.document);
   1.542 +
   1.543 +    if (this._isFirebugInstalled()) {
   1.544 +      let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
   1.545 +      broadcaster.removeAttribute("key");
   1.546 +    }
   1.547 +
   1.548 +    let tabContainer = win.document.getElementById("tabbrowser-tabs")
   1.549 +    tabContainer.addEventListener("TabSelect",
   1.550 +                                  gDevToolsBrowser._updateMenuCheckbox, false);
   1.551 +  },
   1.552 +
   1.553 +  /**
   1.554 +   * Add a <key> to <keyset id="devtoolsKeyset">.
   1.555 +   * Appending a <key> element is not always enough. The <keyset> needs
   1.556 +   * to be detached and reattached to make sure the <key> is taken into
   1.557 +   * account (see bug 832984).
   1.558 +   *
   1.559 +   * @param {XULDocument} doc
   1.560 +   *        The document to which keys are to be added
   1.561 +   * @param {XULElement} or {DocumentFragment} keys
   1.562 +   *        Keys to add
   1.563 +   */
   1.564 +  attachKeybindingsToBrowser: function DT_attachKeybindingsToBrowser(doc, keys) {
   1.565 +    let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
   1.566 +
   1.567 +    if (!devtoolsKeyset) {
   1.568 +      devtoolsKeyset = doc.createElement("keyset");
   1.569 +      devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
   1.570 +    }
   1.571 +    devtoolsKeyset.appendChild(keys);
   1.572 +    let mainKeyset = doc.getElementById("mainKeyset");
   1.573 +    mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
   1.574 +  },
   1.575 +
   1.576 +
   1.577 +  /**
   1.578 +   * Detect the presence of a Firebug.
   1.579 +   *
   1.580 +   * @return promise
   1.581 +   */
   1.582 +  _isFirebugInstalled: function DT_isFirebugInstalled() {
   1.583 +    let bootstrappedAddons = Services.prefs.getCharPref("extensions.bootstrappedAddons");
   1.584 +    return bootstrappedAddons.indexOf("firebug@software.joehewitt.com") != -1;
   1.585 +  },
   1.586 +
   1.587 +  /**
   1.588 +   * Add the menuitem for a tool to all open browser windows.
   1.589 +   *
   1.590 +   * @param {object} toolDefinition
   1.591 +   *        properties of the tool to add
   1.592 +   */
   1.593 +  _addToolToWindows: function DT_addToolToWindows(toolDefinition) {
   1.594 +    // No menu item or global shortcut is required for options panel.
   1.595 +    if (!toolDefinition.inMenu) {
   1.596 +      return;
   1.597 +    }
   1.598 +
   1.599 +    // Skip if the tool is disabled.
   1.600 +    try {
   1.601 +      if (toolDefinition.visibilityswitch &&
   1.602 +         !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) {
   1.603 +        return;
   1.604 +      }
   1.605 +    } catch(e) {}
   1.606 +
   1.607 +    // We need to insert the new tool in the right place, which means knowing
   1.608 +    // the tool that comes before the tool that we're trying to add
   1.609 +    let allDefs = gDevTools.getToolDefinitionArray();
   1.610 +    let prevDef;
   1.611 +    for (let def of allDefs) {
   1.612 +      if (!def.inMenu) {
   1.613 +        continue;
   1.614 +      }
   1.615 +      if (def === toolDefinition) {
   1.616 +        break;
   1.617 +      }
   1.618 +      prevDef = def;
   1.619 +    }
   1.620 +
   1.621 +    for (let win of gDevToolsBrowser._trackedBrowserWindows) {
   1.622 +      let doc = win.document;
   1.623 +      let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
   1.624 +
   1.625 +      doc.getElementById("mainCommandSet").appendChild(elements.cmd);
   1.626 +
   1.627 +      if (elements.key) {
   1.628 +        this.attachKeybindingsToBrowser(doc, elements.key);
   1.629 +      }
   1.630 +
   1.631 +      doc.getElementById("mainBroadcasterSet").appendChild(elements.bc);
   1.632 +
   1.633 +      let amp = doc.getElementById("appmenu_webDeveloper_popup");
   1.634 +      if (amp) {
   1.635 +        let ref;
   1.636 +
   1.637 +        if (prevDef != null) {
   1.638 +          let menuitem = doc.getElementById("appmenuitem_" + prevDef.id);
   1.639 +          ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
   1.640 +        } else {
   1.641 +          ref = doc.getElementById("appmenu_devtools_separator");
   1.642 +        }
   1.643 +
   1.644 +        if (ref) {
   1.645 +          amp.insertBefore(elements.appmenuitem, ref);
   1.646 +        }
   1.647 +      }
   1.648 +
   1.649 +      let mp = doc.getElementById("menuWebDeveloperPopup");
   1.650 +      if (mp) {
   1.651 +        let ref;
   1.652 +
   1.653 +        if (prevDef != null) {
   1.654 +          let menuitem = doc.getElementById("menuitem_" + prevDef.id);
   1.655 +          ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
   1.656 +        } else {
   1.657 +          ref = doc.getElementById("menu_devtools_separator");
   1.658 +        }
   1.659 +
   1.660 +        if (ref) {
   1.661 +          mp.insertBefore(elements.menuitem, ref);
   1.662 +        }
   1.663 +      }
   1.664 +    }
   1.665 +  },
   1.666 +
   1.667 +  /**
   1.668 +   * Add all tools to the developer tools menu of a window.
   1.669 +   *
   1.670 +   * @param {XULDocument} doc
   1.671 +   *        The document to which the tool items are to be added.
   1.672 +   */
   1.673 +  _addAllToolsToMenu: function DT_addAllToolsToMenu(doc) {
   1.674 +    let fragCommands = doc.createDocumentFragment();
   1.675 +    let fragKeys = doc.createDocumentFragment();
   1.676 +    let fragBroadcasters = doc.createDocumentFragment();
   1.677 +    let fragAppMenuItems = doc.createDocumentFragment();
   1.678 +    let fragMenuItems = doc.createDocumentFragment();
   1.679 +
   1.680 +    for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
   1.681 +      if (!toolDefinition.inMenu) {
   1.682 +        continue;
   1.683 +      }
   1.684 +
   1.685 +      let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
   1.686 +
   1.687 +      if (!elements) {
   1.688 +        return;
   1.689 +      }
   1.690 +
   1.691 +      fragCommands.appendChild(elements.cmd);
   1.692 +      if (elements.key) {
   1.693 +        fragKeys.appendChild(elements.key);
   1.694 +      }
   1.695 +      fragBroadcasters.appendChild(elements.bc);
   1.696 +      fragAppMenuItems.appendChild(elements.appmenuitem);
   1.697 +      fragMenuItems.appendChild(elements.menuitem);
   1.698 +    }
   1.699 +
   1.700 +    let mcs = doc.getElementById("mainCommandSet");
   1.701 +    mcs.appendChild(fragCommands);
   1.702 +
   1.703 +    this.attachKeybindingsToBrowser(doc, fragKeys);
   1.704 +
   1.705 +    let mbs = doc.getElementById("mainBroadcasterSet");
   1.706 +    mbs.appendChild(fragBroadcasters);
   1.707 +
   1.708 +    let amp = doc.getElementById("appmenu_webDeveloper_popup");
   1.709 +    if (amp) {
   1.710 +      let amps = doc.getElementById("appmenu_devtools_separator");
   1.711 +      amp.insertBefore(fragAppMenuItems, amps);
   1.712 +    }
   1.713 +
   1.714 +    let mp = doc.getElementById("menuWebDeveloperPopup");
   1.715 +    let mps = doc.getElementById("menu_devtools_separator");
   1.716 +    mp.insertBefore(fragMenuItems, mps);
   1.717 +  },
   1.718 +
   1.719 +  /**
   1.720 +   * Add a menu entry for a tool definition
   1.721 +   *
   1.722 +   * @param {string} toolDefinition
   1.723 +   *        Tool definition of the tool to add a menu entry.
   1.724 +   * @param {XULDocument} doc
   1.725 +   *        The document to which the tool menu item is to be added.
   1.726 +   */
   1.727 +  _createToolMenuElements: function DT_createToolMenuElements(toolDefinition, doc) {
   1.728 +    let id = toolDefinition.id;
   1.729 +
   1.730 +    // Prevent multiple entries for the same tool.
   1.731 +    if (doc.getElementById("Tools:" + id)) {
   1.732 +      return;
   1.733 +    }
   1.734 +
   1.735 +    let cmd = doc.createElement("command");
   1.736 +    cmd.id = "Tools:" + id;
   1.737 +    cmd.setAttribute("oncommand",
   1.738 +        'gDevToolsBrowser.selectToolCommand(gBrowser, "' + id + '");');
   1.739 +
   1.740 +    let key = null;
   1.741 +    if (toolDefinition.key) {
   1.742 +      key = doc.createElement("key");
   1.743 +      key.id = "key_" + id;
   1.744 +
   1.745 +      if (toolDefinition.key.startsWith("VK_")) {
   1.746 +        key.setAttribute("keycode", toolDefinition.key);
   1.747 +      } else {
   1.748 +        key.setAttribute("key", toolDefinition.key);
   1.749 +      }
   1.750 +
   1.751 +      key.setAttribute("command", cmd.id);
   1.752 +      key.setAttribute("modifiers", toolDefinition.modifiers);
   1.753 +    }
   1.754 +
   1.755 +    let bc = doc.createElement("broadcaster");
   1.756 +    bc.id = "devtoolsMenuBroadcaster_" + id;
   1.757 +    bc.setAttribute("label", toolDefinition.menuLabel || toolDefinition.label);
   1.758 +    bc.setAttribute("command", cmd.id);
   1.759 +
   1.760 +    if (key) {
   1.761 +      bc.setAttribute("key", "key_" + id);
   1.762 +    }
   1.763 +
   1.764 +    let appmenuitem = doc.createElement("menuitem");
   1.765 +    appmenuitem.id = "appmenuitem_" + id;
   1.766 +    appmenuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id);
   1.767 +
   1.768 +    let menuitem = doc.createElement("menuitem");
   1.769 +    menuitem.id = "menuitem_" + id;
   1.770 +    menuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id);
   1.771 +
   1.772 +    if (toolDefinition.accesskey) {
   1.773 +      menuitem.setAttribute("accesskey", toolDefinition.accesskey);
   1.774 +    }
   1.775 +
   1.776 +    return {
   1.777 +      cmd: cmd,
   1.778 +      key: key,
   1.779 +      bc: bc,
   1.780 +      appmenuitem: appmenuitem,
   1.781 +      menuitem: menuitem
   1.782 +    };
   1.783 +  },
   1.784 +
   1.785 +  /**
   1.786 +   * Update the "Toggle Tools" checkbox in the developer tools menu. This is
   1.787 +   * called when a toolbox is created or destroyed.
   1.788 +   */
   1.789 +  _updateMenuCheckbox: function DT_updateMenuCheckbox() {
   1.790 +    for (let win of gDevToolsBrowser._trackedBrowserWindows) {
   1.791 +
   1.792 +      let hasToolbox = false;
   1.793 +      if (devtools.TargetFactory.isKnownTab(win.gBrowser.selectedTab)) {
   1.794 +        let target = devtools.TargetFactory.forTab(win.gBrowser.selectedTab);
   1.795 +        if (gDevTools._toolboxes.has(target)) {
   1.796 +          hasToolbox = true;
   1.797 +        }
   1.798 +      }
   1.799 +
   1.800 +      let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
   1.801 +      if (hasToolbox) {
   1.802 +        broadcaster.setAttribute("checked", "true");
   1.803 +      } else {
   1.804 +        broadcaster.removeAttribute("checked");
   1.805 +      }
   1.806 +    }
   1.807 +  },
   1.808 +
   1.809 +  /**
   1.810 +   * Connects to the SPS profiler when the developer tools are open.
   1.811 +   */
   1.812 +  _connectToProfiler: function DT_connectToProfiler() {
   1.813 +    let ProfilerController = devtools.require("devtools/profiler/controller");
   1.814 +
   1.815 +    for (let win of gDevToolsBrowser._trackedBrowserWindows) {
   1.816 +      if (devtools.TargetFactory.isKnownTab(win.gBrowser.selectedTab)) {
   1.817 +        let target = devtools.TargetFactory.forTab(win.gBrowser.selectedTab);
   1.818 +        if (gDevTools._toolboxes.has(target)) {
   1.819 +          target.makeRemote().then(() => {
   1.820 +            let profiler = new ProfilerController(target);
   1.821 +            profiler.connect();
   1.822 +          }).then(null, Cu.reportError);
   1.823 +
   1.824 +          return;
   1.825 +        }
   1.826 +      }
   1.827 +    }
   1.828 +  },
   1.829 +
   1.830 +  /**
   1.831 +   * Remove the menuitem for a tool to all open browser windows.
   1.832 +   *
   1.833 +   * @param {string} toolId
   1.834 +   *        id of the tool to remove
   1.835 +   */
   1.836 +  _removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
   1.837 +    for (let win of gDevToolsBrowser._trackedBrowserWindows) {
   1.838 +      gDevToolsBrowser._removeToolFromMenu(toolId, win.document);
   1.839 +    }
   1.840 +  },
   1.841 +
   1.842 +  /**
   1.843 +   * Remove a tool's menuitem from a window
   1.844 +   *
   1.845 +   * @param {string} toolId
   1.846 +   *        Id of the tool to add a menu entry for
   1.847 +   * @param {XULDocument} doc
   1.848 +   *        The document to which the tool menu item is to be removed from
   1.849 +   */
   1.850 +  _removeToolFromMenu: function DT_removeToolFromMenu(toolId, doc) {
   1.851 +    let command = doc.getElementById("Tools:" + toolId);
   1.852 +    if (command) {
   1.853 +      command.parentNode.removeChild(command);
   1.854 +    }
   1.855 +
   1.856 +    let key = doc.getElementById("key_" + toolId);
   1.857 +    if (key) {
   1.858 +      key.parentNode.removeChild(key);
   1.859 +    }
   1.860 +
   1.861 +    let bc = doc.getElementById("devtoolsMenuBroadcaster_" + toolId);
   1.862 +    if (bc) {
   1.863 +      bc.parentNode.removeChild(bc);
   1.864 +    }
   1.865 +
   1.866 +    let appmenuitem = doc.getElementById("appmenuitem_" + toolId);
   1.867 +    if (appmenuitem) {
   1.868 +      appmenuitem.parentNode.removeChild(appmenuitem);
   1.869 +    }
   1.870 +
   1.871 +    let menuitem = doc.getElementById("menuitem_" + toolId);
   1.872 +    if (menuitem) {
   1.873 +      menuitem.parentNode.removeChild(menuitem);
   1.874 +    }
   1.875 +  },
   1.876 +
   1.877 +  /**
   1.878 +   * Called on browser unload to remove menu entries, toolboxes and event
   1.879 +   * listeners from the closed browser window.
   1.880 +   *
   1.881 +   * @param  {XULWindow} win
   1.882 +   *         The window containing the menu entry
   1.883 +   */
   1.884 +  forgetBrowserWindow: function DT_forgetBrowserWindow(win) {
   1.885 +    gDevToolsBrowser._trackedBrowserWindows.delete(win);
   1.886 +
   1.887 +    // Destroy toolboxes for closed window
   1.888 +    for (let [target, toolbox] of gDevTools._toolboxes) {
   1.889 +      if (toolbox.frame && toolbox.frame.ownerDocument.defaultView == win) {
   1.890 +        toolbox.destroy();
   1.891 +      }
   1.892 +    }
   1.893 +
   1.894 +    let tabContainer = win.document.getElementById("tabbrowser-tabs")
   1.895 +    tabContainer.removeEventListener("TabSelect",
   1.896 +                                     gDevToolsBrowser._updateMenuCheckbox, false);
   1.897 +  },
   1.898 +
   1.899 +  /**
   1.900 +   * All browser windows have been closed, tidy up remaining objects.
   1.901 +   */
   1.902 +  destroy: function() {
   1.903 +    gDevTools.off("toolbox-ready", gDevToolsBrowser._connectToProfiler);
   1.904 +    Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
   1.905 +    Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
   1.906 +  },
   1.907 +}
   1.908 +
   1.909 +this.gDevToolsBrowser = gDevToolsBrowser;
   1.910 +
   1.911 +gDevTools.on("tool-registered", function(ev, toolId) {
   1.912 +  let toolDefinition = gDevTools._tools.get(toolId);
   1.913 +  gDevToolsBrowser._addToolToWindows(toolDefinition);
   1.914 +});
   1.915 +
   1.916 +gDevTools.on("tool-unregistered", function(ev, toolId) {
   1.917 +  if (typeof toolId != "string") {
   1.918 +    toolId = toolId.id;
   1.919 +  }
   1.920 +  gDevToolsBrowser._removeToolFromWindows(toolId);
   1.921 +});
   1.922 +
   1.923 +gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
   1.924 +gDevTools.on("toolbox-ready", gDevToolsBrowser._connectToProfiler);
   1.925 +gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
   1.926 +
   1.927 +Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
   1.928 +
   1.929 +// Load the browser devtools main module as the loader's main module.
   1.930 +devtools.main("main");

mercurial