browser/devtools/shared/DeveloperToolbar.jsm

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

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

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

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

mercurial