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.

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

mercurial