michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ "DeveloperToolbar", "CommandUtils" ]; michael@0: michael@0: const NS_XHTML = "http://www.w3.org/1999/xhtml"; michael@0: const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; michael@0: const { classes: Cc, interfaces: Ci, utils: Cu } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: const { require, TargetFactory } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; michael@0: michael@0: const Node = Ci.nsIDOMNode; michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "console", michael@0: "resource://gre/modules/devtools/Console.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", michael@0: "resource://gre/modules/PluralForm.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", michael@0: "resource://gre/modules/devtools/event-emitter.js"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "prefBranch", function() { michael@0: let prefService = Cc["@mozilla.org/preferences-service;1"] michael@0: .getService(Ci.nsIPrefService); michael@0: return prefService.getBranch(null) michael@0: .QueryInterface(Ci.nsIPrefBranch2); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function () { michael@0: return Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties"); michael@0: }); michael@0: michael@0: const Telemetry = require("devtools/shared/telemetry"); michael@0: michael@0: // This lazy getter is needed to prevent a require loop michael@0: XPCOMUtils.defineLazyGetter(this, "gcli", () => { michael@0: let gcli = require("gcli/index"); michael@0: require("devtools/commandline/commands-index"); michael@0: gcli.load(); michael@0: return gcli; michael@0: }); michael@0: michael@0: Object.defineProperty(this, "ConsoleServiceListener", { michael@0: get: function() { michael@0: return require("devtools/toolkit/webconsole/utils").ConsoleServiceListener; michael@0: }, michael@0: configurable: true, michael@0: enumerable: true michael@0: }); michael@0: michael@0: const promise = Cu.import('resource://gre/modules/Promise.jsm', {}).Promise; michael@0: michael@0: /** michael@0: * A collection of utilities to help working with commands michael@0: */ michael@0: let CommandUtils = { michael@0: /** michael@0: * Utility to ensure that things are loaded in the correct order michael@0: */ michael@0: createRequisition: function(environment) { michael@0: let temp = gcli.createDisplay; // Ensure GCLI is loaded michael@0: let Requisition = require("gcli/cli").Requisition michael@0: return new Requisition({ environment: environment }); michael@0: }, michael@0: michael@0: /** michael@0: * Read a toolbarSpec from preferences michael@0: * @param pref The name of the preference to read michael@0: */ michael@0: getCommandbarSpec: function(pref) { michael@0: let value = prefBranch.getComplexValue(pref, Ci.nsISupportsString).data; michael@0: return JSON.parse(value); michael@0: }, michael@0: michael@0: /** michael@0: * A toolbarSpec is an array of buttonSpecs. A buttonSpec is an array of michael@0: * strings each of which is a GCLI command (including args if needed). michael@0: * michael@0: * Warning: this method uses the unload event of the window that owns the michael@0: * buttons that are of type checkbox. this means that we don't properly michael@0: * unregister event handlers until the window is destroyed. michael@0: */ michael@0: createButtons: function(toolbarSpec, target, document, requisition) { michael@0: let reply = []; michael@0: michael@0: toolbarSpec.forEach(function(buttonSpec) { michael@0: let button = document.createElement("toolbarbutton"); michael@0: reply.push(button); michael@0: michael@0: if (typeof buttonSpec == "string") { michael@0: buttonSpec = { typed: buttonSpec }; michael@0: } michael@0: // Ask GCLI to parse the typed string (doesn't execute it) michael@0: requisition.update(buttonSpec.typed); michael@0: michael@0: // Ignore invalid commands michael@0: let command = requisition.commandAssignment.value; michael@0: if (command == null) { michael@0: // TODO: Have a broken icon michael@0: // button.icon = 'Broken'; michael@0: button.setAttribute("label", "X"); michael@0: button.setAttribute("tooltip", "Unknown command: " + buttonSpec.typed); michael@0: button.setAttribute("disabled", "true"); michael@0: } michael@0: else { michael@0: if (command.buttonId != null) { michael@0: button.id = command.buttonId; michael@0: } michael@0: if (command.buttonClass != null) { michael@0: button.className = command.buttonClass; michael@0: } michael@0: if (command.tooltipText != null) { michael@0: button.setAttribute("tooltiptext", command.tooltipText); michael@0: } michael@0: else if (command.description != null) { michael@0: button.setAttribute("tooltiptext", command.description); michael@0: } michael@0: michael@0: button.addEventListener("click", function() { michael@0: requisition.update(buttonSpec.typed); michael@0: //if (requisition.getStatus() == Status.VALID) { michael@0: requisition.exec(); michael@0: /* michael@0: } michael@0: else { michael@0: console.error('incomplete commands not yet supported'); michael@0: } michael@0: */ michael@0: }, false); michael@0: michael@0: // Allow the command button to be toggleable michael@0: if (command.state) { michael@0: button.setAttribute("autocheck", false); michael@0: let onChange = function(event, eventTab) { michael@0: if (eventTab == target.tab) { michael@0: if (command.state.isChecked(target)) { michael@0: button.setAttribute("checked", true); michael@0: } michael@0: else if (button.hasAttribute("checked")) { michael@0: button.removeAttribute("checked"); michael@0: } michael@0: } michael@0: }; michael@0: command.state.onChange(target, onChange); michael@0: onChange(null, target.tab); michael@0: document.defaultView.addEventListener("unload", function() { michael@0: command.state.offChange(target, onChange); michael@0: }, false); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: requisition.update(''); michael@0: michael@0: return reply; michael@0: }, michael@0: michael@0: /** michael@0: * A helper function to create the environment object that is passed to michael@0: * GCLI commands. michael@0: * @param targetContainer An object containing a 'target' property which michael@0: * reflects the current debug target michael@0: */ michael@0: createEnvironment: function(container, targetProperty='target') { michael@0: if (container[targetProperty].supports == null) { michael@0: throw new Error('Missing target'); michael@0: } michael@0: michael@0: return { michael@0: get target() { michael@0: if (container[targetProperty].supports == null) { michael@0: throw new Error('Removed target'); michael@0: } michael@0: michael@0: return container[targetProperty]; michael@0: }, michael@0: michael@0: get chromeWindow() { michael@0: return this.target.tab.ownerDocument.defaultView; michael@0: }, michael@0: michael@0: get chromeDocument() { michael@0: return this.chromeWindow.document; michael@0: }, michael@0: michael@0: get window() { michael@0: return this.chromeWindow.getBrowser().selectedTab.linkedBrowser.contentWindow; michael@0: }, michael@0: michael@0: get document() { michael@0: return this.window.document; michael@0: } michael@0: }; michael@0: }, michael@0: }; michael@0: michael@0: this.CommandUtils = CommandUtils; michael@0: michael@0: /** michael@0: * Due to a number of panel bugs we need a way to check if we are running on michael@0: * Linux. See the comments for TooltipPanel and OutputPanel for further details. michael@0: * michael@0: * When bug 780102 is fixed all isLinux checks can be removed and we can revert michael@0: * to using panels. michael@0: */ michael@0: XPCOMUtils.defineLazyGetter(this, "isLinux", function() { michael@0: return OS == "Linux"; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "OS", function() { michael@0: let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; michael@0: return os; michael@0: }); michael@0: michael@0: /** michael@0: * A component to manage the global developer toolbar, which contains a GCLI michael@0: * and buttons for various developer tools. michael@0: * @param aChromeWindow The browser window to which this toolbar is attached michael@0: * @param aToolbarElement See browser.xul: michael@0: */ michael@0: this.DeveloperToolbar = function DeveloperToolbar(aChromeWindow, aToolbarElement) michael@0: { michael@0: this._chromeWindow = aChromeWindow; michael@0: michael@0: this._element = aToolbarElement; michael@0: this._element.hidden = true; michael@0: this._doc = this._element.ownerDocument; michael@0: michael@0: this._telemetry = new Telemetry(); michael@0: this._errorsCount = {}; michael@0: this._warningsCount = {}; michael@0: this._errorListeners = {}; michael@0: this._errorCounterButton = this._doc michael@0: .getElementById("developer-toolbar-toolbox-button"); michael@0: this._errorCounterButton._defaultTooltipText = michael@0: this._errorCounterButton.getAttribute("tooltiptext"); michael@0: michael@0: EventEmitter.decorate(this); michael@0: } michael@0: michael@0: /** michael@0: * Inspector notifications dispatched through the nsIObserverService michael@0: */ michael@0: const NOTIFICATIONS = { michael@0: /** DeveloperToolbar.show() has been called, and we're working on it */ michael@0: LOAD: "developer-toolbar-load", michael@0: michael@0: /** DeveloperToolbar.show() has completed */ michael@0: SHOW: "developer-toolbar-show", michael@0: michael@0: /** DeveloperToolbar.hide() has been called */ michael@0: HIDE: "developer-toolbar-hide" michael@0: }; michael@0: michael@0: /** michael@0: * Attach notification constants to the object prototype so tests etc can michael@0: * use them without needing to import anything michael@0: */ michael@0: DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS; michael@0: michael@0: Object.defineProperty(DeveloperToolbar.prototype, "target", { michael@0: get: function() { michael@0: return TargetFactory.forTab(this._chromeWindow.getBrowser().selectedTab); michael@0: }, michael@0: enumerable: true michael@0: }); michael@0: michael@0: /** michael@0: * Is the toolbar open? michael@0: */ michael@0: Object.defineProperty(DeveloperToolbar.prototype, 'visible', { michael@0: get: function DT_visible() { michael@0: return !this._element.hidden; michael@0: }, michael@0: enumerable: true michael@0: }); michael@0: michael@0: let _gSequenceId = 0; michael@0: michael@0: /** michael@0: * Getter for a unique ID. michael@0: */ michael@0: Object.defineProperty(DeveloperToolbar.prototype, 'sequenceId', { michael@0: get: function DT_visible() { michael@0: return _gSequenceId++; michael@0: }, michael@0: enumerable: true michael@0: }); michael@0: michael@0: /** michael@0: * Called from browser.xul in response to menu-click or keyboard shortcut to michael@0: * toggle the toolbar michael@0: */ michael@0: DeveloperToolbar.prototype.toggle = function() { michael@0: if (this.visible) { michael@0: return this.hide(); michael@0: } else { michael@0: return this.show(true); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Called from browser.xul in response to menu-click or keyboard shortcut to michael@0: * toggle the toolbar michael@0: */ michael@0: DeveloperToolbar.prototype.focus = function() { michael@0: if (this.visible) { michael@0: this._input.focus(); michael@0: return promise.resolve(); michael@0: } else { michael@0: return this.show(true); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Called from browser.xul in response to menu-click or keyboard shortcut to michael@0: * toggle the toolbar michael@0: */ michael@0: DeveloperToolbar.prototype.focusToggle = function() { michael@0: if (this.visible) { michael@0: // If we have focus then the active element is the HTML input contained michael@0: // inside the xul input element michael@0: let active = this._chromeWindow.document.activeElement; michael@0: let position = this._input.compareDocumentPosition(active); michael@0: if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) { michael@0: this.hide(); michael@0: } michael@0: else { michael@0: this._input.focus(); michael@0: } michael@0: } else { michael@0: this.show(true); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Even if the user has not clicked on 'Got it' in the intro, we only show it michael@0: * once per session. michael@0: * Warning this is slightly messed up because this.DeveloperToolbar is not the michael@0: * same as this.DeveloperToolbar when in browser.js context. michael@0: */ michael@0: DeveloperToolbar.introShownThisSession = false; michael@0: michael@0: /** michael@0: * Show the developer toolbar michael@0: */ michael@0: DeveloperToolbar.prototype.show = function(focus) { michael@0: if (this._showPromise != null) { michael@0: return this._showPromise; michael@0: } michael@0: michael@0: // hide() is async, so ensure we don't need to wait for hide() to finish michael@0: var waitPromise = this._hidePromise || promise.resolve(); michael@0: michael@0: this._showPromise = waitPromise.then(() => { michael@0: Services.prefs.setBoolPref("devtools.toolbar.visible", true); michael@0: michael@0: this._telemetry.toolOpened("developertoolbar"); michael@0: michael@0: this._notify(NOTIFICATIONS.LOAD); michael@0: michael@0: this._input = this._doc.querySelector(".gclitoolbar-input-node"); michael@0: michael@0: // Initializing GCLI can only be done when we've got content windows to michael@0: // write to, so this needs to be done asynchronously. michael@0: let panelPromises = [ michael@0: TooltipPanel.create(this), michael@0: OutputPanel.create(this) michael@0: ]; michael@0: return promise.all(panelPromises).then(panels => { michael@0: [ this.tooltipPanel, this.outputPanel ] = panels; michael@0: michael@0: this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "true"); michael@0: michael@0: this.display = gcli.createDisplay({ michael@0: contentDocument: this._chromeWindow.getBrowser().contentDocument, michael@0: chromeDocument: this._doc, michael@0: chromeWindow: this._chromeWindow, michael@0: hintElement: this.tooltipPanel.hintElement, michael@0: inputElement: this._input, michael@0: completeElement: this._doc.querySelector(".gclitoolbar-complete-node"), michael@0: backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"), michael@0: outputDocument: this.outputPanel.document, michael@0: environment: CommandUtils.createEnvironment(this, "target"), michael@0: tooltipClass: "gcliterm-tooltip", michael@0: eval: null, michael@0: scratchpad: null michael@0: }); michael@0: michael@0: this.display.focusManager.addMonitoredElement(this.outputPanel._frame); michael@0: this.display.focusManager.addMonitoredElement(this._element); michael@0: michael@0: this.display.onVisibilityChange.add(this.outputPanel._visibilityChanged, michael@0: this.outputPanel); michael@0: this.display.onVisibilityChange.add(this.tooltipPanel._visibilityChanged, michael@0: this.tooltipPanel); michael@0: this.display.onOutput.add(this.outputPanel._outputChanged, this.outputPanel); michael@0: michael@0: let tabbrowser = this._chromeWindow.getBrowser(); michael@0: tabbrowser.tabContainer.addEventListener("TabSelect", this, false); michael@0: tabbrowser.tabContainer.addEventListener("TabClose", this, false); michael@0: tabbrowser.addEventListener("load", this, true); michael@0: tabbrowser.addEventListener("beforeunload", this, true); michael@0: michael@0: this._initErrorsCount(tabbrowser.selectedTab); michael@0: this._devtoolsUnloaded = this._devtoolsUnloaded.bind(this); michael@0: this._devtoolsLoaded = this._devtoolsLoaded.bind(this); michael@0: Services.obs.addObserver(this._devtoolsUnloaded, "devtools-unloaded", false); michael@0: Services.obs.addObserver(this._devtoolsLoaded, "devtools-loaded", false); michael@0: michael@0: this._element.hidden = false; michael@0: michael@0: if (focus) { michael@0: this._input.focus(); michael@0: } michael@0: michael@0: this._notify(NOTIFICATIONS.SHOW); michael@0: michael@0: if (!DeveloperToolbar.introShownThisSession) { michael@0: this.display.maybeShowIntro(); michael@0: DeveloperToolbar.introShownThisSession = true; michael@0: } michael@0: michael@0: this._showPromise = null; michael@0: }); michael@0: }); michael@0: michael@0: return this._showPromise; michael@0: }; michael@0: michael@0: /** michael@0: * Hide the developer toolbar. michael@0: */ michael@0: DeveloperToolbar.prototype.hide = function() { michael@0: // If we're already in the process of hiding, just use the other promise michael@0: if (this._hidePromise != null) { michael@0: return this._hidePromise; michael@0: } michael@0: michael@0: // show() is async, so ensure we don't need to wait for show() to finish michael@0: var waitPromise = this._showPromise || promise.resolve(); michael@0: michael@0: this._hidePromise = waitPromise.then(() => { michael@0: this._element.hidden = true; michael@0: michael@0: Services.prefs.setBoolPref("devtools.toolbar.visible", false); michael@0: michael@0: this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "false"); michael@0: this.destroy(); michael@0: michael@0: this._telemetry.toolClosed("developertoolbar"); michael@0: this._notify(NOTIFICATIONS.HIDE); michael@0: michael@0: this._hidePromise = null; michael@0: }); michael@0: michael@0: return this._hidePromise; michael@0: }; michael@0: michael@0: /** michael@0: * The devtools-unloaded event handler. michael@0: * @private michael@0: */ michael@0: DeveloperToolbar.prototype._devtoolsUnloaded = function() { michael@0: let tabbrowser = this._chromeWindow.getBrowser(); michael@0: Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this); michael@0: }; michael@0: michael@0: /** michael@0: * The devtools-loaded event handler. michael@0: * @private michael@0: */ michael@0: DeveloperToolbar.prototype._devtoolsLoaded = function() { michael@0: let tabbrowser = this._chromeWindow.getBrowser(); michael@0: this._initErrorsCount(tabbrowser.selectedTab); michael@0: }; michael@0: michael@0: /** michael@0: * Initialize the listeners needed for tracking the number of errors for a given michael@0: * tab. michael@0: * michael@0: * @private michael@0: * @param nsIDOMNode tab the xul:tab for which you want to track the number of michael@0: * errors. michael@0: */ michael@0: DeveloperToolbar.prototype._initErrorsCount = function(tab) { michael@0: let tabId = tab.linkedPanel; michael@0: if (tabId in this._errorsCount) { michael@0: this._updateErrorsCount(); michael@0: return; michael@0: } michael@0: michael@0: let window = tab.linkedBrowser.contentWindow; michael@0: let listener = new ConsoleServiceListener(window, { michael@0: onConsoleServiceMessage: this._onPageError.bind(this, tabId), michael@0: }); michael@0: listener.init(); michael@0: michael@0: this._errorListeners[tabId] = listener; michael@0: this._errorsCount[tabId] = 0; michael@0: this._warningsCount[tabId] = 0; michael@0: michael@0: let messages = listener.getCachedMessages(); michael@0: messages.forEach(this._onPageError.bind(this, tabId)); michael@0: michael@0: this._updateErrorsCount(); michael@0: }; michael@0: michael@0: /** michael@0: * Stop the listeners needed for tracking the number of errors for a given michael@0: * tab. michael@0: * michael@0: * @private michael@0: * @param nsIDOMNode tab the xul:tab for which you want to stop tracking the michael@0: * number of errors. michael@0: */ michael@0: DeveloperToolbar.prototype._stopErrorsCount = function(tab) { michael@0: let tabId = tab.linkedPanel; michael@0: if (!(tabId in this._errorsCount) || !(tabId in this._warningsCount)) { michael@0: this._updateErrorsCount(); michael@0: return; michael@0: } michael@0: michael@0: this._errorListeners[tabId].destroy(); michael@0: delete this._errorListeners[tabId]; michael@0: delete this._errorsCount[tabId]; michael@0: delete this._warningsCount[tabId]; michael@0: michael@0: this._updateErrorsCount(); michael@0: }; michael@0: michael@0: /** michael@0: * Hide the developer toolbar michael@0: */ michael@0: DeveloperToolbar.prototype.destroy = function() { michael@0: if (this._input == null) { michael@0: return; // Already destroyed michael@0: } michael@0: michael@0: let tabbrowser = this._chromeWindow.getBrowser(); michael@0: tabbrowser.tabContainer.removeEventListener("TabSelect", this, false); michael@0: tabbrowser.tabContainer.removeEventListener("TabClose", this, false); michael@0: tabbrowser.removeEventListener("load", this, true); michael@0: tabbrowser.removeEventListener("beforeunload", this, true); michael@0: michael@0: Services.obs.removeObserver(this._devtoolsUnloaded, "devtools-unloaded"); michael@0: Services.obs.removeObserver(this._devtoolsLoaded, "devtools-loaded"); michael@0: Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this); michael@0: michael@0: this.display.focusManager.removeMonitoredElement(this.outputPanel._frame); michael@0: this.display.focusManager.removeMonitoredElement(this._element); michael@0: michael@0: this.display.onVisibilityChange.remove(this.outputPanel._visibilityChanged, this.outputPanel); michael@0: this.display.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged, this.tooltipPanel); michael@0: this.display.onOutput.remove(this.outputPanel._outputChanged, this.outputPanel); michael@0: this.display.destroy(); michael@0: this.outputPanel.destroy(); michael@0: this.tooltipPanel.destroy(); michael@0: delete this._input; michael@0: michael@0: // We could "delete this.display" etc if we have hard-to-track-down memory michael@0: // leaks as a belt-and-braces approach, however this prevents our DOM node michael@0: // hunter from looking in all the nooks and crannies, so it's better if we michael@0: // can be leak-free without michael@0: /* michael@0: delete this.display; michael@0: delete this.outputPanel; michael@0: delete this.tooltipPanel; michael@0: */ michael@0: }; michael@0: michael@0: /** michael@0: * Utility for sending notifications michael@0: * @param topic a NOTIFICATION constant michael@0: */ michael@0: DeveloperToolbar.prototype._notify = function(topic) { michael@0: let data = { toolbar: this }; michael@0: data.wrappedJSObject = data; michael@0: Services.obs.notifyObservers(data, topic, null); michael@0: }; michael@0: michael@0: /** michael@0: * Update various parts of the UI when the current tab changes michael@0: */ michael@0: DeveloperToolbar.prototype.handleEvent = function(ev) { michael@0: if (ev.type == "TabSelect" || ev.type == "load") { michael@0: if (this.visible) { michael@0: this.display.reattach({ michael@0: contentDocument: this._chromeWindow.getBrowser().contentDocument michael@0: }); michael@0: michael@0: if (ev.type == "TabSelect") { michael@0: this._initErrorsCount(ev.target); michael@0: } michael@0: } michael@0: } michael@0: else if (ev.type == "TabClose") { michael@0: this._stopErrorsCount(ev.target); michael@0: } michael@0: else if (ev.type == "beforeunload") { michael@0: this._onPageBeforeUnload(ev); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Count a page error received for the currently selected tab. This michael@0: * method counts the JavaScript exceptions received and CSS errors/warnings. michael@0: * michael@0: * @private michael@0: * @param string tabId the ID of the tab from where the page error comes. michael@0: * @param object pageError the page error object received from the michael@0: * PageErrorListener. michael@0: */ michael@0: DeveloperToolbar.prototype._onPageError = function(tabId, pageError) { michael@0: if (pageError.category == "CSS Parser" || michael@0: pageError.category == "CSS Loader") { michael@0: return; michael@0: } michael@0: if ((pageError.flags & pageError.warningFlag) || michael@0: (pageError.flags & pageError.strictFlag)) { michael@0: this._warningsCount[tabId]++; michael@0: } else { michael@0: this._errorsCount[tabId]++; michael@0: } michael@0: this._updateErrorsCount(tabId); michael@0: }; michael@0: michael@0: /** michael@0: * The |beforeunload| event handler. This function resets the errors count when michael@0: * a different page starts loading. michael@0: * michael@0: * @private michael@0: * @param nsIDOMEvent ev the beforeunload DOM event. michael@0: */ michael@0: DeveloperToolbar.prototype._onPageBeforeUnload = function(ev) { michael@0: let window = ev.target.defaultView; michael@0: if (window.top !== window) { michael@0: return; michael@0: } michael@0: michael@0: let tabs = this._chromeWindow.getBrowser().tabs; michael@0: Array.prototype.some.call(tabs, function(tab) { michael@0: if (tab.linkedBrowser.contentWindow === window) { michael@0: let tabId = tab.linkedPanel; michael@0: if (tabId in this._errorsCount || tabId in this._warningsCount) { michael@0: this._errorsCount[tabId] = 0; michael@0: this._warningsCount[tabId] = 0; michael@0: this._updateErrorsCount(tabId); michael@0: } michael@0: return true; michael@0: } michael@0: return false; michael@0: }, this); michael@0: }; michael@0: michael@0: /** michael@0: * Update the page errors count displayed in the Web Console button for the michael@0: * currently selected tab. michael@0: * michael@0: * @private michael@0: * @param string [changedTabId] Optional. The tab ID that had its page errors michael@0: * count changed. If this is provided and it doesn't match the currently michael@0: * selected tab, then the button is not updated. michael@0: */ michael@0: DeveloperToolbar.prototype._updateErrorsCount = function(changedTabId) { michael@0: let tabId = this._chromeWindow.getBrowser().selectedTab.linkedPanel; michael@0: if (changedTabId && tabId != changedTabId) { michael@0: return; michael@0: } michael@0: michael@0: let errors = this._errorsCount[tabId]; michael@0: let warnings = this._warningsCount[tabId]; michael@0: let btn = this._errorCounterButton; michael@0: if (errors) { michael@0: let errorsText = toolboxStrings michael@0: .GetStringFromName("toolboxToggleButton.errors"); michael@0: errorsText = PluralForm.get(errors, errorsText).replace("#1", errors); michael@0: michael@0: let warningsText = toolboxStrings michael@0: .GetStringFromName("toolboxToggleButton.warnings"); michael@0: warningsText = PluralForm.get(warnings, warningsText).replace("#1", warnings); michael@0: michael@0: let tooltiptext = toolboxStrings michael@0: .formatStringFromName("toolboxToggleButton.tooltip", michael@0: [errorsText, warningsText], 2); michael@0: michael@0: btn.setAttribute("error-count", errors); michael@0: btn.setAttribute("tooltiptext", tooltiptext); michael@0: } else { michael@0: btn.removeAttribute("error-count"); michael@0: btn.setAttribute("tooltiptext", btn._defaultTooltipText); michael@0: } michael@0: michael@0: this.emit("errors-counter-updated"); michael@0: }; michael@0: michael@0: /** michael@0: * Reset the errors counter for the given tab. michael@0: * michael@0: * @param nsIDOMElement tab The xul:tab for which you want to reset the page michael@0: * errors counters. michael@0: */ michael@0: DeveloperToolbar.prototype.resetErrorsCount = function(tab) { michael@0: let tabId = tab.linkedPanel; michael@0: if (tabId in this._errorsCount || tabId in this._warningsCount) { michael@0: this._errorsCount[tabId] = 0; michael@0: this._warningsCount[tabId] = 0; michael@0: this._updateErrorsCount(tabId); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Creating a OutputPanel is asynchronous michael@0: */ michael@0: function OutputPanel() { michael@0: throw new Error('Use OutputPanel.create()'); michael@0: } michael@0: michael@0: /** michael@0: * Panel to handle command line output. michael@0: * michael@0: * There is a tooltip bug on Windows and OSX that prevents tooltips from being michael@0: * positioned properly (bug 786975). There is a Gnome panel bug on Linux that michael@0: * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848). michael@0: * We now use a tooltip on Linux and a panel on OSX & Windows. michael@0: * michael@0: * If a panel has no content and no height it is not shown when openPopup is michael@0: * called on Windows and OSX (bug 692348) ... this prevents the panel from michael@0: * appearing the first time it is shown. Setting the panel's height to 1px michael@0: * before calling openPopup works around this issue as we resize it ourselves michael@0: * anyway. michael@0: * michael@0: * @param devtoolbar The parent DeveloperToolbar object michael@0: */ michael@0: OutputPanel.create = function(devtoolbar) { michael@0: var outputPanel = Object.create(OutputPanel.prototype); michael@0: return outputPanel._init(devtoolbar); michael@0: }; michael@0: michael@0: /** michael@0: * @private See OutputPanel.create michael@0: */ michael@0: OutputPanel.prototype._init = function(devtoolbar) { michael@0: this._devtoolbar = devtoolbar; michael@0: this._input = this._devtoolbar._input; michael@0: this._toolbar = this._devtoolbar._doc.getElementById("developer-toolbar"); michael@0: michael@0: /* michael@0: michael@0: michael@0: michael@0: */ michael@0: michael@0: // TODO: Switch back from tooltip to panel when metacity focus issue is fixed: michael@0: // https://bugzilla.mozilla.org/show_bug.cgi?id=780102 michael@0: this._panel = this._devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel"); michael@0: michael@0: this._panel.id = "gcli-output"; michael@0: this._panel.classList.add("gcli-panel"); michael@0: michael@0: if (isLinux) { michael@0: this.canHide = false; michael@0: this._onpopuphiding = this._onpopuphiding.bind(this); michael@0: this._panel.addEventListener("popuphiding", this._onpopuphiding, true); michael@0: } else { michael@0: this._panel.setAttribute("noautofocus", "true"); michael@0: this._panel.setAttribute("noautohide", "true"); michael@0: michael@0: // Bug 692348: On Windows and OSX if a panel has no content and no height michael@0: // openPopup fails to display it. Setting the height to 1px alows the panel michael@0: // to be displayed before has content or a real height i.e. the first time michael@0: // it is displayed. michael@0: this._panel.setAttribute("height", "1px"); michael@0: } michael@0: michael@0: this._toolbar.parentElement.insertBefore(this._panel, this._toolbar); michael@0: michael@0: this._frame = this._devtoolbar._doc.createElementNS(NS_XHTML, "iframe"); michael@0: this._frame.id = "gcli-output-frame"; michael@0: this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlineoutput.xhtml"); michael@0: this._frame.setAttribute("sandbox", "allow-same-origin"); michael@0: this._panel.appendChild(this._frame); michael@0: michael@0: this.displayedOutput = undefined; michael@0: michael@0: this._update = this._update.bind(this); michael@0: michael@0: // Wire up the element from the iframe, and resolve the promise michael@0: let deferred = promise.defer(); michael@0: let onload = () => { michael@0: this._frame.removeEventListener("load", onload, true); michael@0: michael@0: this.document = this._frame.contentDocument; michael@0: michael@0: this._div = this.document.getElementById("gcli-output-root"); michael@0: this._div.classList.add('gcli-row-out'); michael@0: this._div.setAttribute('aria-live', 'assertive'); michael@0: michael@0: let styles = this._toolbar.ownerDocument.defaultView michael@0: .getComputedStyle(this._toolbar); michael@0: this._div.setAttribute("dir", styles.direction); michael@0: michael@0: deferred.resolve(this); michael@0: }; michael@0: this._frame.addEventListener("load", onload, true); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Prevent the popup from hiding if it is not permitted via this.canHide. michael@0: */ michael@0: OutputPanel.prototype._onpopuphiding = function(ev) { michael@0: // TODO: When we switch back from tooltip to panel we can remove this hack: michael@0: // https://bugzilla.mozilla.org/show_bug.cgi?id=780102 michael@0: if (isLinux && !this.canHide) { michael@0: ev.preventDefault(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Display the OutputPanel. michael@0: */ michael@0: OutputPanel.prototype.show = function() { michael@0: if (isLinux) { michael@0: this.canHide = false; michael@0: } michael@0: michael@0: // We need to reset the iframe size in order for future size calculations to michael@0: // be correct michael@0: this._frame.style.minHeight = this._frame.style.maxHeight = 0; michael@0: this._frame.style.minWidth = 0; michael@0: michael@0: this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null); michael@0: this._resize(); michael@0: michael@0: this._input.focus(); michael@0: }; michael@0: michael@0: /** michael@0: * Internal helper to set the height of the output panel to fit the available michael@0: * content; michael@0: */ michael@0: OutputPanel.prototype._resize = function() { michael@0: if (this._panel == null || this.document == null || !this._panel.state == "closed") { michael@0: return michael@0: } michael@0: michael@0: // Set max panel width to match any content with a max of the width of the michael@0: // browser window. michael@0: let maxWidth = this._panel.ownerDocument.documentElement.clientWidth; michael@0: michael@0: // Adjust max width according to OS. michael@0: // We'd like to put this in CSS but we can't: michael@0: // body { width: calc(min(-5px, max-content)); } michael@0: // #_panel { max-width: -5px; } michael@0: switch(OS) { michael@0: case "Linux": michael@0: maxWidth -= 5; michael@0: break; michael@0: case "Darwin": michael@0: maxWidth -= 25; michael@0: break; michael@0: case "WINNT": michael@0: maxWidth -= 5; michael@0: break; michael@0: } michael@0: michael@0: this.document.body.style.width = "-moz-max-content"; michael@0: let style = this._frame.contentWindow.getComputedStyle(this.document.body); michael@0: let frameWidth = parseInt(style.width, 10); michael@0: let width = Math.min(maxWidth, frameWidth); michael@0: this.document.body.style.width = width + "px"; michael@0: michael@0: // Set the width of the iframe. michael@0: this._frame.style.minWidth = width + "px"; michael@0: this._panel.style.maxWidth = maxWidth + "px"; michael@0: michael@0: // browserAdjustment is used to correct the panel height according to the michael@0: // browsers borders etc. michael@0: const browserAdjustment = 15; michael@0: michael@0: // Set max panel height to match any content with a max of the height of the michael@0: // browser window. michael@0: let maxHeight = michael@0: this._panel.ownerDocument.documentElement.clientHeight - browserAdjustment; michael@0: let height = Math.min(maxHeight, this.document.documentElement.scrollHeight); michael@0: michael@0: // Set the height of the iframe. Setting iframe.height does not work. michael@0: this._frame.style.minHeight = this._frame.style.maxHeight = height + "px"; michael@0: michael@0: // Set the height and width of the panel to match the iframe. michael@0: this._panel.sizeTo(width, height); michael@0: michael@0: // Move the panel to the correct position in the case that it has been michael@0: // positioned incorrectly. michael@0: let screenX = this._input.boxObject.screenX; michael@0: let screenY = this._toolbar.boxObject.screenY; michael@0: this._panel.moveTo(screenX, screenY - height); michael@0: }; michael@0: michael@0: /** michael@0: * Called by GCLI when a command is executed. michael@0: */ michael@0: OutputPanel.prototype._outputChanged = function(ev) { michael@0: if (ev.output.hidden) { michael@0: return; michael@0: } michael@0: michael@0: this.remove(); michael@0: michael@0: this.displayedOutput = ev.output; michael@0: michael@0: if (this.displayedOutput.completed) { michael@0: this._update(); michael@0: } michael@0: else { michael@0: this.displayedOutput.promise.then(this._update, this._update) michael@0: .then(null, console.error); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Called when displayed Output says it's changed or from outputChanged, which michael@0: * happens when there is a new displayed Output. michael@0: */ michael@0: OutputPanel.prototype._update = function() { michael@0: // destroy has been called, bail out michael@0: if (this._div == null) { michael@0: return; michael@0: } michael@0: michael@0: // Empty this._div michael@0: while (this._div.hasChildNodes()) { michael@0: this._div.removeChild(this._div.firstChild); michael@0: } michael@0: michael@0: if (this.displayedOutput.data != null) { michael@0: let context = this._devtoolbar.display.requisition.conversionContext; michael@0: this.displayedOutput.convert('dom', context).then((node) => { michael@0: while (this._div.hasChildNodes()) { michael@0: this._div.removeChild(this._div.firstChild); michael@0: } michael@0: michael@0: var links = node.ownerDocument.querySelectorAll('*[href]'); michael@0: for (var i = 0; i < links.length; i++) { michael@0: links[i].setAttribute('target', '_blank'); michael@0: } michael@0: michael@0: this._div.appendChild(node); michael@0: this.show(); michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Detach listeners from the currently displayed Output. michael@0: */ michael@0: OutputPanel.prototype.remove = function() { michael@0: if (isLinux) { michael@0: this.canHide = true; michael@0: } michael@0: michael@0: if (this._panel && this._panel.hidePopup) { michael@0: this._panel.hidePopup(); michael@0: } michael@0: michael@0: if (this.displayedOutput) { michael@0: delete this.displayedOutput; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Detach listeners from the currently displayed Output. michael@0: */ michael@0: OutputPanel.prototype.destroy = function() { michael@0: this.remove(); michael@0: michael@0: this._panel.removeEventListener("popuphiding", this._onpopuphiding, true); michael@0: michael@0: this._panel.removeChild(this._frame); michael@0: this._toolbar.parentElement.removeChild(this._panel); michael@0: michael@0: delete this._devtoolbar; michael@0: delete this._input; michael@0: delete this._toolbar; michael@0: delete this._onpopuphiding; michael@0: delete this._panel; michael@0: delete this._frame; michael@0: delete this._content; michael@0: delete this._div; michael@0: delete this.document; michael@0: }; michael@0: michael@0: /** michael@0: * Called by GCLI to indicate that we should show or hide one either the michael@0: * tooltip panel or the output panel. michael@0: */ michael@0: OutputPanel.prototype._visibilityChanged = function(ev) { michael@0: if (ev.outputVisible === true) { michael@0: // this.show is called by _outputChanged michael@0: } else { michael@0: if (isLinux) { michael@0: this.canHide = true; michael@0: } michael@0: this._panel.hidePopup(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Creating a TooltipPanel is asynchronous michael@0: */ michael@0: function TooltipPanel() { michael@0: throw new Error('Use TooltipPanel.create()'); michael@0: } michael@0: michael@0: /** michael@0: * Panel to handle tooltips. michael@0: * michael@0: * There is a tooltip bug on Windows and OSX that prevents tooltips from being michael@0: * positioned properly (bug 786975). There is a Gnome panel bug on Linux that michael@0: * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848). michael@0: * We now use a tooltip on Linux and a panel on OSX & Windows. michael@0: * michael@0: * If a panel has no content and no height it is not shown when openPopup is michael@0: * called on Windows and OSX (bug 692348) ... this prevents the panel from michael@0: * appearing the first time it is shown. Setting the panel's height to 1px michael@0: * before calling openPopup works around this issue as we resize it ourselves michael@0: * anyway. michael@0: * michael@0: * @param devtoolbar The parent DeveloperToolbar object michael@0: */ michael@0: TooltipPanel.create = function(devtoolbar) { michael@0: var tooltipPanel = Object.create(TooltipPanel.prototype); michael@0: return tooltipPanel._init(devtoolbar); michael@0: }; michael@0: michael@0: /** michael@0: * @private See TooltipPanel.create michael@0: */ michael@0: TooltipPanel.prototype._init = function(devtoolbar) { michael@0: let deferred = promise.defer(); michael@0: michael@0: let chromeDocument = devtoolbar._doc; michael@0: this._input = devtoolbar._doc.querySelector(".gclitoolbar-input-node"); michael@0: this._toolbar = devtoolbar._doc.querySelector("#developer-toolbar"); michael@0: this._dimensions = { start: 0, end: 0 }; michael@0: michael@0: /* michael@0: michael@0: michael@0: michael@0: */ michael@0: michael@0: // TODO: Switch back from tooltip to panel when metacity focus issue is fixed: michael@0: // https://bugzilla.mozilla.org/show_bug.cgi?id=780102 michael@0: this._panel = devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel"); michael@0: michael@0: this._panel.id = "gcli-tooltip"; michael@0: this._panel.classList.add("gcli-panel"); michael@0: michael@0: if (isLinux) { michael@0: this.canHide = false; michael@0: this._onpopuphiding = this._onpopuphiding.bind(this); michael@0: this._panel.addEventListener("popuphiding", this._onpopuphiding, true); michael@0: } else { michael@0: this._panel.setAttribute("noautofocus", "true"); michael@0: this._panel.setAttribute("noautohide", "true"); michael@0: michael@0: // Bug 692348: On Windows and OSX if a panel has no content and no height michael@0: // openPopup fails to display it. Setting the height to 1px alows the panel michael@0: // to be displayed before has content or a real height i.e. the first time michael@0: // it is displayed. michael@0: this._panel.setAttribute("height", "1px"); michael@0: } michael@0: michael@0: this._toolbar.parentElement.insertBefore(this._panel, this._toolbar); michael@0: michael@0: this._frame = devtoolbar._doc.createElementNS(NS_XHTML, "iframe"); michael@0: this._frame.id = "gcli-tooltip-frame"; michael@0: this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlinetooltip.xhtml"); michael@0: this._frame.setAttribute("flex", "1"); michael@0: this._frame.setAttribute("sandbox", "allow-same-origin"); michael@0: this._panel.appendChild(this._frame); michael@0: michael@0: /** michael@0: * Wire up the element from the iframe, and resolve the promise. michael@0: */ michael@0: let onload = () => { michael@0: this._frame.removeEventListener("load", onload, true); michael@0: michael@0: this.document = this._frame.contentDocument; michael@0: this.hintElement = this.document.getElementById("gcli-tooltip-root"); michael@0: this._connector = this.document.getElementById("gcli-tooltip-connector"); michael@0: michael@0: let styles = this._toolbar.ownerDocument.defaultView michael@0: .getComputedStyle(this._toolbar); michael@0: this.hintElement.setAttribute("dir", styles.direction); michael@0: michael@0: deferred.resolve(this); michael@0: }; michael@0: this._frame.addEventListener("load", onload, true); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Prevent the popup from hiding if it is not permitted via this.canHide. michael@0: */ michael@0: TooltipPanel.prototype._onpopuphiding = function(ev) { michael@0: // TODO: When we switch back from tooltip to panel we can remove this hack: michael@0: // https://bugzilla.mozilla.org/show_bug.cgi?id=780102 michael@0: if (isLinux && !this.canHide) { michael@0: ev.preventDefault(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Display the TooltipPanel. michael@0: */ michael@0: TooltipPanel.prototype.show = function(dimensions) { michael@0: if (!dimensions) { michael@0: dimensions = { start: 0, end: 0 }; michael@0: } michael@0: this._dimensions = dimensions; michael@0: michael@0: // This is nasty, but displaying the panel causes it to re-flow, which can michael@0: // change the size it should be, so we need to resize the iframe after the michael@0: // panel has displayed michael@0: this._panel.ownerDocument.defaultView.setTimeout(() => { michael@0: this._resize(); michael@0: }, 0); michael@0: michael@0: if (isLinux) { michael@0: this.canHide = false; michael@0: } michael@0: michael@0: this._resize(); michael@0: this._panel.openPopup(this._input, "before_start", dimensions.start * 10, 0, michael@0: false, false, null); michael@0: this._input.focus(); michael@0: }; michael@0: michael@0: /** michael@0: * One option is to spend lots of time taking an average width of characters michael@0: * in the current font, dynamically, and weighting for the frequency of use of michael@0: * various characters, or even to render the given string off screen, and then michael@0: * measure the width. michael@0: * Or we could do this... michael@0: */ michael@0: const AVE_CHAR_WIDTH = 4.5; michael@0: michael@0: /** michael@0: * Display the TooltipPanel. michael@0: */ michael@0: TooltipPanel.prototype._resize = function() { michael@0: if (this._panel == null || this.document == null || !this._panel.state == "closed") { michael@0: return michael@0: } michael@0: michael@0: let offset = 10 + Math.floor(this._dimensions.start * AVE_CHAR_WIDTH); michael@0: this._panel.style.marginLeft = offset + "px"; michael@0: michael@0: /* michael@0: // Bug 744906: UX review - Not sure if we want this code to fatten connector michael@0: // with param width michael@0: let width = Math.floor(this._dimensions.end * AVE_CHAR_WIDTH); michael@0: width = Math.min(width, 100); michael@0: width = Math.max(width, 10); michael@0: this._connector.style.width = width + "px"; michael@0: */ michael@0: michael@0: this._frame.height = this.document.body.scrollHeight; michael@0: }; michael@0: michael@0: /** michael@0: * Hide the TooltipPanel. michael@0: */ michael@0: TooltipPanel.prototype.remove = function() { michael@0: if (isLinux) { michael@0: this.canHide = true; michael@0: } michael@0: if (this._panel && this._panel.hidePopup) { michael@0: this._panel.hidePopup(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Hide the TooltipPanel. michael@0: */ michael@0: TooltipPanel.prototype.destroy = function() { michael@0: this.remove(); michael@0: michael@0: this._panel.removeEventListener("popuphiding", this._onpopuphiding, true); michael@0: michael@0: this._panel.removeChild(this._frame); michael@0: this._toolbar.parentElement.removeChild(this._panel); michael@0: michael@0: delete this._connector; michael@0: delete this._dimensions; michael@0: delete this._input; michael@0: delete this._onpopuphiding; michael@0: delete this._panel; michael@0: delete this._frame; michael@0: delete this._toolbar; michael@0: delete this._content; michael@0: delete this.document; michael@0: delete this.hintElement; michael@0: }; michael@0: michael@0: /** michael@0: * Called by GCLI to indicate that we should show or hide one either the michael@0: * tooltip panel or the output panel. michael@0: */ michael@0: TooltipPanel.prototype._visibilityChanged = function(ev) { michael@0: if (ev.tooltipVisible === true) { michael@0: this.show(ev.dimensions); michael@0: } else { michael@0: if (isLinux) { michael@0: this.canHide = true; michael@0: } michael@0: this._panel.hidePopup(); michael@0: } michael@0: };