browser/devtools/shared/DeveloperToolbar.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/shared/DeveloperToolbar.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1245 @@
     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 = [ "DeveloperToolbar", "CommandUtils" ];
    1.11 +
    1.12 +const NS_XHTML = "http://www.w3.org/1999/xhtml";
    1.13 +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    1.14 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
    1.15 +
    1.16 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.17 +Cu.import("resource://gre/modules/Services.jsm");
    1.18 +
    1.19 +const { require, TargetFactory } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
    1.20 +
    1.21 +const Node = Ci.nsIDOMNode;
    1.22 +
    1.23 +XPCOMUtils.defineLazyModuleGetter(this, "console",
    1.24 +                                  "resource://gre/modules/devtools/Console.jsm");
    1.25 +
    1.26 +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    1.27 +                                  "resource://gre/modules/PluralForm.jsm");
    1.28 +
    1.29 +XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
    1.30 +                                  "resource://gre/modules/devtools/event-emitter.js");
    1.31 +
    1.32 +XPCOMUtils.defineLazyGetter(this, "prefBranch", function() {
    1.33 +  let prefService = Cc["@mozilla.org/preferences-service;1"]
    1.34 +                    .getService(Ci.nsIPrefService);
    1.35 +  return prefService.getBranch(null)
    1.36 +                    .QueryInterface(Ci.nsIPrefBranch2);
    1.37 +});
    1.38 +
    1.39 +XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function () {
    1.40 +  return Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
    1.41 +});
    1.42 +
    1.43 +const Telemetry = require("devtools/shared/telemetry");
    1.44 +
    1.45 +// This lazy getter is needed to prevent a require loop
    1.46 +XPCOMUtils.defineLazyGetter(this, "gcli", () => {
    1.47 +  let gcli = require("gcli/index");
    1.48 +  require("devtools/commandline/commands-index");
    1.49 +  gcli.load();
    1.50 +  return gcli;
    1.51 +});
    1.52 +
    1.53 +Object.defineProperty(this, "ConsoleServiceListener", {
    1.54 +  get: function() {
    1.55 +    return require("devtools/toolkit/webconsole/utils").ConsoleServiceListener;
    1.56 +  },
    1.57 +  configurable: true,
    1.58 +  enumerable: true
    1.59 +});
    1.60 +
    1.61 +const promise = Cu.import('resource://gre/modules/Promise.jsm', {}).Promise;
    1.62 +
    1.63 +/**
    1.64 + * A collection of utilities to help working with commands
    1.65 + */
    1.66 +let CommandUtils = {
    1.67 +  /**
    1.68 +   * Utility to ensure that things are loaded in the correct order
    1.69 +   */
    1.70 +  createRequisition: function(environment) {
    1.71 +    let temp = gcli.createDisplay; // Ensure GCLI is loaded
    1.72 +    let Requisition = require("gcli/cli").Requisition
    1.73 +    return new Requisition({ environment: environment });
    1.74 +  },
    1.75 +
    1.76 +  /**
    1.77 +   * Read a toolbarSpec from preferences
    1.78 +   * @param pref The name of the preference to read
    1.79 +   */
    1.80 +  getCommandbarSpec: function(pref) {
    1.81 +    let value = prefBranch.getComplexValue(pref, Ci.nsISupportsString).data;
    1.82 +    return JSON.parse(value);
    1.83 +  },
    1.84 +
    1.85 +  /**
    1.86 +   * A toolbarSpec is an array of buttonSpecs. A buttonSpec is an array of
    1.87 +   * strings each of which is a GCLI command (including args if needed).
    1.88 +   *
    1.89 +   * Warning: this method uses the unload event of the window that owns the
    1.90 +   * buttons that are of type checkbox. this means that we don't properly
    1.91 +   * unregister event handlers until the window is destroyed.
    1.92 +   */
    1.93 +  createButtons: function(toolbarSpec, target, document, requisition) {
    1.94 +    let reply = [];
    1.95 +
    1.96 +    toolbarSpec.forEach(function(buttonSpec) {
    1.97 +      let button = document.createElement("toolbarbutton");
    1.98 +      reply.push(button);
    1.99 +
   1.100 +      if (typeof buttonSpec == "string") {
   1.101 +        buttonSpec = { typed: buttonSpec };
   1.102 +      }
   1.103 +      // Ask GCLI to parse the typed string (doesn't execute it)
   1.104 +      requisition.update(buttonSpec.typed);
   1.105 +
   1.106 +      // Ignore invalid commands
   1.107 +      let command = requisition.commandAssignment.value;
   1.108 +      if (command == null) {
   1.109 +        // TODO: Have a broken icon
   1.110 +        // button.icon = 'Broken';
   1.111 +        button.setAttribute("label", "X");
   1.112 +        button.setAttribute("tooltip", "Unknown command: " + buttonSpec.typed);
   1.113 +        button.setAttribute("disabled", "true");
   1.114 +      }
   1.115 +      else {
   1.116 +        if (command.buttonId != null) {
   1.117 +          button.id = command.buttonId;
   1.118 +        }
   1.119 +        if (command.buttonClass != null) {
   1.120 +          button.className = command.buttonClass;
   1.121 +        }
   1.122 +        if (command.tooltipText != null) {
   1.123 +          button.setAttribute("tooltiptext", command.tooltipText);
   1.124 +        }
   1.125 +        else if (command.description != null) {
   1.126 +          button.setAttribute("tooltiptext", command.description);
   1.127 +        }
   1.128 +
   1.129 +        button.addEventListener("click", function() {
   1.130 +          requisition.update(buttonSpec.typed);
   1.131 +          //if (requisition.getStatus() == Status.VALID) {
   1.132 +            requisition.exec();
   1.133 +          /*
   1.134 +          }
   1.135 +          else {
   1.136 +            console.error('incomplete commands not yet supported');
   1.137 +          }
   1.138 +          */
   1.139 +        }, false);
   1.140 +
   1.141 +        // Allow the command button to be toggleable
   1.142 +        if (command.state) {
   1.143 +          button.setAttribute("autocheck", false);
   1.144 +          let onChange = function(event, eventTab) {
   1.145 +            if (eventTab == target.tab) {
   1.146 +              if (command.state.isChecked(target)) {
   1.147 +                button.setAttribute("checked", true);
   1.148 +              }
   1.149 +              else if (button.hasAttribute("checked")) {
   1.150 +                button.removeAttribute("checked");
   1.151 +              }
   1.152 +            }
   1.153 +          };
   1.154 +          command.state.onChange(target, onChange);
   1.155 +          onChange(null, target.tab);
   1.156 +          document.defaultView.addEventListener("unload", function() {
   1.157 +            command.state.offChange(target, onChange);
   1.158 +          }, false);
   1.159 +        }
   1.160 +      }
   1.161 +    });
   1.162 +
   1.163 +    requisition.update('');
   1.164 +
   1.165 +    return reply;
   1.166 +  },
   1.167 +
   1.168 +  /**
   1.169 +   * A helper function to create the environment object that is passed to
   1.170 +   * GCLI commands.
   1.171 +   * @param targetContainer An object containing a 'target' property which
   1.172 +   * reflects the current debug target
   1.173 +   */
   1.174 +  createEnvironment: function(container, targetProperty='target') {
   1.175 +    if (container[targetProperty].supports == null) {
   1.176 +      throw new Error('Missing target');
   1.177 +    }
   1.178 +
   1.179 +    return {
   1.180 +      get target() {
   1.181 +        if (container[targetProperty].supports == null) {
   1.182 +          throw new Error('Removed target');
   1.183 +        }
   1.184 +
   1.185 +        return container[targetProperty];
   1.186 +      },
   1.187 +
   1.188 +      get chromeWindow() {
   1.189 +        return this.target.tab.ownerDocument.defaultView;
   1.190 +      },
   1.191 +
   1.192 +      get chromeDocument() {
   1.193 +        return this.chromeWindow.document;
   1.194 +      },
   1.195 +
   1.196 +      get window() {
   1.197 +        return this.chromeWindow.getBrowser().selectedTab.linkedBrowser.contentWindow;
   1.198 +      },
   1.199 +
   1.200 +      get document() {
   1.201 +        return this.window.document;
   1.202 +      }
   1.203 +    };
   1.204 +  },
   1.205 +};
   1.206 +
   1.207 +this.CommandUtils = CommandUtils;
   1.208 +
   1.209 +/**
   1.210 + * Due to a number of panel bugs we need a way to check if we are running on
   1.211 + * Linux. See the comments for TooltipPanel and OutputPanel for further details.
   1.212 + *
   1.213 + * When bug 780102 is fixed all isLinux checks can be removed and we can revert
   1.214 + * to using panels.
   1.215 + */
   1.216 +XPCOMUtils.defineLazyGetter(this, "isLinux", function() {
   1.217 +  return OS == "Linux";
   1.218 +});
   1.219 +
   1.220 +XPCOMUtils.defineLazyGetter(this, "OS", function() {
   1.221 +  let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
   1.222 +  return os;
   1.223 +});
   1.224 +
   1.225 +/**
   1.226 + * A component to manage the global developer toolbar, which contains a GCLI
   1.227 + * and buttons for various developer tools.
   1.228 + * @param aChromeWindow The browser window to which this toolbar is attached
   1.229 + * @param aToolbarElement See browser.xul:<toolbar id="developer-toolbar">
   1.230 + */
   1.231 +this.DeveloperToolbar = function DeveloperToolbar(aChromeWindow, aToolbarElement)
   1.232 +{
   1.233 +  this._chromeWindow = aChromeWindow;
   1.234 +
   1.235 +  this._element = aToolbarElement;
   1.236 +  this._element.hidden = true;
   1.237 +  this._doc = this._element.ownerDocument;
   1.238 +
   1.239 +  this._telemetry = new Telemetry();
   1.240 +  this._errorsCount = {};
   1.241 +  this._warningsCount = {};
   1.242 +  this._errorListeners = {};
   1.243 +  this._errorCounterButton = this._doc
   1.244 +                             .getElementById("developer-toolbar-toolbox-button");
   1.245 +  this._errorCounterButton._defaultTooltipText =
   1.246 +      this._errorCounterButton.getAttribute("tooltiptext");
   1.247 +
   1.248 +  EventEmitter.decorate(this);
   1.249 +}
   1.250 +
   1.251 +/**
   1.252 + * Inspector notifications dispatched through the nsIObserverService
   1.253 + */
   1.254 +const NOTIFICATIONS = {
   1.255 +  /** DeveloperToolbar.show() has been called, and we're working on it */
   1.256 +  LOAD: "developer-toolbar-load",
   1.257 +
   1.258 +  /** DeveloperToolbar.show() has completed */
   1.259 +  SHOW: "developer-toolbar-show",
   1.260 +
   1.261 +  /** DeveloperToolbar.hide() has been called */
   1.262 +  HIDE: "developer-toolbar-hide"
   1.263 +};
   1.264 +
   1.265 +/**
   1.266 + * Attach notification constants to the object prototype so tests etc can
   1.267 + * use them without needing to import anything
   1.268 + */
   1.269 +DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS;
   1.270 +
   1.271 +Object.defineProperty(DeveloperToolbar.prototype, "target", {
   1.272 +  get: function() {
   1.273 +    return TargetFactory.forTab(this._chromeWindow.getBrowser().selectedTab);
   1.274 +  },
   1.275 +  enumerable: true
   1.276 +});
   1.277 +
   1.278 +/**
   1.279 + * Is the toolbar open?
   1.280 + */
   1.281 +Object.defineProperty(DeveloperToolbar.prototype, 'visible', {
   1.282 +  get: function DT_visible() {
   1.283 +    return !this._element.hidden;
   1.284 +  },
   1.285 +  enumerable: true
   1.286 +});
   1.287 +
   1.288 +let _gSequenceId = 0;
   1.289 +
   1.290 +/**
   1.291 + * Getter for a unique ID.
   1.292 + */
   1.293 +Object.defineProperty(DeveloperToolbar.prototype, 'sequenceId', {
   1.294 +  get: function DT_visible() {
   1.295 +    return _gSequenceId++;
   1.296 +  },
   1.297 +  enumerable: true
   1.298 +});
   1.299 +
   1.300 +/**
   1.301 + * Called from browser.xul in response to menu-click or keyboard shortcut to
   1.302 + * toggle the toolbar
   1.303 + */
   1.304 +DeveloperToolbar.prototype.toggle = function() {
   1.305 +  if (this.visible) {
   1.306 +    return this.hide();
   1.307 +  } else {
   1.308 +    return this.show(true);
   1.309 +  }
   1.310 +};
   1.311 +
   1.312 +/**
   1.313 + * Called from browser.xul in response to menu-click or keyboard shortcut to
   1.314 + * toggle the toolbar
   1.315 + */
   1.316 +DeveloperToolbar.prototype.focus = function() {
   1.317 +  if (this.visible) {
   1.318 +    this._input.focus();
   1.319 +    return promise.resolve();
   1.320 +  } else {
   1.321 +    return this.show(true);
   1.322 +  }
   1.323 +};
   1.324 +
   1.325 +/**
   1.326 + * Called from browser.xul in response to menu-click or keyboard shortcut to
   1.327 + * toggle the toolbar
   1.328 + */
   1.329 +DeveloperToolbar.prototype.focusToggle = function() {
   1.330 +  if (this.visible) {
   1.331 +    // If we have focus then the active element is the HTML input contained
   1.332 +    // inside the xul input element
   1.333 +    let active = this._chromeWindow.document.activeElement;
   1.334 +    let position = this._input.compareDocumentPosition(active);
   1.335 +    if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
   1.336 +      this.hide();
   1.337 +    }
   1.338 +    else {
   1.339 +      this._input.focus();
   1.340 +    }
   1.341 +  } else {
   1.342 +    this.show(true);
   1.343 +  }
   1.344 +};
   1.345 +
   1.346 +/**
   1.347 + * Even if the user has not clicked on 'Got it' in the intro, we only show it
   1.348 + * once per session.
   1.349 + * Warning this is slightly messed up because this.DeveloperToolbar is not the
   1.350 + * same as this.DeveloperToolbar when in browser.js context.
   1.351 + */
   1.352 +DeveloperToolbar.introShownThisSession = false;
   1.353 +
   1.354 +/**
   1.355 + * Show the developer toolbar
   1.356 + */
   1.357 +DeveloperToolbar.prototype.show = function(focus) {
   1.358 +  if (this._showPromise != null) {
   1.359 +    return this._showPromise;
   1.360 +  }
   1.361 +
   1.362 +  // hide() is async, so ensure we don't need to wait for hide() to finish
   1.363 +  var waitPromise = this._hidePromise || promise.resolve();
   1.364 +
   1.365 +  this._showPromise = waitPromise.then(() => {
   1.366 +    Services.prefs.setBoolPref("devtools.toolbar.visible", true);
   1.367 +
   1.368 +    this._telemetry.toolOpened("developertoolbar");
   1.369 +
   1.370 +    this._notify(NOTIFICATIONS.LOAD);
   1.371 +
   1.372 +    this._input = this._doc.querySelector(".gclitoolbar-input-node");
   1.373 +
   1.374 +    // Initializing GCLI can only be done when we've got content windows to
   1.375 +    // write to, so this needs to be done asynchronously.
   1.376 +    let panelPromises = [
   1.377 +      TooltipPanel.create(this),
   1.378 +      OutputPanel.create(this)
   1.379 +    ];
   1.380 +    return promise.all(panelPromises).then(panels => {
   1.381 +      [ this.tooltipPanel, this.outputPanel ] = panels;
   1.382 +
   1.383 +      this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "true");
   1.384 +
   1.385 +      this.display = gcli.createDisplay({
   1.386 +        contentDocument: this._chromeWindow.getBrowser().contentDocument,
   1.387 +        chromeDocument: this._doc,
   1.388 +        chromeWindow: this._chromeWindow,
   1.389 +        hintElement: this.tooltipPanel.hintElement,
   1.390 +        inputElement: this._input,
   1.391 +        completeElement: this._doc.querySelector(".gclitoolbar-complete-node"),
   1.392 +        backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"),
   1.393 +        outputDocument: this.outputPanel.document,
   1.394 +        environment: CommandUtils.createEnvironment(this, "target"),
   1.395 +        tooltipClass: "gcliterm-tooltip",
   1.396 +        eval: null,
   1.397 +        scratchpad: null
   1.398 +      });
   1.399 +
   1.400 +      this.display.focusManager.addMonitoredElement(this.outputPanel._frame);
   1.401 +      this.display.focusManager.addMonitoredElement(this._element);
   1.402 +
   1.403 +      this.display.onVisibilityChange.add(this.outputPanel._visibilityChanged,
   1.404 +                                          this.outputPanel);
   1.405 +      this.display.onVisibilityChange.add(this.tooltipPanel._visibilityChanged,
   1.406 +                                          this.tooltipPanel);
   1.407 +      this.display.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
   1.408 +
   1.409 +      let tabbrowser = this._chromeWindow.getBrowser();
   1.410 +      tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
   1.411 +      tabbrowser.tabContainer.addEventListener("TabClose", this, false);
   1.412 +      tabbrowser.addEventListener("load", this, true);
   1.413 +      tabbrowser.addEventListener("beforeunload", this, true);
   1.414 +
   1.415 +      this._initErrorsCount(tabbrowser.selectedTab);
   1.416 +      this._devtoolsUnloaded = this._devtoolsUnloaded.bind(this);
   1.417 +      this._devtoolsLoaded = this._devtoolsLoaded.bind(this);
   1.418 +      Services.obs.addObserver(this._devtoolsUnloaded, "devtools-unloaded", false);
   1.419 +      Services.obs.addObserver(this._devtoolsLoaded, "devtools-loaded", false);
   1.420 +
   1.421 +      this._element.hidden = false;
   1.422 +
   1.423 +      if (focus) {
   1.424 +        this._input.focus();
   1.425 +      }
   1.426 +
   1.427 +      this._notify(NOTIFICATIONS.SHOW);
   1.428 +
   1.429 +      if (!DeveloperToolbar.introShownThisSession) {
   1.430 +        this.display.maybeShowIntro();
   1.431 +        DeveloperToolbar.introShownThisSession = true;
   1.432 +      }
   1.433 +
   1.434 +      this._showPromise = null;
   1.435 +    });
   1.436 +  });
   1.437 +
   1.438 +  return this._showPromise;
   1.439 +};
   1.440 +
   1.441 +/**
   1.442 + * Hide the developer toolbar.
   1.443 + */
   1.444 +DeveloperToolbar.prototype.hide = function() {
   1.445 +  // If we're already in the process of hiding, just use the other promise
   1.446 +  if (this._hidePromise != null) {
   1.447 +    return this._hidePromise;
   1.448 +  }
   1.449 +
   1.450 +  // show() is async, so ensure we don't need to wait for show() to finish
   1.451 +  var waitPromise = this._showPromise || promise.resolve();
   1.452 +
   1.453 +  this._hidePromise = waitPromise.then(() => {
   1.454 +    this._element.hidden = true;
   1.455 +
   1.456 +    Services.prefs.setBoolPref("devtools.toolbar.visible", false);
   1.457 +
   1.458 +    this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "false");
   1.459 +    this.destroy();
   1.460 +
   1.461 +    this._telemetry.toolClosed("developertoolbar");
   1.462 +    this._notify(NOTIFICATIONS.HIDE);
   1.463 +
   1.464 +    this._hidePromise = null;
   1.465 +  });
   1.466 +
   1.467 +  return this._hidePromise;
   1.468 +};
   1.469 +
   1.470 +/**
   1.471 + * The devtools-unloaded event handler.
   1.472 + * @private
   1.473 + */
   1.474 +DeveloperToolbar.prototype._devtoolsUnloaded = function() {
   1.475 +  let tabbrowser = this._chromeWindow.getBrowser();
   1.476 +  Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
   1.477 +};
   1.478 +
   1.479 +/**
   1.480 + * The devtools-loaded event handler.
   1.481 + * @private
   1.482 + */
   1.483 +DeveloperToolbar.prototype._devtoolsLoaded = function() {
   1.484 +  let tabbrowser = this._chromeWindow.getBrowser();
   1.485 +  this._initErrorsCount(tabbrowser.selectedTab);
   1.486 +};
   1.487 +
   1.488 +/**
   1.489 + * Initialize the listeners needed for tracking the number of errors for a given
   1.490 + * tab.
   1.491 + *
   1.492 + * @private
   1.493 + * @param nsIDOMNode tab the xul:tab for which you want to track the number of
   1.494 + * errors.
   1.495 + */
   1.496 +DeveloperToolbar.prototype._initErrorsCount = function(tab) {
   1.497 +  let tabId = tab.linkedPanel;
   1.498 +  if (tabId in this._errorsCount) {
   1.499 +    this._updateErrorsCount();
   1.500 +    return;
   1.501 +  }
   1.502 +
   1.503 +  let window = tab.linkedBrowser.contentWindow;
   1.504 +  let listener = new ConsoleServiceListener(window, {
   1.505 +    onConsoleServiceMessage: this._onPageError.bind(this, tabId),
   1.506 +  });
   1.507 +  listener.init();
   1.508 +
   1.509 +  this._errorListeners[tabId] = listener;
   1.510 +  this._errorsCount[tabId] = 0;
   1.511 +  this._warningsCount[tabId] = 0;
   1.512 +
   1.513 +  let messages = listener.getCachedMessages();
   1.514 +  messages.forEach(this._onPageError.bind(this, tabId));
   1.515 +
   1.516 +  this._updateErrorsCount();
   1.517 +};
   1.518 +
   1.519 +/**
   1.520 + * Stop the listeners needed for tracking the number of errors for a given
   1.521 + * tab.
   1.522 + *
   1.523 + * @private
   1.524 + * @param nsIDOMNode tab the xul:tab for which you want to stop tracking the
   1.525 + * number of errors.
   1.526 + */
   1.527 +DeveloperToolbar.prototype._stopErrorsCount = function(tab) {
   1.528 +  let tabId = tab.linkedPanel;
   1.529 +  if (!(tabId in this._errorsCount) || !(tabId in this._warningsCount)) {
   1.530 +    this._updateErrorsCount();
   1.531 +    return;
   1.532 +  }
   1.533 +
   1.534 +  this._errorListeners[tabId].destroy();
   1.535 +  delete this._errorListeners[tabId];
   1.536 +  delete this._errorsCount[tabId];
   1.537 +  delete this._warningsCount[tabId];
   1.538 +
   1.539 +  this._updateErrorsCount();
   1.540 +};
   1.541 +
   1.542 +/**
   1.543 + * Hide the developer toolbar
   1.544 + */
   1.545 +DeveloperToolbar.prototype.destroy = function() {
   1.546 +  if (this._input == null) {
   1.547 +    return; // Already destroyed
   1.548 +  }
   1.549 +
   1.550 +  let tabbrowser = this._chromeWindow.getBrowser();
   1.551 +  tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
   1.552 +  tabbrowser.tabContainer.removeEventListener("TabClose", this, false);
   1.553 +  tabbrowser.removeEventListener("load", this, true);
   1.554 +  tabbrowser.removeEventListener("beforeunload", this, true);
   1.555 +
   1.556 +  Services.obs.removeObserver(this._devtoolsUnloaded, "devtools-unloaded");
   1.557 +  Services.obs.removeObserver(this._devtoolsLoaded, "devtools-loaded");
   1.558 +  Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
   1.559 +
   1.560 +  this.display.focusManager.removeMonitoredElement(this.outputPanel._frame);
   1.561 +  this.display.focusManager.removeMonitoredElement(this._element);
   1.562 +
   1.563 +  this.display.onVisibilityChange.remove(this.outputPanel._visibilityChanged, this.outputPanel);
   1.564 +  this.display.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged, this.tooltipPanel);
   1.565 +  this.display.onOutput.remove(this.outputPanel._outputChanged, this.outputPanel);
   1.566 +  this.display.destroy();
   1.567 +  this.outputPanel.destroy();
   1.568 +  this.tooltipPanel.destroy();
   1.569 +  delete this._input;
   1.570 +
   1.571 +  // We could "delete this.display" etc if we have hard-to-track-down memory
   1.572 +  // leaks as a belt-and-braces approach, however this prevents our DOM node
   1.573 +  // hunter from looking in all the nooks and crannies, so it's better if we
   1.574 +  // can be leak-free without
   1.575 +  /*
   1.576 +  delete this.display;
   1.577 +  delete this.outputPanel;
   1.578 +  delete this.tooltipPanel;
   1.579 +  */
   1.580 +};
   1.581 +
   1.582 +/**
   1.583 + * Utility for sending notifications
   1.584 + * @param topic a NOTIFICATION constant
   1.585 + */
   1.586 +DeveloperToolbar.prototype._notify = function(topic) {
   1.587 +  let data = { toolbar: this };
   1.588 +  data.wrappedJSObject = data;
   1.589 +  Services.obs.notifyObservers(data, topic, null);
   1.590 +};
   1.591 +
   1.592 +/**
   1.593 + * Update various parts of the UI when the current tab changes
   1.594 + */
   1.595 +DeveloperToolbar.prototype.handleEvent = function(ev) {
   1.596 +  if (ev.type == "TabSelect" || ev.type == "load") {
   1.597 +    if (this.visible) {
   1.598 +      this.display.reattach({
   1.599 +        contentDocument: this._chromeWindow.getBrowser().contentDocument
   1.600 +      });
   1.601 +
   1.602 +      if (ev.type == "TabSelect") {
   1.603 +        this._initErrorsCount(ev.target);
   1.604 +      }
   1.605 +    }
   1.606 +  }
   1.607 +  else if (ev.type == "TabClose") {
   1.608 +    this._stopErrorsCount(ev.target);
   1.609 +  }
   1.610 +  else if (ev.type == "beforeunload") {
   1.611 +    this._onPageBeforeUnload(ev);
   1.612 +  }
   1.613 +};
   1.614 +
   1.615 +/**
   1.616 + * Count a page error received for the currently selected tab. This
   1.617 + * method counts the JavaScript exceptions received and CSS errors/warnings.
   1.618 + *
   1.619 + * @private
   1.620 + * @param string tabId the ID of the tab from where the page error comes.
   1.621 + * @param object pageError the page error object received from the
   1.622 + * PageErrorListener.
   1.623 + */
   1.624 +DeveloperToolbar.prototype._onPageError = function(tabId, pageError) {
   1.625 +  if (pageError.category == "CSS Parser" ||
   1.626 +      pageError.category == "CSS Loader") {
   1.627 +    return;
   1.628 +  }
   1.629 +  if ((pageError.flags & pageError.warningFlag) ||
   1.630 +      (pageError.flags & pageError.strictFlag)) {
   1.631 +    this._warningsCount[tabId]++;
   1.632 +  } else {
   1.633 +    this._errorsCount[tabId]++;
   1.634 +  }
   1.635 +  this._updateErrorsCount(tabId);
   1.636 +};
   1.637 +
   1.638 +/**
   1.639 + * The |beforeunload| event handler. This function resets the errors count when
   1.640 + * a different page starts loading.
   1.641 + *
   1.642 + * @private
   1.643 + * @param nsIDOMEvent ev the beforeunload DOM event.
   1.644 + */
   1.645 +DeveloperToolbar.prototype._onPageBeforeUnload = function(ev) {
   1.646 +  let window = ev.target.defaultView;
   1.647 +  if (window.top !== window) {
   1.648 +    return;
   1.649 +  }
   1.650 +
   1.651 +  let tabs = this._chromeWindow.getBrowser().tabs;
   1.652 +  Array.prototype.some.call(tabs, function(tab) {
   1.653 +    if (tab.linkedBrowser.contentWindow === window) {
   1.654 +      let tabId = tab.linkedPanel;
   1.655 +      if (tabId in this._errorsCount || tabId in this._warningsCount) {
   1.656 +        this._errorsCount[tabId] = 0;
   1.657 +        this._warningsCount[tabId] = 0;
   1.658 +        this._updateErrorsCount(tabId);
   1.659 +      }
   1.660 +      return true;
   1.661 +    }
   1.662 +    return false;
   1.663 +  }, this);
   1.664 +};
   1.665 +
   1.666 +/**
   1.667 + * Update the page errors count displayed in the Web Console button for the
   1.668 + * currently selected tab.
   1.669 + *
   1.670 + * @private
   1.671 + * @param string [changedTabId] Optional. The tab ID that had its page errors
   1.672 + * count changed. If this is provided and it doesn't match the currently
   1.673 + * selected tab, then the button is not updated.
   1.674 + */
   1.675 +DeveloperToolbar.prototype._updateErrorsCount = function(changedTabId) {
   1.676 +  let tabId = this._chromeWindow.getBrowser().selectedTab.linkedPanel;
   1.677 +  if (changedTabId && tabId != changedTabId) {
   1.678 +    return;
   1.679 +  }
   1.680 +
   1.681 +  let errors = this._errorsCount[tabId];
   1.682 +  let warnings = this._warningsCount[tabId];
   1.683 +  let btn = this._errorCounterButton;
   1.684 +  if (errors) {
   1.685 +    let errorsText = toolboxStrings
   1.686 +                     .GetStringFromName("toolboxToggleButton.errors");
   1.687 +    errorsText = PluralForm.get(errors, errorsText).replace("#1", errors);
   1.688 +
   1.689 +    let warningsText = toolboxStrings
   1.690 +                       .GetStringFromName("toolboxToggleButton.warnings");
   1.691 +    warningsText = PluralForm.get(warnings, warningsText).replace("#1", warnings);
   1.692 +
   1.693 +    let tooltiptext = toolboxStrings
   1.694 +                      .formatStringFromName("toolboxToggleButton.tooltip",
   1.695 +                                            [errorsText, warningsText], 2);
   1.696 +
   1.697 +    btn.setAttribute("error-count", errors);
   1.698 +    btn.setAttribute("tooltiptext", tooltiptext);
   1.699 +  } else {
   1.700 +    btn.removeAttribute("error-count");
   1.701 +    btn.setAttribute("tooltiptext", btn._defaultTooltipText);
   1.702 +  }
   1.703 +
   1.704 +  this.emit("errors-counter-updated");
   1.705 +};
   1.706 +
   1.707 +/**
   1.708 + * Reset the errors counter for the given tab.
   1.709 + *
   1.710 + * @param nsIDOMElement tab The xul:tab for which you want to reset the page
   1.711 + * errors counters.
   1.712 + */
   1.713 +DeveloperToolbar.prototype.resetErrorsCount = function(tab) {
   1.714 +  let tabId = tab.linkedPanel;
   1.715 +  if (tabId in this._errorsCount || tabId in this._warningsCount) {
   1.716 +    this._errorsCount[tabId] = 0;
   1.717 +    this._warningsCount[tabId] = 0;
   1.718 +    this._updateErrorsCount(tabId);
   1.719 +  }
   1.720 +};
   1.721 +
   1.722 +/**
   1.723 + * Creating a OutputPanel is asynchronous
   1.724 + */
   1.725 +function OutputPanel() {
   1.726 +  throw new Error('Use OutputPanel.create()');
   1.727 +}
   1.728 +
   1.729 +/**
   1.730 + * Panel to handle command line output.
   1.731 + *
   1.732 + * There is a tooltip bug on Windows and OSX that prevents tooltips from being
   1.733 + * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
   1.734 + * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
   1.735 + * We now use a tooltip on Linux and a panel on OSX & Windows.
   1.736 + *
   1.737 + * If a panel has no content and no height it is not shown when openPopup is
   1.738 + * called on Windows and OSX (bug 692348) ... this prevents the panel from
   1.739 + * appearing the first time it is shown. Setting the panel's height to 1px
   1.740 + * before calling openPopup works around this issue as we resize it ourselves
   1.741 + * anyway.
   1.742 + *
   1.743 + * @param devtoolbar The parent DeveloperToolbar object
   1.744 + */
   1.745 +OutputPanel.create = function(devtoolbar) {
   1.746 +  var outputPanel = Object.create(OutputPanel.prototype);
   1.747 +  return outputPanel._init(devtoolbar);
   1.748 +};
   1.749 +
   1.750 +/**
   1.751 + * @private See OutputPanel.create
   1.752 + */
   1.753 +OutputPanel.prototype._init = function(devtoolbar) {
   1.754 +  this._devtoolbar = devtoolbar;
   1.755 +  this._input = this._devtoolbar._input;
   1.756 +  this._toolbar = this._devtoolbar._doc.getElementById("developer-toolbar");
   1.757 +
   1.758 +  /*
   1.759 +  <tooltip|panel id="gcli-output"
   1.760 +         noautofocus="true"
   1.761 +         noautohide="true"
   1.762 +         class="gcli-panel">
   1.763 +    <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
   1.764 +                 id="gcli-output-frame"
   1.765 +                 src="chrome://browser/content/devtools/commandlineoutput.xhtml"
   1.766 +                 sandbox="allow-same-origin"/>
   1.767 +  </tooltip|panel>
   1.768 +  */
   1.769 +
   1.770 +  // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
   1.771 +  // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
   1.772 +  this._panel = this._devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
   1.773 +
   1.774 +  this._panel.id = "gcli-output";
   1.775 +  this._panel.classList.add("gcli-panel");
   1.776 +
   1.777 +  if (isLinux) {
   1.778 +    this.canHide = false;
   1.779 +    this._onpopuphiding = this._onpopuphiding.bind(this);
   1.780 +    this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
   1.781 +  } else {
   1.782 +    this._panel.setAttribute("noautofocus", "true");
   1.783 +    this._panel.setAttribute("noautohide", "true");
   1.784 +
   1.785 +    // Bug 692348: On Windows and OSX if a panel has no content and no height
   1.786 +    // openPopup fails to display it. Setting the height to 1px alows the panel
   1.787 +    // to be displayed before has content or a real height i.e. the first time
   1.788 +    // it is displayed.
   1.789 +    this._panel.setAttribute("height", "1px");
   1.790 +  }
   1.791 +
   1.792 +  this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
   1.793 +
   1.794 +  this._frame = this._devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
   1.795 +  this._frame.id = "gcli-output-frame";
   1.796 +  this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlineoutput.xhtml");
   1.797 +  this._frame.setAttribute("sandbox", "allow-same-origin");
   1.798 +  this._panel.appendChild(this._frame);
   1.799 +
   1.800 +  this.displayedOutput = undefined;
   1.801 +
   1.802 +  this._update = this._update.bind(this);
   1.803 +
   1.804 +  // Wire up the element from the iframe, and resolve the promise
   1.805 +  let deferred = promise.defer();
   1.806 +  let onload = () => {
   1.807 +    this._frame.removeEventListener("load", onload, true);
   1.808 +
   1.809 +    this.document = this._frame.contentDocument;
   1.810 +
   1.811 +    this._div = this.document.getElementById("gcli-output-root");
   1.812 +    this._div.classList.add('gcli-row-out');
   1.813 +    this._div.setAttribute('aria-live', 'assertive');
   1.814 +
   1.815 +    let styles = this._toolbar.ownerDocument.defaultView
   1.816 +                    .getComputedStyle(this._toolbar);
   1.817 +    this._div.setAttribute("dir", styles.direction);
   1.818 +
   1.819 +    deferred.resolve(this);
   1.820 +  };
   1.821 +  this._frame.addEventListener("load", onload, true);
   1.822 +
   1.823 +  return deferred.promise;
   1.824 +}
   1.825 +
   1.826 +/**
   1.827 + * Prevent the popup from hiding if it is not permitted via this.canHide.
   1.828 + */
   1.829 +OutputPanel.prototype._onpopuphiding = function(ev) {
   1.830 +  // TODO: When we switch back from tooltip to panel we can remove this hack:
   1.831 +  // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
   1.832 +  if (isLinux && !this.canHide) {
   1.833 +    ev.preventDefault();
   1.834 +  }
   1.835 +};
   1.836 +
   1.837 +/**
   1.838 + * Display the OutputPanel.
   1.839 + */
   1.840 +OutputPanel.prototype.show = function() {
   1.841 +  if (isLinux) {
   1.842 +    this.canHide = false;
   1.843 +  }
   1.844 +
   1.845 +  // We need to reset the iframe size in order for future size calculations to
   1.846 +  // be correct
   1.847 +  this._frame.style.minHeight = this._frame.style.maxHeight = 0;
   1.848 +  this._frame.style.minWidth = 0;
   1.849 +
   1.850 +  this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null);
   1.851 +  this._resize();
   1.852 +
   1.853 +  this._input.focus();
   1.854 +};
   1.855 +
   1.856 +/**
   1.857 + * Internal helper to set the height of the output panel to fit the available
   1.858 + * content;
   1.859 + */
   1.860 +OutputPanel.prototype._resize = function() {
   1.861 +  if (this._panel == null || this.document == null || !this._panel.state == "closed") {
   1.862 +    return
   1.863 +  }
   1.864 +
   1.865 +  // Set max panel width to match any content with a max of the width of the
   1.866 +  // browser window.
   1.867 +  let maxWidth = this._panel.ownerDocument.documentElement.clientWidth;
   1.868 +
   1.869 +  // Adjust max width according to OS.
   1.870 +  // We'd like to put this in CSS but we can't:
   1.871 +  //   body { width: calc(min(-5px, max-content)); }
   1.872 +  //   #_panel { max-width: -5px; }
   1.873 +  switch(OS) {
   1.874 +    case "Linux":
   1.875 +      maxWidth -= 5;
   1.876 +      break;
   1.877 +    case "Darwin":
   1.878 +      maxWidth -= 25;
   1.879 +      break;
   1.880 +    case "WINNT":
   1.881 +      maxWidth -= 5;
   1.882 +      break;
   1.883 +  }
   1.884 +
   1.885 +  this.document.body.style.width = "-moz-max-content";
   1.886 +  let style = this._frame.contentWindow.getComputedStyle(this.document.body);
   1.887 +  let frameWidth = parseInt(style.width, 10);
   1.888 +  let width = Math.min(maxWidth, frameWidth);
   1.889 +  this.document.body.style.width = width + "px";
   1.890 +
   1.891 +  // Set the width of the iframe.
   1.892 +  this._frame.style.minWidth = width + "px";
   1.893 +  this._panel.style.maxWidth = maxWidth + "px";
   1.894 +
   1.895 +  // browserAdjustment is used to correct the panel height according to the
   1.896 +  // browsers borders etc.
   1.897 +  const browserAdjustment = 15;
   1.898 +
   1.899 +  // Set max panel height to match any content with a max of the height of the
   1.900 +  // browser window.
   1.901 +  let maxHeight =
   1.902 +    this._panel.ownerDocument.documentElement.clientHeight - browserAdjustment;
   1.903 +  let height = Math.min(maxHeight, this.document.documentElement.scrollHeight);
   1.904 +
   1.905 +  // Set the height of the iframe. Setting iframe.height does not work.
   1.906 +  this._frame.style.minHeight = this._frame.style.maxHeight = height + "px";
   1.907 +
   1.908 +  // Set the height and width of the panel to match the iframe.
   1.909 +  this._panel.sizeTo(width, height);
   1.910 +
   1.911 +  // Move the panel to the correct position in the case that it has been
   1.912 +  // positioned incorrectly.
   1.913 +  let screenX = this._input.boxObject.screenX;
   1.914 +  let screenY = this._toolbar.boxObject.screenY;
   1.915 +  this._panel.moveTo(screenX, screenY - height);
   1.916 +};
   1.917 +
   1.918 +/**
   1.919 + * Called by GCLI when a command is executed.
   1.920 + */
   1.921 +OutputPanel.prototype._outputChanged = function(ev) {
   1.922 +  if (ev.output.hidden) {
   1.923 +    return;
   1.924 +  }
   1.925 +
   1.926 +  this.remove();
   1.927 +
   1.928 +  this.displayedOutput = ev.output;
   1.929 +
   1.930 +  if (this.displayedOutput.completed) {
   1.931 +    this._update();
   1.932 +  }
   1.933 +  else {
   1.934 +    this.displayedOutput.promise.then(this._update, this._update)
   1.935 +                                .then(null, console.error);
   1.936 +  }
   1.937 +};
   1.938 +
   1.939 +/**
   1.940 + * Called when displayed Output says it's changed or from outputChanged, which
   1.941 + * happens when there is a new displayed Output.
   1.942 + */
   1.943 +OutputPanel.prototype._update = function() {
   1.944 +  // destroy has been called, bail out
   1.945 +  if (this._div == null) {
   1.946 +    return;
   1.947 +  }
   1.948 +
   1.949 +  // Empty this._div
   1.950 +  while (this._div.hasChildNodes()) {
   1.951 +    this._div.removeChild(this._div.firstChild);
   1.952 +  }
   1.953 +
   1.954 +  if (this.displayedOutput.data != null) {
   1.955 +    let context = this._devtoolbar.display.requisition.conversionContext;
   1.956 +    this.displayedOutput.convert('dom', context).then((node) => {
   1.957 +      while (this._div.hasChildNodes()) {
   1.958 +        this._div.removeChild(this._div.firstChild);
   1.959 +      }
   1.960 +
   1.961 +      var links = node.ownerDocument.querySelectorAll('*[href]');
   1.962 +      for (var i = 0; i < links.length; i++) {
   1.963 +        links[i].setAttribute('target', '_blank');
   1.964 +      }
   1.965 +
   1.966 +      this._div.appendChild(node);
   1.967 +      this.show();
   1.968 +    });
   1.969 +  }
   1.970 +};
   1.971 +
   1.972 +/**
   1.973 + * Detach listeners from the currently displayed Output.
   1.974 + */
   1.975 +OutputPanel.prototype.remove = function() {
   1.976 +  if (isLinux) {
   1.977 +    this.canHide = true;
   1.978 +  }
   1.979 +
   1.980 +  if (this._panel && this._panel.hidePopup) {
   1.981 +    this._panel.hidePopup();
   1.982 +  }
   1.983 +
   1.984 +  if (this.displayedOutput) {
   1.985 +    delete this.displayedOutput;
   1.986 +  }
   1.987 +};
   1.988 +
   1.989 +/**
   1.990 + * Detach listeners from the currently displayed Output.
   1.991 + */
   1.992 +OutputPanel.prototype.destroy = function() {
   1.993 +  this.remove();
   1.994 +
   1.995 +  this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
   1.996 +
   1.997 +  this._panel.removeChild(this._frame);
   1.998 +  this._toolbar.parentElement.removeChild(this._panel);
   1.999 +
  1.1000 +  delete this._devtoolbar;
  1.1001 +  delete this._input;
  1.1002 +  delete this._toolbar;
  1.1003 +  delete this._onpopuphiding;
  1.1004 +  delete this._panel;
  1.1005 +  delete this._frame;
  1.1006 +  delete this._content;
  1.1007 +  delete this._div;
  1.1008 +  delete this.document;
  1.1009 +};
  1.1010 +
  1.1011 +/**
  1.1012 + * Called by GCLI to indicate that we should show or hide one either the
  1.1013 + * tooltip panel or the output panel.
  1.1014 + */
  1.1015 +OutputPanel.prototype._visibilityChanged = function(ev) {
  1.1016 +  if (ev.outputVisible === true) {
  1.1017 +    // this.show is called by _outputChanged
  1.1018 +  } else {
  1.1019 +    if (isLinux) {
  1.1020 +      this.canHide = true;
  1.1021 +    }
  1.1022 +    this._panel.hidePopup();
  1.1023 +  }
  1.1024 +};
  1.1025 +
  1.1026 +/**
  1.1027 + * Creating a TooltipPanel is asynchronous
  1.1028 + */
  1.1029 +function TooltipPanel() {
  1.1030 +  throw new Error('Use TooltipPanel.create()');
  1.1031 +}
  1.1032 +
  1.1033 +/**
  1.1034 + * Panel to handle tooltips.
  1.1035 + *
  1.1036 + * There is a tooltip bug on Windows and OSX that prevents tooltips from being
  1.1037 + * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
  1.1038 + * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
  1.1039 + * We now use a tooltip on Linux and a panel on OSX & Windows.
  1.1040 + *
  1.1041 + * If a panel has no content and no height it is not shown when openPopup is
  1.1042 + * called on Windows and OSX (bug 692348) ... this prevents the panel from
  1.1043 + * appearing the first time it is shown. Setting the panel's height to 1px
  1.1044 + * before calling openPopup works around this issue as we resize it ourselves
  1.1045 + * anyway.
  1.1046 + *
  1.1047 + * @param devtoolbar The parent DeveloperToolbar object
  1.1048 + */
  1.1049 +TooltipPanel.create = function(devtoolbar) {
  1.1050 +  var tooltipPanel = Object.create(TooltipPanel.prototype);
  1.1051 +  return tooltipPanel._init(devtoolbar);
  1.1052 +};
  1.1053 +
  1.1054 +/**
  1.1055 + * @private See TooltipPanel.create
  1.1056 + */
  1.1057 +TooltipPanel.prototype._init = function(devtoolbar) {
  1.1058 +  let deferred = promise.defer();
  1.1059 +
  1.1060 +  let chromeDocument = devtoolbar._doc;
  1.1061 +  this._input = devtoolbar._doc.querySelector(".gclitoolbar-input-node");
  1.1062 +  this._toolbar = devtoolbar._doc.querySelector("#developer-toolbar");
  1.1063 +  this._dimensions = { start: 0, end: 0 };
  1.1064 +
  1.1065 +  /*
  1.1066 +  <tooltip|panel id="gcli-tooltip"
  1.1067 +         type="arrow"
  1.1068 +         noautofocus="true"
  1.1069 +         noautohide="true"
  1.1070 +         class="gcli-panel">
  1.1071 +    <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
  1.1072 +                 id="gcli-tooltip-frame"
  1.1073 +                 src="chrome://browser/content/devtools/commandlinetooltip.xhtml"
  1.1074 +                 flex="1"
  1.1075 +                 sandbox="allow-same-origin"/>
  1.1076 +  </tooltip|panel>
  1.1077 +  */
  1.1078 +
  1.1079 +  // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
  1.1080 +  // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
  1.1081 +  this._panel = devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
  1.1082 +
  1.1083 +  this._panel.id = "gcli-tooltip";
  1.1084 +  this._panel.classList.add("gcli-panel");
  1.1085 +
  1.1086 +  if (isLinux) {
  1.1087 +    this.canHide = false;
  1.1088 +    this._onpopuphiding = this._onpopuphiding.bind(this);
  1.1089 +    this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
  1.1090 +  } else {
  1.1091 +    this._panel.setAttribute("noautofocus", "true");
  1.1092 +    this._panel.setAttribute("noautohide", "true");
  1.1093 +
  1.1094 +    // Bug 692348: On Windows and OSX if a panel has no content and no height
  1.1095 +    // openPopup fails to display it. Setting the height to 1px alows the panel
  1.1096 +    // to be displayed before has content or a real height i.e. the first time
  1.1097 +    // it is displayed.
  1.1098 +    this._panel.setAttribute("height", "1px");
  1.1099 +  }
  1.1100 +
  1.1101 +  this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
  1.1102 +
  1.1103 +  this._frame = devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
  1.1104 +  this._frame.id = "gcli-tooltip-frame";
  1.1105 +  this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlinetooltip.xhtml");
  1.1106 +  this._frame.setAttribute("flex", "1");
  1.1107 +  this._frame.setAttribute("sandbox", "allow-same-origin");
  1.1108 +  this._panel.appendChild(this._frame);
  1.1109 +
  1.1110 +  /**
  1.1111 +   * Wire up the element from the iframe, and resolve the promise.
  1.1112 +   */
  1.1113 +  let onload = () => {
  1.1114 +    this._frame.removeEventListener("load", onload, true);
  1.1115 +
  1.1116 +    this.document = this._frame.contentDocument;
  1.1117 +    this.hintElement = this.document.getElementById("gcli-tooltip-root");
  1.1118 +    this._connector = this.document.getElementById("gcli-tooltip-connector");
  1.1119 +
  1.1120 +    let styles = this._toolbar.ownerDocument.defaultView
  1.1121 +                    .getComputedStyle(this._toolbar);
  1.1122 +    this.hintElement.setAttribute("dir", styles.direction);
  1.1123 +
  1.1124 +    deferred.resolve(this);
  1.1125 +  };
  1.1126 +  this._frame.addEventListener("load", onload, true);
  1.1127 +
  1.1128 +  return deferred.promise;
  1.1129 +}
  1.1130 +
  1.1131 +/**
  1.1132 + * Prevent the popup from hiding if it is not permitted via this.canHide.
  1.1133 + */
  1.1134 +TooltipPanel.prototype._onpopuphiding = function(ev) {
  1.1135 +  // TODO: When we switch back from tooltip to panel we can remove this hack:
  1.1136 +  // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
  1.1137 +  if (isLinux && !this.canHide) {
  1.1138 +    ev.preventDefault();
  1.1139 +  }
  1.1140 +};
  1.1141 +
  1.1142 +/**
  1.1143 + * Display the TooltipPanel.
  1.1144 + */
  1.1145 +TooltipPanel.prototype.show = function(dimensions) {
  1.1146 +  if (!dimensions) {
  1.1147 +    dimensions = { start: 0, end: 0 };
  1.1148 +  }
  1.1149 +  this._dimensions = dimensions;
  1.1150 +
  1.1151 +  // This is nasty, but displaying the panel causes it to re-flow, which can
  1.1152 +  // change the size it should be, so we need to resize the iframe after the
  1.1153 +  // panel has displayed
  1.1154 +  this._panel.ownerDocument.defaultView.setTimeout(() => {
  1.1155 +    this._resize();
  1.1156 +  }, 0);
  1.1157 +
  1.1158 +  if (isLinux) {
  1.1159 +    this.canHide = false;
  1.1160 +  }
  1.1161 +
  1.1162 +  this._resize();
  1.1163 +  this._panel.openPopup(this._input, "before_start", dimensions.start * 10, 0,
  1.1164 +                        false, false, null);
  1.1165 +  this._input.focus();
  1.1166 +};
  1.1167 +
  1.1168 +/**
  1.1169 + * One option is to spend lots of time taking an average width of characters
  1.1170 + * in the current font, dynamically, and weighting for the frequency of use of
  1.1171 + * various characters, or even to render the given string off screen, and then
  1.1172 + * measure the width.
  1.1173 + * Or we could do this...
  1.1174 + */
  1.1175 +const AVE_CHAR_WIDTH = 4.5;
  1.1176 +
  1.1177 +/**
  1.1178 + * Display the TooltipPanel.
  1.1179 + */
  1.1180 +TooltipPanel.prototype._resize = function() {
  1.1181 +  if (this._panel == null || this.document == null || !this._panel.state == "closed") {
  1.1182 +    return
  1.1183 +  }
  1.1184 +
  1.1185 +  let offset = 10 + Math.floor(this._dimensions.start * AVE_CHAR_WIDTH);
  1.1186 +  this._panel.style.marginLeft = offset + "px";
  1.1187 +
  1.1188 +  /*
  1.1189 +  // Bug 744906: UX review - Not sure if we want this code to fatten connector
  1.1190 +  // with param width
  1.1191 +  let width = Math.floor(this._dimensions.end * AVE_CHAR_WIDTH);
  1.1192 +  width = Math.min(width, 100);
  1.1193 +  width = Math.max(width, 10);
  1.1194 +  this._connector.style.width = width + "px";
  1.1195 +  */
  1.1196 +
  1.1197 +  this._frame.height = this.document.body.scrollHeight;
  1.1198 +};
  1.1199 +
  1.1200 +/**
  1.1201 + * Hide the TooltipPanel.
  1.1202 + */
  1.1203 +TooltipPanel.prototype.remove = function() {
  1.1204 +  if (isLinux) {
  1.1205 +    this.canHide = true;
  1.1206 +  }
  1.1207 +  if (this._panel && this._panel.hidePopup) {
  1.1208 +    this._panel.hidePopup();
  1.1209 +  }
  1.1210 +};
  1.1211 +
  1.1212 +/**
  1.1213 + * Hide the TooltipPanel.
  1.1214 + */
  1.1215 +TooltipPanel.prototype.destroy = function() {
  1.1216 +  this.remove();
  1.1217 +
  1.1218 +  this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
  1.1219 +
  1.1220 +  this._panel.removeChild(this._frame);
  1.1221 +  this._toolbar.parentElement.removeChild(this._panel);
  1.1222 +
  1.1223 +  delete this._connector;
  1.1224 +  delete this._dimensions;
  1.1225 +  delete this._input;
  1.1226 +  delete this._onpopuphiding;
  1.1227 +  delete this._panel;
  1.1228 +  delete this._frame;
  1.1229 +  delete this._toolbar;
  1.1230 +  delete this._content;
  1.1231 +  delete this.document;
  1.1232 +  delete this.hintElement;
  1.1233 +};
  1.1234 +
  1.1235 +/**
  1.1236 + * Called by GCLI to indicate that we should show or hide one either the
  1.1237 + * tooltip panel or the output panel.
  1.1238 + */
  1.1239 +TooltipPanel.prototype._visibilityChanged = function(ev) {
  1.1240 +  if (ev.tooltipVisible === true) {
  1.1241 +    this.show(ev.dimensions);
  1.1242 +  } else {
  1.1243 +    if (isLinux) {
  1.1244 +      this.canHide = true;
  1.1245 +    }
  1.1246 +    this._panel.hidePopup();
  1.1247 +  }
  1.1248 +};

mercurial