browser/devtools/shared/DeveloperToolbar.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 this.EXPORTED_SYMBOLS = [ "DeveloperToolbar", "CommandUtils" ];
     9 const NS_XHTML = "http://www.w3.org/1999/xhtml";
    10 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    11 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
    13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    14 Cu.import("resource://gre/modules/Services.jsm");
    16 const { require, TargetFactory } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
    18 const Node = Ci.nsIDOMNode;
    20 XPCOMUtils.defineLazyModuleGetter(this, "console",
    21                                   "resource://gre/modules/devtools/Console.jsm");
    23 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    24                                   "resource://gre/modules/PluralForm.jsm");
    26 XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
    27                                   "resource://gre/modules/devtools/event-emitter.js");
    29 XPCOMUtils.defineLazyGetter(this, "prefBranch", function() {
    30   let prefService = Cc["@mozilla.org/preferences-service;1"]
    31                     .getService(Ci.nsIPrefService);
    32   return prefService.getBranch(null)
    33                     .QueryInterface(Ci.nsIPrefBranch2);
    34 });
    36 XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function () {
    37   return Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
    38 });
    40 const Telemetry = require("devtools/shared/telemetry");
    42 // This lazy getter is needed to prevent a require loop
    43 XPCOMUtils.defineLazyGetter(this, "gcli", () => {
    44   let gcli = require("gcli/index");
    45   require("devtools/commandline/commands-index");
    46   gcli.load();
    47   return gcli;
    48 });
    50 Object.defineProperty(this, "ConsoleServiceListener", {
    51   get: function() {
    52     return require("devtools/toolkit/webconsole/utils").ConsoleServiceListener;
    53   },
    54   configurable: true,
    55   enumerable: true
    56 });
    58 const promise = Cu.import('resource://gre/modules/Promise.jsm', {}).Promise;
    60 /**
    61  * A collection of utilities to help working with commands
    62  */
    63 let CommandUtils = {
    64   /**
    65    * Utility to ensure that things are loaded in the correct order
    66    */
    67   createRequisition: function(environment) {
    68     let temp = gcli.createDisplay; // Ensure GCLI is loaded
    69     let Requisition = require("gcli/cli").Requisition
    70     return new Requisition({ environment: environment });
    71   },
    73   /**
    74    * Read a toolbarSpec from preferences
    75    * @param pref The name of the preference to read
    76    */
    77   getCommandbarSpec: function(pref) {
    78     let value = prefBranch.getComplexValue(pref, Ci.nsISupportsString).data;
    79     return JSON.parse(value);
    80   },
    82   /**
    83    * A toolbarSpec is an array of buttonSpecs. A buttonSpec is an array of
    84    * strings each of which is a GCLI command (including args if needed).
    85    *
    86    * Warning: this method uses the unload event of the window that owns the
    87    * buttons that are of type checkbox. this means that we don't properly
    88    * unregister event handlers until the window is destroyed.
    89    */
    90   createButtons: function(toolbarSpec, target, document, requisition) {
    91     let reply = [];
    93     toolbarSpec.forEach(function(buttonSpec) {
    94       let button = document.createElement("toolbarbutton");
    95       reply.push(button);
    97       if (typeof buttonSpec == "string") {
    98         buttonSpec = { typed: buttonSpec };
    99       }
   100       // Ask GCLI to parse the typed string (doesn't execute it)
   101       requisition.update(buttonSpec.typed);
   103       // Ignore invalid commands
   104       let command = requisition.commandAssignment.value;
   105       if (command == null) {
   106         // TODO: Have a broken icon
   107         // button.icon = 'Broken';
   108         button.setAttribute("label", "X");
   109         button.setAttribute("tooltip", "Unknown command: " + buttonSpec.typed);
   110         button.setAttribute("disabled", "true");
   111       }
   112       else {
   113         if (command.buttonId != null) {
   114           button.id = command.buttonId;
   115         }
   116         if (command.buttonClass != null) {
   117           button.className = command.buttonClass;
   118         }
   119         if (command.tooltipText != null) {
   120           button.setAttribute("tooltiptext", command.tooltipText);
   121         }
   122         else if (command.description != null) {
   123           button.setAttribute("tooltiptext", command.description);
   124         }
   126         button.addEventListener("click", function() {
   127           requisition.update(buttonSpec.typed);
   128           //if (requisition.getStatus() == Status.VALID) {
   129             requisition.exec();
   130           /*
   131           }
   132           else {
   133             console.error('incomplete commands not yet supported');
   134           }
   135           */
   136         }, false);
   138         // Allow the command button to be toggleable
   139         if (command.state) {
   140           button.setAttribute("autocheck", false);
   141           let onChange = function(event, eventTab) {
   142             if (eventTab == target.tab) {
   143               if (command.state.isChecked(target)) {
   144                 button.setAttribute("checked", true);
   145               }
   146               else if (button.hasAttribute("checked")) {
   147                 button.removeAttribute("checked");
   148               }
   149             }
   150           };
   151           command.state.onChange(target, onChange);
   152           onChange(null, target.tab);
   153           document.defaultView.addEventListener("unload", function() {
   154             command.state.offChange(target, onChange);
   155           }, false);
   156         }
   157       }
   158     });
   160     requisition.update('');
   162     return reply;
   163   },
   165   /**
   166    * A helper function to create the environment object that is passed to
   167    * GCLI commands.
   168    * @param targetContainer An object containing a 'target' property which
   169    * reflects the current debug target
   170    */
   171   createEnvironment: function(container, targetProperty='target') {
   172     if (container[targetProperty].supports == null) {
   173       throw new Error('Missing target');
   174     }
   176     return {
   177       get target() {
   178         if (container[targetProperty].supports == null) {
   179           throw new Error('Removed target');
   180         }
   182         return container[targetProperty];
   183       },
   185       get chromeWindow() {
   186         return this.target.tab.ownerDocument.defaultView;
   187       },
   189       get chromeDocument() {
   190         return this.chromeWindow.document;
   191       },
   193       get window() {
   194         return this.chromeWindow.getBrowser().selectedTab.linkedBrowser.contentWindow;
   195       },
   197       get document() {
   198         return this.window.document;
   199       }
   200     };
   201   },
   202 };
   204 this.CommandUtils = CommandUtils;
   206 /**
   207  * Due to a number of panel bugs we need a way to check if we are running on
   208  * Linux. See the comments for TooltipPanel and OutputPanel for further details.
   209  *
   210  * When bug 780102 is fixed all isLinux checks can be removed and we can revert
   211  * to using panels.
   212  */
   213 XPCOMUtils.defineLazyGetter(this, "isLinux", function() {
   214   return OS == "Linux";
   215 });
   217 XPCOMUtils.defineLazyGetter(this, "OS", function() {
   218   let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
   219   return os;
   220 });
   222 /**
   223  * A component to manage the global developer toolbar, which contains a GCLI
   224  * and buttons for various developer tools.
   225  * @param aChromeWindow The browser window to which this toolbar is attached
   226  * @param aToolbarElement See browser.xul:<toolbar id="developer-toolbar">
   227  */
   228 this.DeveloperToolbar = function DeveloperToolbar(aChromeWindow, aToolbarElement)
   229 {
   230   this._chromeWindow = aChromeWindow;
   232   this._element = aToolbarElement;
   233   this._element.hidden = true;
   234   this._doc = this._element.ownerDocument;
   236   this._telemetry = new Telemetry();
   237   this._errorsCount = {};
   238   this._warningsCount = {};
   239   this._errorListeners = {};
   240   this._errorCounterButton = this._doc
   241                              .getElementById("developer-toolbar-toolbox-button");
   242   this._errorCounterButton._defaultTooltipText =
   243       this._errorCounterButton.getAttribute("tooltiptext");
   245   EventEmitter.decorate(this);
   246 }
   248 /**
   249  * Inspector notifications dispatched through the nsIObserverService
   250  */
   251 const NOTIFICATIONS = {
   252   /** DeveloperToolbar.show() has been called, and we're working on it */
   253   LOAD: "developer-toolbar-load",
   255   /** DeveloperToolbar.show() has completed */
   256   SHOW: "developer-toolbar-show",
   258   /** DeveloperToolbar.hide() has been called */
   259   HIDE: "developer-toolbar-hide"
   260 };
   262 /**
   263  * Attach notification constants to the object prototype so tests etc can
   264  * use them without needing to import anything
   265  */
   266 DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS;
   268 Object.defineProperty(DeveloperToolbar.prototype, "target", {
   269   get: function() {
   270     return TargetFactory.forTab(this._chromeWindow.getBrowser().selectedTab);
   271   },
   272   enumerable: true
   273 });
   275 /**
   276  * Is the toolbar open?
   277  */
   278 Object.defineProperty(DeveloperToolbar.prototype, 'visible', {
   279   get: function DT_visible() {
   280     return !this._element.hidden;
   281   },
   282   enumerable: true
   283 });
   285 let _gSequenceId = 0;
   287 /**
   288  * Getter for a unique ID.
   289  */
   290 Object.defineProperty(DeveloperToolbar.prototype, 'sequenceId', {
   291   get: function DT_visible() {
   292     return _gSequenceId++;
   293   },
   294   enumerable: true
   295 });
   297 /**
   298  * Called from browser.xul in response to menu-click or keyboard shortcut to
   299  * toggle the toolbar
   300  */
   301 DeveloperToolbar.prototype.toggle = function() {
   302   if (this.visible) {
   303     return this.hide();
   304   } else {
   305     return this.show(true);
   306   }
   307 };
   309 /**
   310  * Called from browser.xul in response to menu-click or keyboard shortcut to
   311  * toggle the toolbar
   312  */
   313 DeveloperToolbar.prototype.focus = function() {
   314   if (this.visible) {
   315     this._input.focus();
   316     return promise.resolve();
   317   } else {
   318     return this.show(true);
   319   }
   320 };
   322 /**
   323  * Called from browser.xul in response to menu-click or keyboard shortcut to
   324  * toggle the toolbar
   325  */
   326 DeveloperToolbar.prototype.focusToggle = function() {
   327   if (this.visible) {
   328     // If we have focus then the active element is the HTML input contained
   329     // inside the xul input element
   330     let active = this._chromeWindow.document.activeElement;
   331     let position = this._input.compareDocumentPosition(active);
   332     if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
   333       this.hide();
   334     }
   335     else {
   336       this._input.focus();
   337     }
   338   } else {
   339     this.show(true);
   340   }
   341 };
   343 /**
   344  * Even if the user has not clicked on 'Got it' in the intro, we only show it
   345  * once per session.
   346  * Warning this is slightly messed up because this.DeveloperToolbar is not the
   347  * same as this.DeveloperToolbar when in browser.js context.
   348  */
   349 DeveloperToolbar.introShownThisSession = false;
   351 /**
   352  * Show the developer toolbar
   353  */
   354 DeveloperToolbar.prototype.show = function(focus) {
   355   if (this._showPromise != null) {
   356     return this._showPromise;
   357   }
   359   // hide() is async, so ensure we don't need to wait for hide() to finish
   360   var waitPromise = this._hidePromise || promise.resolve();
   362   this._showPromise = waitPromise.then(() => {
   363     Services.prefs.setBoolPref("devtools.toolbar.visible", true);
   365     this._telemetry.toolOpened("developertoolbar");
   367     this._notify(NOTIFICATIONS.LOAD);
   369     this._input = this._doc.querySelector(".gclitoolbar-input-node");
   371     // Initializing GCLI can only be done when we've got content windows to
   372     // write to, so this needs to be done asynchronously.
   373     let panelPromises = [
   374       TooltipPanel.create(this),
   375       OutputPanel.create(this)
   376     ];
   377     return promise.all(panelPromises).then(panels => {
   378       [ this.tooltipPanel, this.outputPanel ] = panels;
   380       this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "true");
   382       this.display = gcli.createDisplay({
   383         contentDocument: this._chromeWindow.getBrowser().contentDocument,
   384         chromeDocument: this._doc,
   385         chromeWindow: this._chromeWindow,
   386         hintElement: this.tooltipPanel.hintElement,
   387         inputElement: this._input,
   388         completeElement: this._doc.querySelector(".gclitoolbar-complete-node"),
   389         backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"),
   390         outputDocument: this.outputPanel.document,
   391         environment: CommandUtils.createEnvironment(this, "target"),
   392         tooltipClass: "gcliterm-tooltip",
   393         eval: null,
   394         scratchpad: null
   395       });
   397       this.display.focusManager.addMonitoredElement(this.outputPanel._frame);
   398       this.display.focusManager.addMonitoredElement(this._element);
   400       this.display.onVisibilityChange.add(this.outputPanel._visibilityChanged,
   401                                           this.outputPanel);
   402       this.display.onVisibilityChange.add(this.tooltipPanel._visibilityChanged,
   403                                           this.tooltipPanel);
   404       this.display.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
   406       let tabbrowser = this._chromeWindow.getBrowser();
   407       tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
   408       tabbrowser.tabContainer.addEventListener("TabClose", this, false);
   409       tabbrowser.addEventListener("load", this, true);
   410       tabbrowser.addEventListener("beforeunload", this, true);
   412       this._initErrorsCount(tabbrowser.selectedTab);
   413       this._devtoolsUnloaded = this._devtoolsUnloaded.bind(this);
   414       this._devtoolsLoaded = this._devtoolsLoaded.bind(this);
   415       Services.obs.addObserver(this._devtoolsUnloaded, "devtools-unloaded", false);
   416       Services.obs.addObserver(this._devtoolsLoaded, "devtools-loaded", false);
   418       this._element.hidden = false;
   420       if (focus) {
   421         this._input.focus();
   422       }
   424       this._notify(NOTIFICATIONS.SHOW);
   426       if (!DeveloperToolbar.introShownThisSession) {
   427         this.display.maybeShowIntro();
   428         DeveloperToolbar.introShownThisSession = true;
   429       }
   431       this._showPromise = null;
   432     });
   433   });
   435   return this._showPromise;
   436 };
   438 /**
   439  * Hide the developer toolbar.
   440  */
   441 DeveloperToolbar.prototype.hide = function() {
   442   // If we're already in the process of hiding, just use the other promise
   443   if (this._hidePromise != null) {
   444     return this._hidePromise;
   445   }
   447   // show() is async, so ensure we don't need to wait for show() to finish
   448   var waitPromise = this._showPromise || promise.resolve();
   450   this._hidePromise = waitPromise.then(() => {
   451     this._element.hidden = true;
   453     Services.prefs.setBoolPref("devtools.toolbar.visible", false);
   455     this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "false");
   456     this.destroy();
   458     this._telemetry.toolClosed("developertoolbar");
   459     this._notify(NOTIFICATIONS.HIDE);
   461     this._hidePromise = null;
   462   });
   464   return this._hidePromise;
   465 };
   467 /**
   468  * The devtools-unloaded event handler.
   469  * @private
   470  */
   471 DeveloperToolbar.prototype._devtoolsUnloaded = function() {
   472   let tabbrowser = this._chromeWindow.getBrowser();
   473   Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
   474 };
   476 /**
   477  * The devtools-loaded event handler.
   478  * @private
   479  */
   480 DeveloperToolbar.prototype._devtoolsLoaded = function() {
   481   let tabbrowser = this._chromeWindow.getBrowser();
   482   this._initErrorsCount(tabbrowser.selectedTab);
   483 };
   485 /**
   486  * Initialize the listeners needed for tracking the number of errors for a given
   487  * tab.
   488  *
   489  * @private
   490  * @param nsIDOMNode tab the xul:tab for which you want to track the number of
   491  * errors.
   492  */
   493 DeveloperToolbar.prototype._initErrorsCount = function(tab) {
   494   let tabId = tab.linkedPanel;
   495   if (tabId in this._errorsCount) {
   496     this._updateErrorsCount();
   497     return;
   498   }
   500   let window = tab.linkedBrowser.contentWindow;
   501   let listener = new ConsoleServiceListener(window, {
   502     onConsoleServiceMessage: this._onPageError.bind(this, tabId),
   503   });
   504   listener.init();
   506   this._errorListeners[tabId] = listener;
   507   this._errorsCount[tabId] = 0;
   508   this._warningsCount[tabId] = 0;
   510   let messages = listener.getCachedMessages();
   511   messages.forEach(this._onPageError.bind(this, tabId));
   513   this._updateErrorsCount();
   514 };
   516 /**
   517  * Stop the listeners needed for tracking the number of errors for a given
   518  * tab.
   519  *
   520  * @private
   521  * @param nsIDOMNode tab the xul:tab for which you want to stop tracking the
   522  * number of errors.
   523  */
   524 DeveloperToolbar.prototype._stopErrorsCount = function(tab) {
   525   let tabId = tab.linkedPanel;
   526   if (!(tabId in this._errorsCount) || !(tabId in this._warningsCount)) {
   527     this._updateErrorsCount();
   528     return;
   529   }
   531   this._errorListeners[tabId].destroy();
   532   delete this._errorListeners[tabId];
   533   delete this._errorsCount[tabId];
   534   delete this._warningsCount[tabId];
   536   this._updateErrorsCount();
   537 };
   539 /**
   540  * Hide the developer toolbar
   541  */
   542 DeveloperToolbar.prototype.destroy = function() {
   543   if (this._input == null) {
   544     return; // Already destroyed
   545   }
   547   let tabbrowser = this._chromeWindow.getBrowser();
   548   tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
   549   tabbrowser.tabContainer.removeEventListener("TabClose", this, false);
   550   tabbrowser.removeEventListener("load", this, true);
   551   tabbrowser.removeEventListener("beforeunload", this, true);
   553   Services.obs.removeObserver(this._devtoolsUnloaded, "devtools-unloaded");
   554   Services.obs.removeObserver(this._devtoolsLoaded, "devtools-loaded");
   555   Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
   557   this.display.focusManager.removeMonitoredElement(this.outputPanel._frame);
   558   this.display.focusManager.removeMonitoredElement(this._element);
   560   this.display.onVisibilityChange.remove(this.outputPanel._visibilityChanged, this.outputPanel);
   561   this.display.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged, this.tooltipPanel);
   562   this.display.onOutput.remove(this.outputPanel._outputChanged, this.outputPanel);
   563   this.display.destroy();
   564   this.outputPanel.destroy();
   565   this.tooltipPanel.destroy();
   566   delete this._input;
   568   // We could "delete this.display" etc if we have hard-to-track-down memory
   569   // leaks as a belt-and-braces approach, however this prevents our DOM node
   570   // hunter from looking in all the nooks and crannies, so it's better if we
   571   // can be leak-free without
   572   /*
   573   delete this.display;
   574   delete this.outputPanel;
   575   delete this.tooltipPanel;
   576   */
   577 };
   579 /**
   580  * Utility for sending notifications
   581  * @param topic a NOTIFICATION constant
   582  */
   583 DeveloperToolbar.prototype._notify = function(topic) {
   584   let data = { toolbar: this };
   585   data.wrappedJSObject = data;
   586   Services.obs.notifyObservers(data, topic, null);
   587 };
   589 /**
   590  * Update various parts of the UI when the current tab changes
   591  */
   592 DeveloperToolbar.prototype.handleEvent = function(ev) {
   593   if (ev.type == "TabSelect" || ev.type == "load") {
   594     if (this.visible) {
   595       this.display.reattach({
   596         contentDocument: this._chromeWindow.getBrowser().contentDocument
   597       });
   599       if (ev.type == "TabSelect") {
   600         this._initErrorsCount(ev.target);
   601       }
   602     }
   603   }
   604   else if (ev.type == "TabClose") {
   605     this._stopErrorsCount(ev.target);
   606   }
   607   else if (ev.type == "beforeunload") {
   608     this._onPageBeforeUnload(ev);
   609   }
   610 };
   612 /**
   613  * Count a page error received for the currently selected tab. This
   614  * method counts the JavaScript exceptions received and CSS errors/warnings.
   615  *
   616  * @private
   617  * @param string tabId the ID of the tab from where the page error comes.
   618  * @param object pageError the page error object received from the
   619  * PageErrorListener.
   620  */
   621 DeveloperToolbar.prototype._onPageError = function(tabId, pageError) {
   622   if (pageError.category == "CSS Parser" ||
   623       pageError.category == "CSS Loader") {
   624     return;
   625   }
   626   if ((pageError.flags & pageError.warningFlag) ||
   627       (pageError.flags & pageError.strictFlag)) {
   628     this._warningsCount[tabId]++;
   629   } else {
   630     this._errorsCount[tabId]++;
   631   }
   632   this._updateErrorsCount(tabId);
   633 };
   635 /**
   636  * The |beforeunload| event handler. This function resets the errors count when
   637  * a different page starts loading.
   638  *
   639  * @private
   640  * @param nsIDOMEvent ev the beforeunload DOM event.
   641  */
   642 DeveloperToolbar.prototype._onPageBeforeUnload = function(ev) {
   643   let window = ev.target.defaultView;
   644   if (window.top !== window) {
   645     return;
   646   }
   648   let tabs = this._chromeWindow.getBrowser().tabs;
   649   Array.prototype.some.call(tabs, function(tab) {
   650     if (tab.linkedBrowser.contentWindow === window) {
   651       let tabId = tab.linkedPanel;
   652       if (tabId in this._errorsCount || tabId in this._warningsCount) {
   653         this._errorsCount[tabId] = 0;
   654         this._warningsCount[tabId] = 0;
   655         this._updateErrorsCount(tabId);
   656       }
   657       return true;
   658     }
   659     return false;
   660   }, this);
   661 };
   663 /**
   664  * Update the page errors count displayed in the Web Console button for the
   665  * currently selected tab.
   666  *
   667  * @private
   668  * @param string [changedTabId] Optional. The tab ID that had its page errors
   669  * count changed. If this is provided and it doesn't match the currently
   670  * selected tab, then the button is not updated.
   671  */
   672 DeveloperToolbar.prototype._updateErrorsCount = function(changedTabId) {
   673   let tabId = this._chromeWindow.getBrowser().selectedTab.linkedPanel;
   674   if (changedTabId && tabId != changedTabId) {
   675     return;
   676   }
   678   let errors = this._errorsCount[tabId];
   679   let warnings = this._warningsCount[tabId];
   680   let btn = this._errorCounterButton;
   681   if (errors) {
   682     let errorsText = toolboxStrings
   683                      .GetStringFromName("toolboxToggleButton.errors");
   684     errorsText = PluralForm.get(errors, errorsText).replace("#1", errors);
   686     let warningsText = toolboxStrings
   687                        .GetStringFromName("toolboxToggleButton.warnings");
   688     warningsText = PluralForm.get(warnings, warningsText).replace("#1", warnings);
   690     let tooltiptext = toolboxStrings
   691                       .formatStringFromName("toolboxToggleButton.tooltip",
   692                                             [errorsText, warningsText], 2);
   694     btn.setAttribute("error-count", errors);
   695     btn.setAttribute("tooltiptext", tooltiptext);
   696   } else {
   697     btn.removeAttribute("error-count");
   698     btn.setAttribute("tooltiptext", btn._defaultTooltipText);
   699   }
   701   this.emit("errors-counter-updated");
   702 };
   704 /**
   705  * Reset the errors counter for the given tab.
   706  *
   707  * @param nsIDOMElement tab The xul:tab for which you want to reset the page
   708  * errors counters.
   709  */
   710 DeveloperToolbar.prototype.resetErrorsCount = function(tab) {
   711   let tabId = tab.linkedPanel;
   712   if (tabId in this._errorsCount || tabId in this._warningsCount) {
   713     this._errorsCount[tabId] = 0;
   714     this._warningsCount[tabId] = 0;
   715     this._updateErrorsCount(tabId);
   716   }
   717 };
   719 /**
   720  * Creating a OutputPanel is asynchronous
   721  */
   722 function OutputPanel() {
   723   throw new Error('Use OutputPanel.create()');
   724 }
   726 /**
   727  * Panel to handle command line output.
   728  *
   729  * There is a tooltip bug on Windows and OSX that prevents tooltips from being
   730  * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
   731  * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
   732  * We now use a tooltip on Linux and a panel on OSX & Windows.
   733  *
   734  * If a panel has no content and no height it is not shown when openPopup is
   735  * called on Windows and OSX (bug 692348) ... this prevents the panel from
   736  * appearing the first time it is shown. Setting the panel's height to 1px
   737  * before calling openPopup works around this issue as we resize it ourselves
   738  * anyway.
   739  *
   740  * @param devtoolbar The parent DeveloperToolbar object
   741  */
   742 OutputPanel.create = function(devtoolbar) {
   743   var outputPanel = Object.create(OutputPanel.prototype);
   744   return outputPanel._init(devtoolbar);
   745 };
   747 /**
   748  * @private See OutputPanel.create
   749  */
   750 OutputPanel.prototype._init = function(devtoolbar) {
   751   this._devtoolbar = devtoolbar;
   752   this._input = this._devtoolbar._input;
   753   this._toolbar = this._devtoolbar._doc.getElementById("developer-toolbar");
   755   /*
   756   <tooltip|panel id="gcli-output"
   757          noautofocus="true"
   758          noautohide="true"
   759          class="gcli-panel">
   760     <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
   761                  id="gcli-output-frame"
   762                  src="chrome://browser/content/devtools/commandlineoutput.xhtml"
   763                  sandbox="allow-same-origin"/>
   764   </tooltip|panel>
   765   */
   767   // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
   768   // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
   769   this._panel = this._devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
   771   this._panel.id = "gcli-output";
   772   this._panel.classList.add("gcli-panel");
   774   if (isLinux) {
   775     this.canHide = false;
   776     this._onpopuphiding = this._onpopuphiding.bind(this);
   777     this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
   778   } else {
   779     this._panel.setAttribute("noautofocus", "true");
   780     this._panel.setAttribute("noautohide", "true");
   782     // Bug 692348: On Windows and OSX if a panel has no content and no height
   783     // openPopup fails to display it. Setting the height to 1px alows the panel
   784     // to be displayed before has content or a real height i.e. the first time
   785     // it is displayed.
   786     this._panel.setAttribute("height", "1px");
   787   }
   789   this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
   791   this._frame = this._devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
   792   this._frame.id = "gcli-output-frame";
   793   this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlineoutput.xhtml");
   794   this._frame.setAttribute("sandbox", "allow-same-origin");
   795   this._panel.appendChild(this._frame);
   797   this.displayedOutput = undefined;
   799   this._update = this._update.bind(this);
   801   // Wire up the element from the iframe, and resolve the promise
   802   let deferred = promise.defer();
   803   let onload = () => {
   804     this._frame.removeEventListener("load", onload, true);
   806     this.document = this._frame.contentDocument;
   808     this._div = this.document.getElementById("gcli-output-root");
   809     this._div.classList.add('gcli-row-out');
   810     this._div.setAttribute('aria-live', 'assertive');
   812     let styles = this._toolbar.ownerDocument.defaultView
   813                     .getComputedStyle(this._toolbar);
   814     this._div.setAttribute("dir", styles.direction);
   816     deferred.resolve(this);
   817   };
   818   this._frame.addEventListener("load", onload, true);
   820   return deferred.promise;
   821 }
   823 /**
   824  * Prevent the popup from hiding if it is not permitted via this.canHide.
   825  */
   826 OutputPanel.prototype._onpopuphiding = function(ev) {
   827   // TODO: When we switch back from tooltip to panel we can remove this hack:
   828   // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
   829   if (isLinux && !this.canHide) {
   830     ev.preventDefault();
   831   }
   832 };
   834 /**
   835  * Display the OutputPanel.
   836  */
   837 OutputPanel.prototype.show = function() {
   838   if (isLinux) {
   839     this.canHide = false;
   840   }
   842   // We need to reset the iframe size in order for future size calculations to
   843   // be correct
   844   this._frame.style.minHeight = this._frame.style.maxHeight = 0;
   845   this._frame.style.minWidth = 0;
   847   this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null);
   848   this._resize();
   850   this._input.focus();
   851 };
   853 /**
   854  * Internal helper to set the height of the output panel to fit the available
   855  * content;
   856  */
   857 OutputPanel.prototype._resize = function() {
   858   if (this._panel == null || this.document == null || !this._panel.state == "closed") {
   859     return
   860   }
   862   // Set max panel width to match any content with a max of the width of the
   863   // browser window.
   864   let maxWidth = this._panel.ownerDocument.documentElement.clientWidth;
   866   // Adjust max width according to OS.
   867   // We'd like to put this in CSS but we can't:
   868   //   body { width: calc(min(-5px, max-content)); }
   869   //   #_panel { max-width: -5px; }
   870   switch(OS) {
   871     case "Linux":
   872       maxWidth -= 5;
   873       break;
   874     case "Darwin":
   875       maxWidth -= 25;
   876       break;
   877     case "WINNT":
   878       maxWidth -= 5;
   879       break;
   880   }
   882   this.document.body.style.width = "-moz-max-content";
   883   let style = this._frame.contentWindow.getComputedStyle(this.document.body);
   884   let frameWidth = parseInt(style.width, 10);
   885   let width = Math.min(maxWidth, frameWidth);
   886   this.document.body.style.width = width + "px";
   888   // Set the width of the iframe.
   889   this._frame.style.minWidth = width + "px";
   890   this._panel.style.maxWidth = maxWidth + "px";
   892   // browserAdjustment is used to correct the panel height according to the
   893   // browsers borders etc.
   894   const browserAdjustment = 15;
   896   // Set max panel height to match any content with a max of the height of the
   897   // browser window.
   898   let maxHeight =
   899     this._panel.ownerDocument.documentElement.clientHeight - browserAdjustment;
   900   let height = Math.min(maxHeight, this.document.documentElement.scrollHeight);
   902   // Set the height of the iframe. Setting iframe.height does not work.
   903   this._frame.style.minHeight = this._frame.style.maxHeight = height + "px";
   905   // Set the height and width of the panel to match the iframe.
   906   this._panel.sizeTo(width, height);
   908   // Move the panel to the correct position in the case that it has been
   909   // positioned incorrectly.
   910   let screenX = this._input.boxObject.screenX;
   911   let screenY = this._toolbar.boxObject.screenY;
   912   this._panel.moveTo(screenX, screenY - height);
   913 };
   915 /**
   916  * Called by GCLI when a command is executed.
   917  */
   918 OutputPanel.prototype._outputChanged = function(ev) {
   919   if (ev.output.hidden) {
   920     return;
   921   }
   923   this.remove();
   925   this.displayedOutput = ev.output;
   927   if (this.displayedOutput.completed) {
   928     this._update();
   929   }
   930   else {
   931     this.displayedOutput.promise.then(this._update, this._update)
   932                                 .then(null, console.error);
   933   }
   934 };
   936 /**
   937  * Called when displayed Output says it's changed or from outputChanged, which
   938  * happens when there is a new displayed Output.
   939  */
   940 OutputPanel.prototype._update = function() {
   941   // destroy has been called, bail out
   942   if (this._div == null) {
   943     return;
   944   }
   946   // Empty this._div
   947   while (this._div.hasChildNodes()) {
   948     this._div.removeChild(this._div.firstChild);
   949   }
   951   if (this.displayedOutput.data != null) {
   952     let context = this._devtoolbar.display.requisition.conversionContext;
   953     this.displayedOutput.convert('dom', context).then((node) => {
   954       while (this._div.hasChildNodes()) {
   955         this._div.removeChild(this._div.firstChild);
   956       }
   958       var links = node.ownerDocument.querySelectorAll('*[href]');
   959       for (var i = 0; i < links.length; i++) {
   960         links[i].setAttribute('target', '_blank');
   961       }
   963       this._div.appendChild(node);
   964       this.show();
   965     });
   966   }
   967 };
   969 /**
   970  * Detach listeners from the currently displayed Output.
   971  */
   972 OutputPanel.prototype.remove = function() {
   973   if (isLinux) {
   974     this.canHide = true;
   975   }
   977   if (this._panel && this._panel.hidePopup) {
   978     this._panel.hidePopup();
   979   }
   981   if (this.displayedOutput) {
   982     delete this.displayedOutput;
   983   }
   984 };
   986 /**
   987  * Detach listeners from the currently displayed Output.
   988  */
   989 OutputPanel.prototype.destroy = function() {
   990   this.remove();
   992   this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
   994   this._panel.removeChild(this._frame);
   995   this._toolbar.parentElement.removeChild(this._panel);
   997   delete this._devtoolbar;
   998   delete this._input;
   999   delete this._toolbar;
  1000   delete this._onpopuphiding;
  1001   delete this._panel;
  1002   delete this._frame;
  1003   delete this._content;
  1004   delete this._div;
  1005   delete this.document;
  1006 };
  1008 /**
  1009  * Called by GCLI to indicate that we should show or hide one either the
  1010  * tooltip panel or the output panel.
  1011  */
  1012 OutputPanel.prototype._visibilityChanged = function(ev) {
  1013   if (ev.outputVisible === true) {
  1014     // this.show is called by _outputChanged
  1015   } else {
  1016     if (isLinux) {
  1017       this.canHide = true;
  1019     this._panel.hidePopup();
  1021 };
  1023 /**
  1024  * Creating a TooltipPanel is asynchronous
  1025  */
  1026 function TooltipPanel() {
  1027   throw new Error('Use TooltipPanel.create()');
  1030 /**
  1031  * Panel to handle tooltips.
  1033  * There is a tooltip bug on Windows and OSX that prevents tooltips from being
  1034  * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
  1035  * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
  1036  * We now use a tooltip on Linux and a panel on OSX & Windows.
  1038  * If a panel has no content and no height it is not shown when openPopup is
  1039  * called on Windows and OSX (bug 692348) ... this prevents the panel from
  1040  * appearing the first time it is shown. Setting the panel's height to 1px
  1041  * before calling openPopup works around this issue as we resize it ourselves
  1042  * anyway.
  1044  * @param devtoolbar The parent DeveloperToolbar object
  1045  */
  1046 TooltipPanel.create = function(devtoolbar) {
  1047   var tooltipPanel = Object.create(TooltipPanel.prototype);
  1048   return tooltipPanel._init(devtoolbar);
  1049 };
  1051 /**
  1052  * @private See TooltipPanel.create
  1053  */
  1054 TooltipPanel.prototype._init = function(devtoolbar) {
  1055   let deferred = promise.defer();
  1057   let chromeDocument = devtoolbar._doc;
  1058   this._input = devtoolbar._doc.querySelector(".gclitoolbar-input-node");
  1059   this._toolbar = devtoolbar._doc.querySelector("#developer-toolbar");
  1060   this._dimensions = { start: 0, end: 0 };
  1062   /*
  1063   <tooltip|panel id="gcli-tooltip"
  1064          type="arrow"
  1065          noautofocus="true"
  1066          noautohide="true"
  1067          class="gcli-panel">
  1068     <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
  1069                  id="gcli-tooltip-frame"
  1070                  src="chrome://browser/content/devtools/commandlinetooltip.xhtml"
  1071                  flex="1"
  1072                  sandbox="allow-same-origin"/>
  1073   </tooltip|panel>
  1074   */
  1076   // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
  1077   // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
  1078   this._panel = devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
  1080   this._panel.id = "gcli-tooltip";
  1081   this._panel.classList.add("gcli-panel");
  1083   if (isLinux) {
  1084     this.canHide = false;
  1085     this._onpopuphiding = this._onpopuphiding.bind(this);
  1086     this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
  1087   } else {
  1088     this._panel.setAttribute("noautofocus", "true");
  1089     this._panel.setAttribute("noautohide", "true");
  1091     // Bug 692348: On Windows and OSX if a panel has no content and no height
  1092     // openPopup fails to display it. Setting the height to 1px alows the panel
  1093     // to be displayed before has content or a real height i.e. the first time
  1094     // it is displayed.
  1095     this._panel.setAttribute("height", "1px");
  1098   this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
  1100   this._frame = devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
  1101   this._frame.id = "gcli-tooltip-frame";
  1102   this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlinetooltip.xhtml");
  1103   this._frame.setAttribute("flex", "1");
  1104   this._frame.setAttribute("sandbox", "allow-same-origin");
  1105   this._panel.appendChild(this._frame);
  1107   /**
  1108    * Wire up the element from the iframe, and resolve the promise.
  1109    */
  1110   let onload = () => {
  1111     this._frame.removeEventListener("load", onload, true);
  1113     this.document = this._frame.contentDocument;
  1114     this.hintElement = this.document.getElementById("gcli-tooltip-root");
  1115     this._connector = this.document.getElementById("gcli-tooltip-connector");
  1117     let styles = this._toolbar.ownerDocument.defaultView
  1118                     .getComputedStyle(this._toolbar);
  1119     this.hintElement.setAttribute("dir", styles.direction);
  1121     deferred.resolve(this);
  1122   };
  1123   this._frame.addEventListener("load", onload, true);
  1125   return deferred.promise;
  1128 /**
  1129  * Prevent the popup from hiding if it is not permitted via this.canHide.
  1130  */
  1131 TooltipPanel.prototype._onpopuphiding = function(ev) {
  1132   // TODO: When we switch back from tooltip to panel we can remove this hack:
  1133   // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
  1134   if (isLinux && !this.canHide) {
  1135     ev.preventDefault();
  1137 };
  1139 /**
  1140  * Display the TooltipPanel.
  1141  */
  1142 TooltipPanel.prototype.show = function(dimensions) {
  1143   if (!dimensions) {
  1144     dimensions = { start: 0, end: 0 };
  1146   this._dimensions = dimensions;
  1148   // This is nasty, but displaying the panel causes it to re-flow, which can
  1149   // change the size it should be, so we need to resize the iframe after the
  1150   // panel has displayed
  1151   this._panel.ownerDocument.defaultView.setTimeout(() => {
  1152     this._resize();
  1153   }, 0);
  1155   if (isLinux) {
  1156     this.canHide = false;
  1159   this._resize();
  1160   this._panel.openPopup(this._input, "before_start", dimensions.start * 10, 0,
  1161                         false, false, null);
  1162   this._input.focus();
  1163 };
  1165 /**
  1166  * One option is to spend lots of time taking an average width of characters
  1167  * in the current font, dynamically, and weighting for the frequency of use of
  1168  * various characters, or even to render the given string off screen, and then
  1169  * measure the width.
  1170  * Or we could do this...
  1171  */
  1172 const AVE_CHAR_WIDTH = 4.5;
  1174 /**
  1175  * Display the TooltipPanel.
  1176  */
  1177 TooltipPanel.prototype._resize = function() {
  1178   if (this._panel == null || this.document == null || !this._panel.state == "closed") {
  1179     return
  1182   let offset = 10 + Math.floor(this._dimensions.start * AVE_CHAR_WIDTH);
  1183   this._panel.style.marginLeft = offset + "px";
  1185   /*
  1186   // Bug 744906: UX review - Not sure if we want this code to fatten connector
  1187   // with param width
  1188   let width = Math.floor(this._dimensions.end * AVE_CHAR_WIDTH);
  1189   width = Math.min(width, 100);
  1190   width = Math.max(width, 10);
  1191   this._connector.style.width = width + "px";
  1192   */
  1194   this._frame.height = this.document.body.scrollHeight;
  1195 };
  1197 /**
  1198  * Hide the TooltipPanel.
  1199  */
  1200 TooltipPanel.prototype.remove = function() {
  1201   if (isLinux) {
  1202     this.canHide = true;
  1204   if (this._panel && this._panel.hidePopup) {
  1205     this._panel.hidePopup();
  1207 };
  1209 /**
  1210  * Hide the TooltipPanel.
  1211  */
  1212 TooltipPanel.prototype.destroy = function() {
  1213   this.remove();
  1215   this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
  1217   this._panel.removeChild(this._frame);
  1218   this._toolbar.parentElement.removeChild(this._panel);
  1220   delete this._connector;
  1221   delete this._dimensions;
  1222   delete this._input;
  1223   delete this._onpopuphiding;
  1224   delete this._panel;
  1225   delete this._frame;
  1226   delete this._toolbar;
  1227   delete this._content;
  1228   delete this.document;
  1229   delete this.hintElement;
  1230 };
  1232 /**
  1233  * Called by GCLI to indicate that we should show or hide one either the
  1234  * tooltip panel or the output panel.
  1235  */
  1236 TooltipPanel.prototype._visibilityChanged = function(ev) {
  1237   if (ev.tooltipVisible === true) {
  1238     this.show(ev.dimensions);
  1239   } else {
  1240     if (isLinux) {
  1241       this.canHide = true;
  1243     this._panel.hidePopup();
  1245 };

mercurial