browser/devtools/framework/toolbox.js

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 const MAX_ORDINAL = 99;
     8 const ZOOM_PREF = "devtools.toolbox.zoomValue";
     9 const MIN_ZOOM = 0.5;
    10 const MAX_ZOOM = 2;
    12 let {Cc, Ci, Cu} = require("chrome");
    13 let {Promise: promise} = require("resource://gre/modules/Promise.jsm");
    14 let EventEmitter = require("devtools/toolkit/event-emitter");
    15 let Telemetry = require("devtools/shared/telemetry");
    16 let HUDService = require("devtools/webconsole/hudservice");
    18 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    19 Cu.import("resource://gre/modules/Services.jsm");
    20 Cu.import("resource:///modules/devtools/gDevTools.jsm");
    21 Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
    22 Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
    23 Cu.import("resource://gre/modules/Task.jsm");
    25 loader.lazyGetter(this, "Hosts", () => require("devtools/framework/toolbox-hosts").Hosts);
    27 loader.lazyImporter(this, "CommandUtils", "resource:///modules/devtools/DeveloperToolbar.jsm");
    29 loader.lazyGetter(this, "toolboxStrings", () => {
    30   let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
    31   return (name, ...args) => {
    32     try {
    33       if (!args.length) {
    34         return bundle.GetStringFromName(name);
    35       }
    36       return bundle.formatStringFromName(name, args, args.length);
    37     } catch (ex) {
    38       Services.console.logStringMessage("Error reading '" + name + "'");
    39       return null;
    40     }
    41   };
    42 });
    44 loader.lazyGetter(this, "Selection", () => require("devtools/framework/selection").Selection);
    45 loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront);
    47 /**
    48  * A "Toolbox" is the component that holds all the tools for one specific
    49  * target. Visually, it's a document that includes the tools tabs and all
    50  * the iframes where the tool panels will be living in.
    51  *
    52  * @param {object} target
    53  *        The object the toolbox is debugging.
    54  * @param {string} selectedTool
    55  *        Tool to select initially
    56  * @param {Toolbox.HostType} hostType
    57  *        Type of host that will host the toolbox (e.g. sidebar, window)
    58  * @param {object} hostOptions
    59  *        Options for host specifically
    60  */
    61 function Toolbox(target, selectedTool, hostType, hostOptions) {
    62   this._target = target;
    63   this._toolPanels = new Map();
    64   this._telemetry = new Telemetry();
    66   this._toolRegistered = this._toolRegistered.bind(this);
    67   this._toolUnregistered = this._toolUnregistered.bind(this);
    68   this._refreshHostTitle = this._refreshHostTitle.bind(this);
    69   this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this)
    70   this.destroy = this.destroy.bind(this);
    71   this.highlighterUtils = new ToolboxHighlighterUtils(this);
    72   this._highlighterReady = this._highlighterReady.bind(this);
    73   this._highlighterHidden = this._highlighterHidden.bind(this);
    75   this._target.on("close", this.destroy);
    77   if (!hostType) {
    78     hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST);
    79   }
    80   if (!selectedTool) {
    81     selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
    82   }
    83   if (!gDevTools.getToolDefinition(selectedTool)) {
    84     selectedTool = "webconsole";
    85   }
    86   this._defaultToolId = selectedTool;
    88   this._host = this._createHost(hostType, hostOptions);
    90   EventEmitter.decorate(this);
    92   this._target.on("navigate", this._refreshHostTitle);
    93   this.on("host-changed", this._refreshHostTitle);
    94   this.on("select", this._refreshHostTitle);
    96   gDevTools.on("tool-registered", this._toolRegistered);
    97   gDevTools.on("tool-unregistered", this._toolUnregistered);
    98 }
    99 exports.Toolbox = Toolbox;
   101 /**
   102  * The toolbox can be 'hosted' either embedded in a browser window
   103  * or in a separate window.
   104  */
   105 Toolbox.HostType = {
   106   BOTTOM: "bottom",
   107   SIDE: "side",
   108   WINDOW: "window",
   109   CUSTOM: "custom"
   110 };
   112 Toolbox.prototype = {
   113   _URL: "chrome://browser/content/devtools/framework/toolbox.xul",
   115   _prefs: {
   116     LAST_HOST: "devtools.toolbox.host",
   117     LAST_TOOL: "devtools.toolbox.selectedTool",
   118     SIDE_ENABLED: "devtools.toolbox.sideEnabled"
   119   },
   121   currentToolId: null,
   123   /**
   124    * Returns a *copy* of the _toolPanels collection.
   125    *
   126    * @return {Map} panels
   127    *         All the running panels in the toolbox
   128    */
   129   getToolPanels: function() {
   130     return new Map(this._toolPanels);
   131   },
   133   /**
   134    * Access the panel for a given tool
   135    */
   136   getPanel: function(id) {
   137     return this._toolPanels.get(id);
   138   },
   140   /**
   141    * This is a shortcut for getPanel(currentToolId) because it is much more
   142    * likely that we're going to want to get the panel that we've just made
   143    * visible
   144    */
   145   getCurrentPanel: function() {
   146     return this._toolPanels.get(this.currentToolId);
   147   },
   149   /**
   150    * Get/alter the target of a Toolbox so we're debugging something different.
   151    * See Target.jsm for more details.
   152    * TODO: Do we allow |toolbox.target = null;| ?
   153    */
   154   get target() {
   155     return this._target;
   156   },
   158   /**
   159    * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate
   160    * tab. See HostType for more details.
   161    */
   162   get hostType() {
   163     return this._host.type;
   164   },
   166   /**
   167    * Get the iframe containing the toolbox UI.
   168    */
   169   get frame() {
   170     return this._host.frame;
   171   },
   173   /**
   174    * Shortcut to the document containing the toolbox UI
   175    */
   176   get doc() {
   177     return this.frame.contentDocument;
   178   },
   180   /**
   181    * Get current zoom level of toolbox
   182    */
   183   get zoomValue() {
   184     return parseFloat(Services.prefs.getCharPref(ZOOM_PREF));
   185   },
   187   /**
   188    * Get the toolbox highlighter front. Note that it may not always have been
   189    * initialized first. Use `initInspector()` if needed.
   190    */
   191   get highlighter() {
   192     if (this.highlighterUtils.isRemoteHighlightable) {
   193       return this._highlighter;
   194     } else {
   195       return null;
   196     }
   197   },
   199   /**
   200    * Get the toolbox's inspector front. Note that it may not always have been
   201    * initialized first. Use `initInspector()` if needed.
   202    */
   203   get inspector() {
   204     return this._inspector;
   205   },
   207   /**
   208    * Get the toolbox's walker front. Note that it may not always have been
   209    * initialized first. Use `initInspector()` if needed.
   210    */
   211   get walker() {
   212     return this._walker;
   213   },
   215   /**
   216    * Get the toolbox's node selection. Note that it may not always have been
   217    * initialized first. Use `initInspector()` if needed.
   218    */
   219   get selection() {
   220     return this._selection;
   221   },
   223   /**
   224    * Get the toggled state of the split console
   225    */
   226   get splitConsole() {
   227     return this._splitConsole;
   228   },
   230   /**
   231    * Open the toolbox
   232    */
   233   open: function() {
   234     let deferred = promise.defer();
   236     return this._host.create().then(iframe => {
   237       let deferred = promise.defer();
   239       let domReady = () => {
   240         this.isReady = true;
   242         let closeButton = this.doc.getElementById("toolbox-close");
   243         closeButton.addEventListener("command", this.destroy, true);
   245         this._buildDockButtons();
   246         this._buildOptions();
   247         this._buildTabs();
   248         this._buildButtons();
   249         this._addKeysToWindow();
   250         this._addToolSwitchingKeys();
   251         this._addZoomKeys();
   252         this._loadInitialZoom();
   254         this._telemetry.toolOpened("toolbox");
   256         this.selectTool(this._defaultToolId).then(panel => {
   257           this.emit("ready");
   258           deferred.resolve();
   259         });
   260       };
   262       // Load the toolbox-level actor fronts and utilities now
   263       this._target.makeRemote().then(() => {
   264         iframe.setAttribute("src", this._URL);
   265         let domHelper = new DOMHelpers(iframe.contentWindow);
   266         domHelper.onceDOMReady(domReady);
   267       });
   269       return deferred.promise;
   270     });
   271   },
   273   _buildOptions: function() {
   274     let key = this.doc.getElementById("toolbox-options-key");
   275     key.addEventListener("command", () => {
   276       this.selectTool("options");
   277     }, true);
   278   },
   280   _isResponsiveModeActive: function() {
   281     let responsiveModeActive = false;
   282     if (this.target.isLocalTab) {
   283       let tab = this.target.tab;
   284       let browserWindow = tab.ownerDocument.defaultView;
   285       let responsiveUIManager = browserWindow.ResponsiveUI.ResponsiveUIManager;
   286       responsiveModeActive = responsiveUIManager.isActiveForTab(tab);
   287     }
   288     return responsiveModeActive;
   289   },
   291   _splitConsoleOnKeypress: function(e) {
   292     let responsiveModeActive = this._isResponsiveModeActive();
   293     if (e.keyCode === e.DOM_VK_ESCAPE && !responsiveModeActive) {
   294       this.toggleSplitConsole();
   295     }
   296   },
   298   _addToolSwitchingKeys: function() {
   299     let nextKey = this.doc.getElementById("toolbox-next-tool-key");
   300     nextKey.addEventListener("command", this.selectNextTool.bind(this), true);
   301     let prevKey = this.doc.getElementById("toolbox-previous-tool-key");
   302     prevKey.addEventListener("command", this.selectPreviousTool.bind(this), true);
   304     // Split console uses keypress instead of command so the event can be
   305     // cancelled with stopPropagation on the keypress, and not preventDefault.
   306     this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false);
   307   },
   309   /**
   310    * Make sure that the console is showing up properly based on all the
   311    * possible conditions.
   312    *   1) If the console tab is selected, then regardless of split state
   313    *      it should take up the full height of the deck, and we should
   314    *      hide the deck and splitter.
   315    *   2) If the console tab is not selected and it is split, then we should
   316    *      show the splitter, deck, and console.
   317    *   3) If the console tab is not selected and it is *not* split,
   318    *      then we should hide the console and splitter, and show the deck
   319    *      at full height.
   320    */
   321   _refreshConsoleDisplay: function() {
   322     let deck = this.doc.getElementById("toolbox-deck");
   323     let webconsolePanel = this.doc.getElementById("toolbox-panel-webconsole");
   324     let splitter = this.doc.getElementById("toolbox-console-splitter");
   325     let openedConsolePanel = this.currentToolId === "webconsole";
   327     if (openedConsolePanel) {
   328       deck.setAttribute("collapsed", "true");
   329       splitter.setAttribute("hidden", "true");
   330       webconsolePanel.removeAttribute("collapsed");
   331     } else {
   332       deck.removeAttribute("collapsed");
   333       if (this._splitConsole) {
   334         webconsolePanel.removeAttribute("collapsed");
   335         splitter.removeAttribute("hidden");
   336       } else {
   337         webconsolePanel.setAttribute("collapsed", "true");
   338         splitter.setAttribute("hidden", "true");
   339       }
   340     }
   341   },
   343   /**
   344    * Wire up the listeners for the zoom keys.
   345    */
   346   _addZoomKeys: function() {
   347     let inKey = this.doc.getElementById("toolbox-zoom-in-key");
   348     inKey.addEventListener("command", this.zoomIn.bind(this), true);
   350     let inKey2 = this.doc.getElementById("toolbox-zoom-in-key2");
   351     inKey2.addEventListener("command", this.zoomIn.bind(this), true);
   353     let outKey = this.doc.getElementById("toolbox-zoom-out-key");
   354     outKey.addEventListener("command", this.zoomOut.bind(this), true);
   356     let resetKey = this.doc.getElementById("toolbox-zoom-reset-key");
   357     resetKey.addEventListener("command", this.zoomReset.bind(this), true);
   358   },
   360   /**
   361    * Set zoom on toolbox to whatever the last setting was.
   362    */
   363   _loadInitialZoom: function() {
   364     this.setZoom(this.zoomValue);
   365   },
   367   /**
   368    * Increase zoom level of toolbox window - make things bigger.
   369    */
   370   zoomIn: function() {
   371     this.setZoom(this.zoomValue + 0.1);
   372   },
   374   /**
   375    * Decrease zoom level of toolbox window - make things smaller.
   376    */
   377   zoomOut: function() {
   378     this.setZoom(this.zoomValue - 0.1);
   379   },
   381   /**
   382    * Reset zoom level of the toolbox window.
   383    */
   384   zoomReset: function() {
   385     this.setZoom(1);
   386   },
   388   /**
   389    * Set zoom level of the toolbox window.
   390    *
   391    * @param {number} zoomValue
   392    *        Zoom level e.g. 1.2
   393    */
   394   setZoom: function(zoomValue) {
   395     // cap zoom value
   396     zoomValue = Math.max(zoomValue, MIN_ZOOM);
   397     zoomValue = Math.min(zoomValue, MAX_ZOOM);
   399     let contViewer = this.frame.docShell.contentViewer;
   400     let docViewer = contViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
   402     docViewer.fullZoom = zoomValue;
   404     Services.prefs.setCharPref(ZOOM_PREF, zoomValue);
   405   },
   407   /**
   408    * Adds the keys and commands to the Toolbox Window in window mode.
   409    */
   410   _addKeysToWindow: function() {
   411     if (this.hostType != Toolbox.HostType.WINDOW) {
   412       return;
   413     }
   415     let doc = this.doc.defaultView.parent.document;
   417     for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) {
   418       // Prevent multiple entries for the same tool.
   419       if (!toolDefinition.key || doc.getElementById("key_" + id)) {
   420         continue;
   421       }
   423       let toolId = id;
   424       let key = doc.createElement("key");
   426       key.id = "key_" + toolId;
   428       if (toolDefinition.key.startsWith("VK_")) {
   429         key.setAttribute("keycode", toolDefinition.key);
   430       } else {
   431         key.setAttribute("key", toolDefinition.key);
   432       }
   434       key.setAttribute("modifiers", toolDefinition.modifiers);
   435       key.setAttribute("oncommand", "void(0);"); // needed. See bug 371900
   436       key.addEventListener("command", () => {
   437         this.selectTool(toolId).then(() => this.fireCustomKey(toolId));
   438       }, true);
   439       doc.getElementById("toolbox-keyset").appendChild(key);
   440     }
   442     // Add key for toggling the browser console from the detached window
   443     if (!doc.getElementById("key_browserconsole")) {
   444       let key = doc.createElement("key");
   445       key.id = "key_browserconsole";
   447       key.setAttribute("key", toolboxStrings("browserConsoleCmd.commandkey"));
   448       key.setAttribute("modifiers", "accel,shift");
   449       key.setAttribute("oncommand", "void(0)"); // needed. See bug 371900
   450       key.addEventListener("command", () => {
   451         HUDService.toggleBrowserConsole();
   452       }, true);
   453       doc.getElementById("toolbox-keyset").appendChild(key);
   454     }
   455   },
   457   /**
   458    * Handle any custom key events.  Returns true if there was a custom key binding run
   459    * @param {string} toolId
   460    *        Which tool to run the command on (skip if not current)
   461    */
   462   fireCustomKey: function(toolId) {
   463     let toolDefinition = gDevTools.getToolDefinition(toolId);
   465     if (toolDefinition.onkey &&
   466         ((this.currentToolId === toolId) ||
   467           (toolId == "webconsole" && this.splitConsole))) {
   468       toolDefinition.onkey(this.getCurrentPanel(), this);
   469     }
   470   },
   472   /**
   473    * Build the buttons for changing hosts. Called every time
   474    * the host changes.
   475    */
   476   _buildDockButtons: function() {
   477     let dockBox = this.doc.getElementById("toolbox-dock-buttons");
   479     while (dockBox.firstChild) {
   480       dockBox.removeChild(dockBox.firstChild);
   481     }
   483     if (!this._target.isLocalTab) {
   484       return;
   485     }
   487     let closeButton = this.doc.getElementById("toolbox-close");
   488     if (this.hostType == Toolbox.HostType.WINDOW) {
   489       closeButton.setAttribute("hidden", "true");
   490     } else {
   491       closeButton.removeAttribute("hidden");
   492     }
   494     let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
   496     for (let type in Toolbox.HostType) {
   497       let position = Toolbox.HostType[type];
   498       if (position == this.hostType ||
   499           position == Toolbox.HostType.CUSTOM ||
   500           (!sideEnabled && position == Toolbox.HostType.SIDE)) {
   501         continue;
   502       }
   504       let button = this.doc.createElement("toolbarbutton");
   505       button.id = "toolbox-dock-" + position;
   506       button.className = "toolbox-dock-button";
   507       button.setAttribute("tooltiptext", toolboxStrings("toolboxDockButtons." +
   508                                                         position + ".tooltip"));
   509       button.addEventListener("command", () => {
   510         this.switchHost(position);
   511       });
   513       dockBox.appendChild(button);
   514     }
   515   },
   517   /**
   518    * Add tabs to the toolbox UI for registered tools
   519    */
   520   _buildTabs: function() {
   521     for (let definition of gDevTools.getToolDefinitionArray()) {
   522       this._buildTabForTool(definition);
   523     }
   524   },
   526   /**
   527    * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
   528    */
   529   _buildButtons: function() {
   530     this._buildPickerButton();
   532     if (!this.target.isLocalTab) {
   533       return;
   534     }
   536     let spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
   537     let environment = CommandUtils.createEnvironment(this, '_target');
   538     this._requisition = CommandUtils.createRequisition(environment);
   539     let buttons = CommandUtils.createButtons(spec, this._target,
   540                                              this.doc, this._requisition);
   541     let container = this.doc.getElementById("toolbox-buttons");
   542     buttons.forEach(container.appendChild.bind(container));
   543     this.setToolboxButtonsVisibility();
   544   },
   546   /**
   547    * Adding the element picker button is done here unlike the other buttons
   548    * since we want it to work for remote targets too
   549    */
   550   _buildPickerButton: function() {
   551     this._pickerButton = this.doc.createElement("toolbarbutton");
   552     this._pickerButton.id = "command-button-pick";
   553     this._pickerButton.className = "command-button command-button-invertable";
   554     this._pickerButton.setAttribute("tooltiptext", toolboxStrings("pickButton.tooltip"));
   556     let container = this.doc.querySelector("#toolbox-buttons");
   557     container.appendChild(this._pickerButton);
   559     this._togglePicker = this.highlighterUtils.togglePicker.bind(this.highlighterUtils);
   560     this._pickerButton.addEventListener("command", this._togglePicker, false);
   561   },
   563   /**
   564    * Return all toolbox buttons (command buttons, plus any others that were
   565    * added manually).
   566    */
   567   get toolboxButtons() {
   568     // White-list buttons that can be toggled to prevent adding prefs for
   569     // addons that have manually inserted toolbarbuttons into DOM.
   570     return [
   571       "command-button-pick",
   572       "command-button-splitconsole",
   573       "command-button-responsive",
   574       "command-button-paintflashing",
   575       "command-button-tilt",
   576       "command-button-scratchpad",
   577       "command-button-eyedropper"
   578     ].map(id => {
   579       let button = this.doc.getElementById(id);
   580       // Some buttons may not exist inside of Browser Toolbox
   581       if (!button) {
   582         return false;
   583       }
   584       return {
   585         id: id,
   586         button: button,
   587         label: button.getAttribute("tooltiptext"),
   588         visibilityswitch: "devtools." + id + ".enabled"
   589       }
   590     }).filter(button=>button);
   591   },
   593   /**
   594    * Ensure the visibility of each toolbox button matches the
   595    * preference value.  Simply hide buttons that are preffed off.
   596    */
   597   setToolboxButtonsVisibility: function() {
   598     this.toolboxButtons.forEach(buttonSpec => {
   599       let {visibilityswitch, id, button}=buttonSpec;
   600       let on = true;
   601       try {
   602         on = Services.prefs.getBoolPref(visibilityswitch);
   603       } catch (ex) { }
   605       if (button) {
   606         if (on) {
   607           button.removeAttribute("hidden");
   608         } else {
   609           button.setAttribute("hidden", "true");
   610         }
   611       }
   612     });
   613   },
   615   /**
   616    * Build a tab for one tool definition and add to the toolbox
   617    *
   618    * @param {string} toolDefinition
   619    *        Tool definition of the tool to build a tab for.
   620    */
   621   _buildTabForTool: function(toolDefinition) {
   622     if (!toolDefinition.isTargetSupported(this._target)) {
   623       return;
   624     }
   626     let tabs = this.doc.getElementById("toolbox-tabs");
   627     let deck = this.doc.getElementById("toolbox-deck");
   629     let id = toolDefinition.id;
   631     if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
   632       toolDefinition.ordinal = MAX_ORDINAL;
   633     }
   635     let radio = this.doc.createElement("radio");
   636     // The radio element is not being used in the conventional way, thus
   637     // the devtools-tab class replaces the radio XBL binding with its base
   638     // binding (the control-item binding).
   639     radio.className = "devtools-tab";
   640     radio.id = "toolbox-tab-" + id;
   641     radio.setAttribute("toolid", id);
   642     radio.setAttribute("ordinal", toolDefinition.ordinal);
   643     radio.setAttribute("tooltiptext", toolDefinition.tooltip);
   644     if (toolDefinition.invertIconForLightTheme) {
   645       radio.setAttribute("icon-invertable", "true");
   646     }
   648     radio.addEventListener("command", () => {
   649       this.selectTool(id);
   650     });
   652     // spacer lets us center the image and label, while allowing cropping
   653     let spacer = this.doc.createElement("spacer");
   654     spacer.setAttribute("flex", "1");
   655     radio.appendChild(spacer);
   657     if (toolDefinition.icon) {
   658       let image = this.doc.createElement("image");
   659       image.className = "default-icon";
   660       image.setAttribute("src",
   661                          toolDefinition.icon || toolDefinition.highlightedicon);
   662       radio.appendChild(image);
   663       // Adding the highlighted icon image
   664       image = this.doc.createElement("image");
   665       image.className = "highlighted-icon";
   666       image.setAttribute("src",
   667                          toolDefinition.highlightedicon || toolDefinition.icon);
   668       radio.appendChild(image);
   669     }
   671     if (toolDefinition.label) {
   672       let label = this.doc.createElement("label");
   673       label.setAttribute("value", toolDefinition.label)
   674       label.setAttribute("crop", "end");
   675       label.setAttribute("flex", "1");
   676       radio.appendChild(label);
   677       radio.setAttribute("flex", "1");
   678     }
   680     if (!toolDefinition.bgTheme) {
   681       toolDefinition.bgTheme = "theme-toolbar";
   682     }
   683     let vbox = this.doc.createElement("vbox");
   684     vbox.className = "toolbox-panel " + toolDefinition.bgTheme;
   686     // There is already a container for the webconsole frame.
   687     if (!this.doc.getElementById("toolbox-panel-" + id)) {
   688       vbox.id = "toolbox-panel-" + id;
   689     }
   691     // If there is no tab yet, or the ordinal to be added is the largest one.
   692     if (tabs.childNodes.length == 0 ||
   693         +tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) {
   694       tabs.appendChild(radio);
   695       deck.appendChild(vbox);
   696     } else {
   697       // else, iterate over all the tabs to get the correct location.
   698       Array.some(tabs.childNodes, (node, i) => {
   699         if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
   700           tabs.insertBefore(radio, node);
   701           deck.insertBefore(vbox, deck.childNodes[i]);
   702           return true;
   703         }
   704         return false;
   705       });
   706     }
   708     this._addKeysToWindow();
   709   },
   711   /**
   712    * Ensure the tool with the given id is loaded.
   713    *
   714    * @param {string} id
   715    *        The id of the tool to load.
   716    */
   717   loadTool: function(id) {
   718     if (id === "inspector" && !this._inspector) {
   719       return this.initInspector().then(() => {
   720         return this.loadTool(id);
   721       });
   722     }
   724     let deferred = promise.defer();
   725     let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
   727     if (iframe) {
   728       let panel = this._toolPanels.get(id);
   729       if (panel) {
   730         deferred.resolve(panel);
   731       } else {
   732         this.once(id + "-ready", panel => {
   733           deferred.resolve(panel);
   734         });
   735       }
   736       return deferred.promise;
   737     }
   739     let definition = gDevTools.getToolDefinition(id);
   740     if (!definition) {
   741       deferred.reject(new Error("no such tool id "+id));
   742       return deferred.promise;
   743     }
   745     iframe = this.doc.createElement("iframe");
   746     iframe.className = "toolbox-panel-iframe";
   747     iframe.id = "toolbox-panel-iframe-" + id;
   748     iframe.setAttribute("flex", 1);
   749     iframe.setAttribute("forceOwnRefreshDriver", "");
   750     iframe.tooltip = "aHTMLTooltip";
   751     iframe.style.visibility = "hidden";
   753     let vbox = this.doc.getElementById("toolbox-panel-" + id);
   754     vbox.appendChild(iframe);
   756     let onLoad = () => {
   757       // Prevent flicker while loading by waiting to make visible until now.
   758       iframe.style.visibility = "visible";
   760       let built = definition.build(iframe.contentWindow, this);
   761       promise.resolve(built).then((panel) => {
   762         this._toolPanels.set(id, panel);
   763         this.emit(id + "-ready", panel);
   764         gDevTools.emit(id + "-ready", this, panel);
   765         deferred.resolve(panel);
   766       }, console.error);
   767     };
   769     iframe.setAttribute("src", definition.url);
   771     // Depending on the host, iframe.contentWindow is not always
   772     // defined at this moment. If it is not defined, we use an
   773     // event listener on the iframe DOM node. If it's defined,
   774     // we use the chromeEventHandler. We can't use a listener
   775     // on the DOM node every time because this won't work
   776     // if the (xul chrome) iframe is loaded in a content docshell.
   777     if (iframe.contentWindow) {
   778       let domHelper = new DOMHelpers(iframe.contentWindow);
   779       domHelper.onceDOMReady(onLoad);
   780     } else {
   781       let callback = () => {
   782         iframe.removeEventListener("DOMContentLoaded", callback);
   783         onLoad();
   784       }
   785       iframe.addEventListener("DOMContentLoaded", callback);
   786     }
   788     return deferred.promise;
   789   },
   791   /**
   792    * Switch to the tool with the given id
   793    *
   794    * @param {string} id
   795    *        The id of the tool to switch to
   796    */
   797   selectTool: function(id) {
   798     let selected = this.doc.querySelector(".devtools-tab[selected]");
   799     if (selected) {
   800       selected.removeAttribute("selected");
   801     }
   803     let tab = this.doc.getElementById("toolbox-tab-" + id);
   804     tab.setAttribute("selected", "true");
   806     if (this.currentToolId == id) {
   807       // re-focus tool to get key events again
   808       this.focusTool(id);
   810       // Return the existing panel in order to have a consistent return value.
   811       return promise.resolve(this._toolPanels.get(id));
   812     }
   814     if (!this.isReady) {
   815       throw new Error("Can't select tool, wait for toolbox 'ready' event");
   816     }
   818     tab = this.doc.getElementById("toolbox-tab-" + id);
   820     if (tab) {
   821       if (this.currentToolId) {
   822         this._telemetry.toolClosed(this.currentToolId);
   823       }
   824       this._telemetry.toolOpened(id);
   825     } else {
   826       throw new Error("No tool found");
   827     }
   829     let tabstrip = this.doc.getElementById("toolbox-tabs");
   831     // select the right tab, making 0th index the default tab if right tab not
   832     // found
   833     let index = 0;
   834     let tabs = tabstrip.childNodes;
   835     for (let i = 0; i < tabs.length; i++) {
   836       if (tabs[i] === tab) {
   837         index = i;
   838         break;
   839       }
   840     }
   841     tabstrip.selectedItem = tab;
   843     // and select the right iframe
   844     let deck = this.doc.getElementById("toolbox-deck");
   845     deck.selectedIndex = index;
   847     this.currentToolId = id;
   848     this._refreshConsoleDisplay();
   849     if (id != "options") {
   850       Services.prefs.setCharPref(this._prefs.LAST_TOOL, id);
   851     }
   853     return this.loadTool(id).then(panel => {
   854       // focus the tool's frame to start receiving key events
   855       this.focusTool(id);
   857       this.emit("select", id);
   858       this.emit(id + "-selected", panel);
   859       return panel;
   860     });
   861   },
   863   /**
   864    * Focus a tool's panel by id
   865    * @param  {string} id
   866    *         The id of tool to focus
   867    */
   868   focusTool: function(id) {
   869     let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
   870     iframe.focus();
   871   },
   873   /**
   874    * Focus split console's input line
   875    */
   876   focusConsoleInput: function() {
   877     let hud = this.getPanel("webconsole").hud;
   878     if (hud && hud.jsterm) {
   879       hud.jsterm.inputNode.focus();
   880     }
   881   },
   883   /**
   884    * Toggles the split state of the webconsole.  If the webconsole panel
   885    * is already selected, then this command is ignored.
   886    */
   887   toggleSplitConsole: function() {
   888     let openedConsolePanel = this.currentToolId === "webconsole";
   890     // Don't allow changes when console is open, since it could be confusing
   891     if (!openedConsolePanel) {
   892       this._splitConsole = !this._splitConsole;
   893       this._refreshConsoleDisplay();
   894       this.emit("split-console");
   896       if (this._splitConsole) {
   897         this.loadTool("webconsole").then(() => {
   898           this.focusConsoleInput();
   899         });
   900       }
   901     }
   902   },
   904   /**
   905    * Loads the tool next to the currently selected tool.
   906    */
   907   selectNextTool: function() {
   908     let selected = this.doc.querySelector(".devtools-tab[selected]");
   909     let next = selected.nextSibling || selected.parentNode.firstChild;
   910     let tool = next.getAttribute("toolid");
   911     return this.selectTool(tool);
   912   },
   914   /**
   915    * Loads the tool just left to the currently selected tool.
   916    */
   917   selectPreviousTool: function() {
   918     let selected = this.doc.querySelector(".devtools-tab[selected]");
   919     let previous = selected.previousSibling || selected.parentNode.lastChild;
   920     let tool = previous.getAttribute("toolid");
   921     return this.selectTool(tool);
   922   },
   924   /**
   925    * Highlights the tool's tab if it is not the currently selected tool.
   926    *
   927    * @param {string} id
   928    *        The id of the tool to highlight
   929    */
   930   highlightTool: function(id) {
   931     let tab = this.doc.getElementById("toolbox-tab-" + id);
   932     tab && tab.setAttribute("highlighted", "true");
   933   },
   935   /**
   936    * De-highlights the tool's tab.
   937    *
   938    * @param {string} id
   939    *        The id of the tool to unhighlight
   940    */
   941   unhighlightTool: function(id) {
   942     let tab = this.doc.getElementById("toolbox-tab-" + id);
   943     tab && tab.removeAttribute("highlighted");
   944   },
   946   /**
   947    * Raise the toolbox host.
   948    */
   949   raise: function() {
   950     this._host.raise();
   951   },
   953   /**
   954    * Refresh the host's title.
   955    */
   956   _refreshHostTitle: function() {
   957     let toolName;
   958     let toolDef = gDevTools.getToolDefinition(this.currentToolId);
   959     if (toolDef) {
   960       toolName = toolDef.label;
   961     } else {
   962       // no tool is selected
   963       toolName = toolboxStrings("toolbox.defaultTitle");
   964     }
   965     let title = toolboxStrings("toolbox.titleTemplate",
   966                                toolName, this.target.url || this.target.name);
   967     this._host.setTitle(title);
   968   },
   970   /**
   971    * Create a host object based on the given host type.
   972    *
   973    * Warning: some hosts require that the toolbox target provides a reference to
   974    * the attached tab. Not all Targets have a tab property - make sure you correctly
   975    * mix and match hosts and targets.
   976    *
   977    * @param {string} hostType
   978    *        The host type of the new host object
   979    *
   980    * @return {Host} host
   981    *        The created host object
   982    */
   983   _createHost: function(hostType, options) {
   984     if (!Hosts[hostType]) {
   985       throw new Error("Unknown hostType: " + hostType);
   986     }
   988     // clean up the toolbox if its window is closed
   989     let newHost = new Hosts[hostType](this.target.tab, options);
   990     newHost.on("window-closed", this.destroy);
   991     return newHost;
   992   },
   994   /**
   995    * Switch to a new host for the toolbox UI. E.g.
   996    * bottom, sidebar, separate window.
   997    *
   998    * @param {string} hostType
   999    *        The host type of the new host object
  1000    */
  1001   switchHost: function(hostType) {
  1002     if (hostType == this._host.type || !this._target.isLocalTab) {
  1003       return null;
  1006     let newHost = this._createHost(hostType);
  1007     return newHost.create().then(iframe => {
  1008       // change toolbox document's parent to the new host
  1009       iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
  1010       iframe.swapFrameLoaders(this.frame);
  1012       this._host.off("window-closed", this.destroy);
  1013       this.destroyHost();
  1015       this._host = newHost;
  1017       if (this.hostType != Toolbox.HostType.CUSTOM) {
  1018         Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type);
  1021       this._buildDockButtons();
  1022       this._addKeysToWindow();
  1024       this.emit("host-changed");
  1025     });
  1026   },
  1028   /**
  1029    * Handler for the tool-registered event.
  1030    * @param  {string} event
  1031    *         Name of the event ("tool-registered")
  1032    * @param  {string} toolId
  1033    *         Id of the tool that was registered
  1034    */
  1035   _toolRegistered: function(event, toolId) {
  1036     let tool = gDevTools.getToolDefinition(toolId);
  1037     this._buildTabForTool(tool);
  1038   },
  1040   /**
  1041    * Handler for the tool-unregistered event.
  1042    * @param  {string} event
  1043    *         Name of the event ("tool-unregistered")
  1044    * @param  {string|object} toolId
  1045    *         Definition or id of the tool that was unregistered. Passing the
  1046    *         tool id should be avoided as it is a temporary measure.
  1047    */
  1048   _toolUnregistered: function(event, toolId) {
  1049     if (typeof toolId != "string") {
  1050       toolId = toolId.id;
  1053     if (this._toolPanels.has(toolId)) {
  1054       let instance = this._toolPanels.get(toolId);
  1055       instance.destroy();
  1056       this._toolPanels.delete(toolId);
  1059     let radio = this.doc.getElementById("toolbox-tab-" + toolId);
  1060     let panel = this.doc.getElementById("toolbox-panel-" + toolId);
  1062     if (radio) {
  1063       if (this.currentToolId == toolId) {
  1064         let nextToolName = null;
  1065         if (radio.nextSibling) {
  1066           nextToolName = radio.nextSibling.getAttribute("toolid");
  1068         if (radio.previousSibling) {
  1069           nextToolName = radio.previousSibling.getAttribute("toolid");
  1071         if (nextToolName) {
  1072           this.selectTool(nextToolName);
  1075       radio.parentNode.removeChild(radio);
  1078     if (panel) {
  1079       panel.parentNode.removeChild(panel);
  1082     if (this.hostType == Toolbox.HostType.WINDOW) {
  1083       let doc = this.doc.defaultView.parent.document;
  1084       let key = doc.getElementById("key_" + toolId);
  1085       if (key) {
  1086         key.parentNode.removeChild(key);
  1089   },
  1091   /**
  1092    * Initialize the inspector/walker/selection/highlighter fronts.
  1093    * Returns a promise that resolves when the fronts are initialized
  1094    */
  1095   initInspector: function() {
  1096     if (!this._initInspector) {
  1097       this._initInspector = Task.spawn(function*() {
  1098         this._inspector = InspectorFront(this._target.client, this._target.form);
  1099         this._walker = yield this._inspector.getWalker();
  1100         this._selection = new Selection(this._walker);
  1102         if (this.highlighterUtils.isRemoteHighlightable) {
  1103           let autohide = !gDevTools.testing;
  1105           this.walker.on("highlighter-ready", this._highlighterReady);
  1106           this.walker.on("highlighter-hide", this._highlighterHidden);
  1108           this._highlighter = yield this._inspector.getHighlighter(autohide);
  1110       }.bind(this));
  1112     return this._initInspector;
  1113   },
  1115   /**
  1116    * Destroy the inspector/walker/selection fronts
  1117    * Returns a promise that resolves when the fronts are destroyed
  1118    */
  1119   destroyInspector: function() {
  1120     if (this._destroying) {
  1121       return this._destroying;
  1124     if (!this._inspector) {
  1125       return promise.resolve();
  1128     let outstanding = () => {
  1129       return Task.spawn(function*() {
  1130         yield this.highlighterUtils.stopPicker();
  1131         yield this._inspector.destroy();
  1132         if (this._highlighter) {
  1133           yield this._highlighter.destroy();
  1135         if (this._selection) {
  1136           this._selection.destroy();
  1139         if (this.walker) {
  1140           this.walker.off("highlighter-ready", this._highlighterReady);
  1141           this.walker.off("highlighter-hide", this._highlighterHidden);
  1144         this._inspector = null;
  1145         this._highlighter = null;
  1146         this._selection = null;
  1147         this._walker = null;
  1148       }.bind(this));
  1149     };
  1151     // Releasing the walker (if it has been created)
  1152     // This can fail, but in any case, we want to continue destroying the
  1153     // inspector/highlighter/selection
  1154     let walker = (this._destroying = this._walker) ?
  1155                  this._walker.release() :
  1156                  promise.resolve();
  1157     return walker.then(outstanding, outstanding);
  1158   },
  1160   /**
  1161    * Get the toolbox's notification box
  1163    * @return The notification box element.
  1164    */
  1165   getNotificationBox: function() {
  1166     return this.doc.getElementById("toolbox-notificationbox");
  1167   },
  1169   /**
  1170    * Destroy the current host, and remove event listeners from its frame.
  1172    * @return {promise} to be resolved when the host is destroyed.
  1173    */
  1174   destroyHost: function() {
  1175     this.doc.removeEventListener("keypress",
  1176       this._splitConsoleOnKeypress, false);
  1177     return this._host.destroy();
  1178   },
  1180   /**
  1181    * Remove all UI elements, detach from target and clear up
  1182    */
  1183   destroy: function() {
  1184     // If several things call destroy then we give them all the same
  1185     // destruction promise so we're sure to destroy only once
  1186     if (this._destroyer) {
  1187       return this._destroyer;
  1190     this._target.off("navigate", this._refreshHostTitle);
  1191     this.off("select", this._refreshHostTitle);
  1192     this.off("host-changed", this._refreshHostTitle);
  1194     gDevTools.off("tool-registered", this._toolRegistered);
  1195     gDevTools.off("tool-unregistered", this._toolUnregistered);
  1197     let outstanding = [];
  1198     for (let [id, panel] of this._toolPanels) {
  1199       try {
  1200         outstanding.push(panel.destroy());
  1201       } catch (e) {
  1202         // We don't want to stop here if any panel fail to close.
  1203         console.error("Panel " + id + ":", e);
  1207     // Destroying the walker and inspector fronts
  1208     outstanding.push(this.destroyInspector());
  1209     // Removing buttons
  1210     outstanding.push(() => {
  1211       this._pickerButton.removeEventListener("command", this._togglePicker, false);
  1212       this._pickerButton = null;
  1213       let container = this.doc.getElementById("toolbox-buttons");
  1214       while (container.firstChild) {
  1215         container.removeChild(container.firstChild);
  1217     });
  1218     // Remove the host UI
  1219     outstanding.push(this.destroyHost());
  1221     if (this.target.isLocalTab) {
  1222       this._requisition.destroy();
  1224     this._telemetry.destroy();
  1226     return this._destroyer = promise.all(outstanding).then(() => {
  1227       // Targets need to be notified that the toolbox is being torn down.
  1228       // This is done after other destruction tasks since it may tear down
  1229       // fronts and the debugger transport which earlier destroy methods may
  1230       // require to complete.
  1231       if (!this._target) {
  1232         return null;
  1234       let target = this._target;
  1235       this._target = null;
  1236       target.off("close", this.destroy);
  1237       return target.destroy();
  1238     }).then(() => {
  1239       this.emit("destroyed");
  1240       // Free _host after the call to destroyed in order to let a chance
  1241       // to destroyed listeners to still query toolbox attributes
  1242       this._host = null;
  1243       this._toolPanels.clear();
  1244     }).then(null, console.error);
  1245   },
  1247   _highlighterReady: function() {
  1248     this.emit("highlighter-ready");
  1249   },
  1251   _highlighterHidden: function() {
  1252     this.emit("highlighter-hide");
  1253   },
  1254 };
  1256 /**
  1257  * The ToolboxHighlighterUtils is what you should use for anything related to
  1258  * node highlighting and picking.
  1259  * It encapsulates the logic to connecting to the HighlighterActor.
  1260  */
  1261 function ToolboxHighlighterUtils(toolbox) {
  1262   this.toolbox = toolbox;
  1263   this._onPickerNodeHovered = this._onPickerNodeHovered.bind(this);
  1264   this._onPickerNodePicked = this._onPickerNodePicked.bind(this);
  1265   this.stopPicker = this.stopPicker.bind(this);
  1268 ToolboxHighlighterUtils.prototype = {
  1269   /**
  1270    * Indicates whether the highlighter actor exists on the server.
  1271    */
  1272   get isRemoteHighlightable() {
  1273     return this.toolbox._target.client.traits.highlightable;
  1274   },
  1276   /**
  1277    * Start/stop the element picker on the debuggee target.
  1278    */
  1279   togglePicker: function() {
  1280     if (this._isPicking) {
  1281       return this.stopPicker();
  1282     } else {
  1283       return this.startPicker();
  1285   },
  1287   _onPickerNodeHovered: function(res) {
  1288     this.toolbox.emit("picker-node-hovered", res.node);
  1289   },
  1291   _onPickerNodePicked: function(res) {
  1292     this.toolbox.selection.setNodeFront(res.node, "picker-node-picked");
  1293     this.stopPicker();
  1294   },
  1296   /**
  1297    * Start the element picker on the debuggee target.
  1298    * This will request the inspector actor to start listening for mouse/touch
  1299    * events on the target to highlight the hovered/picked element.
  1300    * Depending on the server-side capabilities, this may fire events when nodes
  1301    * are hovered.
  1302    * @return A promise that resolves when the picker has started or immediately
  1303    * if it is already started
  1304    */
  1305   startPicker: function() {
  1306     if (this._isPicking) {
  1307       return promise.resolve();
  1310     let deferred = promise.defer();
  1312     let done = () => {
  1313       this._isPicking = true;
  1314       this.toolbox.emit("picker-started");
  1315       this.toolbox.on("select", this.stopPicker);
  1316       deferred.resolve();
  1317     };
  1319     promise.all([
  1320       this.toolbox.initInspector(),
  1321       this.toolbox.selectTool("inspector")
  1322     ]).then(() => {
  1323       this.toolbox._pickerButton.setAttribute("checked", "true");
  1325       if (this.isRemoteHighlightable) {
  1326         this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered);
  1327         this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked);
  1329         this.toolbox.highlighter.pick().then(done);
  1330       } else {
  1331         return this.toolbox.walker.pick().then(node => {
  1332           this.toolbox.selection.setNodeFront(node, "picker-node-picked").then(() => {
  1333             this.stopPicker();
  1334             done();
  1335           });
  1336         });
  1338     });
  1340     return deferred.promise;
  1341   },
  1343   /**
  1344    * Stop the element picker
  1345    * @return A promise that resolves when the picker has stopped or immediately
  1346    * if it is already stopped
  1347    */
  1348   stopPicker: function() {
  1349     if (!this._isPicking) {
  1350       return promise.resolve();
  1353     let deferred = promise.defer();
  1355     let done = () => {
  1356       this.toolbox.emit("picker-stopped");
  1357       this.toolbox.off("select", this.stopPicker);
  1358       deferred.resolve();
  1359     };
  1361     this.toolbox.initInspector().then(() => {
  1362       this._isPicking = false;
  1363       this.toolbox._pickerButton.removeAttribute("checked");
  1364       if (this.isRemoteHighlightable) {
  1365         this.toolbox.highlighter.cancelPick().then(done);
  1366         this.toolbox.walker.off("picker-node-hovered", this._onPickerNodeHovered);
  1367         this.toolbox.walker.off("picker-node-picked", this._onPickerNodePicked);
  1368       } else {
  1369         this.toolbox.walker.cancelPick().then(done);
  1371     });
  1373     return deferred.promise;
  1374   },
  1376   /**
  1377    * Show the box model highlighter on a node, given its NodeFront (this type
  1378    * of front is normally returned by the WalkerActor).
  1379    * @return a promise that resolves to the nodeFront when the node has been
  1380    * highlit
  1381    */
  1382   highlightNodeFront: function(nodeFront, options={}) {
  1383     let deferred = promise.defer();
  1385     // If the remote highlighter exists on the target, use it
  1386     if (this.isRemoteHighlightable) {
  1387       this.toolbox.initInspector().then(() => {
  1388         this.toolbox.highlighter.showBoxModel(nodeFront, options).then(() => {
  1389           this.toolbox.emit("node-highlight", nodeFront);
  1390           deferred.resolve(nodeFront);
  1391         });
  1392       });
  1394     // Else, revert to the "older" version of the highlighter in the walker
  1395     // actor
  1396     else {
  1397       this.toolbox.walker.highlight(nodeFront).then(() => {
  1398         this.toolbox.emit("node-highlight", nodeFront);
  1399         deferred.resolve(nodeFront);
  1400       });
  1403     return deferred.promise;
  1404   },
  1406   /**
  1407    * This is a convenience method in case you don't have a nodeFront but a
  1408    * valueGrip. This is often the case with VariablesView properties.
  1409    * This method will simply translate the grip into a nodeFront and call
  1410    * highlightNodeFront
  1411    * @return a promise that resolves to the nodeFront when the node has been
  1412    * highlit
  1413    */
  1414   highlightDomValueGrip: function(valueGrip, options={}) {
  1415     return this._translateGripToNodeFront(valueGrip).then(nodeFront => {
  1416       if (nodeFront) {
  1417         return this.highlightNodeFront(nodeFront, options);
  1418       } else {
  1419         return promise.reject();
  1421     });
  1422   },
  1424   _translateGripToNodeFront: function(grip) {
  1425     return this.toolbox.initInspector().then(() => {
  1426       return this.toolbox.walker.getNodeActorFromObjectActor(grip.actor);
  1427     });
  1428   },
  1430   /**
  1431    * Hide the highlighter.
  1432    * @return a promise that resolves when the highlighter is hidden
  1433    */
  1434   unhighlight: function(forceHide=false) {
  1435     let unhighlightPromise;
  1436     forceHide = forceHide || !gDevTools.testing;
  1438     if (forceHide && this.isRemoteHighlightable && this.toolbox.highlighter) {
  1439       // If the remote highlighter exists on the target, use it
  1440       unhighlightPromise = this.toolbox.highlighter.hideBoxModel();
  1441     } else {
  1442       // If not, no need to unhighlight as the older highlight method uses a
  1443       // setTimeout to hide itself
  1444       unhighlightPromise = promise.resolve();
  1447     return unhighlightPromise.then(() => {
  1448       this.toolbox.emit("node-unhighlight");
  1449     });
  1451 };

mercurial