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