summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/developer-toolbar.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/developer-toolbar.js')
-rw-r--r--devtools/client/shared/developer-toolbar.js1397
1 files changed, 1397 insertions, 0 deletions
diff --git a/devtools/client/shared/developer-toolbar.js b/devtools/client/shared/developer-toolbar.js
new file mode 100644
index 000000000..2528591a6
--- /dev/null
+++ b/devtools/client/shared/developer-toolbar.js
@@ -0,0 +1,1397 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Ci } = require("chrome");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const Services = require("Services");
+const { TargetFactory } = require("devtools/client/framework/target");
+const Telemetry = require("devtools/client/shared/telemetry");
+const {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+const {Task} = require("devtools/shared/task");
+
+const NS_XHTML = "http://www.w3.org/1999/xhtml";
+
+const { PluralForm } = require("devtools/shared/plural-form");
+
+loader.lazyGetter(this, "prefBranch", function () {
+ return Services.prefs.getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+});
+
+loader.lazyRequireGetter(this, "gcliInit", "devtools/shared/gcli/commands/index");
+loader.lazyRequireGetter(this, "util", "gcli/util/util");
+loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/utils/webconsole-utils", true);
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
+loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+
+/**
+ * A collection of utilities to help working with commands
+ */
+var CommandUtils = {
+ /**
+ * Utility to ensure that things are loaded in the correct order
+ */
+ createRequisition: function (target, options) {
+ if (!gcliInit) {
+ return promise.reject("Unable to load gcli");
+ }
+ return gcliInit.getSystem(target).then(system => {
+ let Requisition = require("gcli/cli").Requisition;
+ return new Requisition(system, options);
+ });
+ },
+
+ /**
+ * Destroy the remote side of the requisition as well as the local side
+ */
+ destroyRequisition: function (requisition, target) {
+ requisition.destroy();
+ gcliInit.releaseSystem(target);
+ },
+
+ /**
+ * Read a toolbarSpec from preferences
+ * @param pref The name of the preference to read
+ */
+ getCommandbarSpec: function (pref) {
+ let value = prefBranch.getComplexValue(pref, Ci.nsISupportsString).data;
+ return JSON.parse(value);
+ },
+
+ /**
+ * A toolbarSpec is an array of strings each of which is a GCLI command.
+ *
+ * Warning: this method uses the unload event of the window that owns the
+ * buttons that are of type checkbox. this means that we don't properly
+ * unregister event handlers until the window is destroyed.
+ */
+ createButtons: function (toolbarSpec, target, document, requisition) {
+ return util.promiseEach(toolbarSpec, typed => {
+ // Ask GCLI to parse the typed string (doesn't execute it)
+ return requisition.update(typed).then(() => {
+ let button = document.createElementNS(NS_XHTML, "button");
+
+ // Ignore invalid commands
+ let command = requisition.commandAssignment.value;
+ if (command == null) {
+ throw new Error("No command '" + typed + "'");
+ }
+
+ if (command.buttonId != null) {
+ button.id = command.buttonId;
+ if (command.buttonClass != null) {
+ button.className = command.buttonClass;
+ }
+ } else {
+ button.setAttribute("text-as-image", "true");
+ button.setAttribute("label", command.name);
+ }
+
+ button.classList.add("devtools-button");
+
+ if (command.tooltipText != null) {
+ button.setAttribute("title", command.tooltipText);
+ } else if (command.description != null) {
+ button.setAttribute("title", command.description);
+ }
+
+ button.addEventListener("click",
+ requisition.updateExec.bind(requisition, typed));
+
+ button.addEventListener("keypress", (event) => {
+ if (ViewHelpers.isSpaceOrReturn(event)) {
+ event.preventDefault();
+ requisition.updateExec(typed);
+ }
+ }, false);
+
+ // Allow the command button to be toggleable
+ let onChange = null;
+ if (command.state) {
+ button.setAttribute("autocheck", false);
+
+ /**
+ * The onChange event should be called with an event object that
+ * contains a target property which specifies which target the event
+ * applies to. For legacy reasons the event object can also contain
+ * a tab property.
+ */
+ onChange = (eventName, ev) => {
+ if (ev.target == target || ev.tab == target.tab) {
+ let updateChecked = (checked) => {
+ if (checked) {
+ button.setAttribute("checked", true);
+ } else if (button.hasAttribute("checked")) {
+ button.removeAttribute("checked");
+ }
+ };
+
+ // isChecked would normally be synchronous. An annoying quirk
+ // of the 'csscoverage toggle' command forces us to accept a
+ // promise here, but doing Promise.resolve(reply).then(...) here
+ // makes this async for everyone, which breaks some tests so we
+ // treat non-promise replies separately to keep then synchronous.
+ let reply = command.state.isChecked(target);
+ if (typeof reply.then == "function") {
+ reply.then(updateChecked, console.error);
+ } else {
+ updateChecked(reply);
+ }
+ }
+ };
+
+ command.state.onChange(target, onChange);
+ onChange("", { target: target });
+ }
+ document.defaultView.addEventListener("unload", function (event) {
+ if (onChange && command.state.offChange) {
+ command.state.offChange(target, onChange);
+ }
+ button.remove();
+ button = null;
+ }, { once: true });
+
+ requisition.clear();
+
+ return button;
+ });
+ });
+ },
+
+ /**
+ * A helper function to create the environment object that is passed to
+ * GCLI commands.
+ * @param targetContainer An object containing a 'target' property which
+ * reflects the current debug target
+ */
+ createEnvironment: function (container, targetProperty = "target") {
+ if (!container[targetProperty].toString ||
+ !/TabTarget/.test(container[targetProperty].toString())) {
+ throw new Error("Missing target");
+ }
+
+ return {
+ get target() {
+ if (!container[targetProperty].toString ||
+ !/TabTarget/.test(container[targetProperty].toString())) {
+ throw new Error("Removed target");
+ }
+
+ return container[targetProperty];
+ },
+
+ get chromeWindow() {
+ return this.target.tab.ownerDocument.defaultView;
+ },
+
+ get chromeDocument() {
+ return this.target.tab.ownerDocument.defaultView.document;
+ },
+
+ get window() {
+ // throw new
+ // Error("environment.window is not available in runAt:client commands");
+ return this.chromeWindow.gBrowser.contentWindowAsCPOW;
+ },
+
+ get document() {
+ // throw new
+ // Error("environment.document is not available in runAt:client commands");
+ return this.chromeWindow.gBrowser.contentDocumentAsCPOW;
+ }
+ };
+ },
+};
+
+exports.CommandUtils = CommandUtils;
+
+/**
+ * Due to a number of panel bugs we need a way to check if we are running on
+ * Linux. See the comments for TooltipPanel and OutputPanel for further details.
+ *
+ * When bug 780102 is fixed all isLinux checks can be removed and we can revert
+ * to using panels.
+ */
+loader.lazyGetter(this, "isLinux", function () {
+ return Services.appinfo.OS == "Linux";
+});
+loader.lazyGetter(this, "isMac", function () {
+ return Services.appinfo.OS == "Darwin";
+});
+
+/**
+ * A component to manage the global developer toolbar, which contains a GCLI
+ * and buttons for various developer tools.
+ * @param chromeWindow The browser window to which this toolbar is attached
+ */
+function DeveloperToolbar(chromeWindow) {
+ this._chromeWindow = chromeWindow;
+
+ // Will be setup when show() is called
+ this.target = null;
+
+ this._doc = chromeWindow.document;
+
+ this._telemetry = new Telemetry();
+ this._errorsCount = {};
+ this._warningsCount = {};
+ this._errorListeners = {};
+
+ this._onToolboxReady = this._onToolboxReady.bind(this);
+ this._onToolboxDestroyed = this._onToolboxDestroyed.bind(this);
+
+ EventEmitter.decorate(this);
+}
+exports.DeveloperToolbar = DeveloperToolbar;
+
+/**
+ * Inspector notifications dispatched through the nsIObserverService
+ */
+const NOTIFICATIONS = {
+ /** DeveloperToolbar.show() has been called, and we're working on it */
+ LOAD: "developer-toolbar-load",
+
+ /** DeveloperToolbar.show() has completed */
+ SHOW: "developer-toolbar-show",
+
+ /** DeveloperToolbar.hide() has been called */
+ HIDE: "developer-toolbar-hide"
+};
+
+/**
+ * Attach notification constants to the object prototype so tests etc can
+ * use them without needing to import anything
+ */
+DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS;
+
+/**
+ * Is the toolbar open?
+ */
+Object.defineProperty(DeveloperToolbar.prototype, "visible", {
+ get: function () {
+ return this._element && !this._element.hidden;
+ },
+ enumerable: true
+});
+
+var _gSequenceId = 0;
+
+/**
+ * Getter for a unique ID.
+ */
+Object.defineProperty(DeveloperToolbar.prototype, "sequenceId", {
+ get: function () {
+ return _gSequenceId++;
+ },
+ enumerable: true
+});
+
+/**
+ * Create the <toolbar> element to insert within browser UI
+ */
+DeveloperToolbar.prototype.createToolbar = function () {
+ if (this._element) {
+ return;
+ }
+ let toolbar = this._doc.createElement("toolbar");
+ toolbar.setAttribute("id", "developer-toolbar");
+ toolbar.setAttribute("hidden", "true");
+
+ let close = this._doc.createElement("toolbarbutton");
+ close.setAttribute("id", "developer-toolbar-closebutton");
+ close.setAttribute("class", "close-icon");
+ close.setAttribute("oncommand", "DeveloperToolbar.hide();");
+ let closeTooltip = L10N.getStr("toolbar.closeButton.tooltip");
+ close.setAttribute("tooltiptext", closeTooltip);
+
+ let stack = this._doc.createElement("stack");
+ stack.setAttribute("flex", "1");
+
+ let input = this._doc.createElement("textbox");
+ input.setAttribute("class", "gclitoolbar-input-node");
+ input.setAttribute("rows", "1");
+ stack.appendChild(input);
+
+ let hbox = this._doc.createElement("hbox");
+ hbox.setAttribute("class", "gclitoolbar-complete-node");
+ stack.appendChild(hbox);
+
+ let toolboxBtn = this._doc.createElement("toolbarbutton");
+ toolboxBtn.setAttribute("id", "developer-toolbar-toolbox-button");
+ toolboxBtn.setAttribute("class", "developer-toolbar-button");
+ let toolboxTooltip = L10N.getStr("toolbar.toolsButton.tooltip");
+ toolboxBtn.setAttribute("tooltiptext", toolboxTooltip);
+ let toolboxOpen = gDevToolsBrowser.hasToolboxOpened(this._chromeWindow);
+ toolboxBtn.setAttribute("checked", toolboxOpen);
+ toolboxBtn.addEventListener("command", function (event) {
+ let window = event.target.ownerDocument.defaultView;
+ gDevToolsBrowser.toggleToolboxCommand(window.gBrowser);
+ });
+ this._errorCounterButton = toolboxBtn;
+ this._errorCounterButton._defaultTooltipText = toolboxTooltip;
+
+ // On Mac, the close button is on the left,
+ // while it is on the right on every other platforms.
+ if (isMac) {
+ toolbar.appendChild(close);
+ toolbar.appendChild(stack);
+ toolbar.appendChild(toolboxBtn);
+ } else {
+ toolbar.appendChild(stack);
+ toolbar.appendChild(toolboxBtn);
+ toolbar.appendChild(close);
+ }
+
+ this._element = toolbar;
+ let bottomBox = this._doc.getElementById("browser-bottombox");
+ if (bottomBox) {
+ bottomBox.appendChild(this._element);
+ } else {
+ // SeaMonkey does not have a "browser-bottombox".
+ let statusBar = this._doc.getElementById("status-bar");
+ if (statusBar) {
+ statusBar.parentNode.insertBefore(this._element, statusBar);
+ }
+ }
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.toggle = function () {
+ if (this.visible) {
+ return this.hide().catch(console.error);
+ }
+ return this.show(true).catch(console.error);
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.focus = function () {
+ if (this.visible) {
+ this._input.focus();
+ return promise.resolve();
+ }
+ return this.show(true);
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.focusToggle = function () {
+ if (this.visible) {
+ // If we have focus then the active element is the HTML input contained
+ // inside the xul input element
+ let active = this._chromeWindow.document.activeElement;
+ let position = this._input.compareDocumentPosition(active);
+ if (position & nodeConstants.DOCUMENT_POSITION_CONTAINED_BY) {
+ this.hide();
+ } else {
+ this._input.focus();
+ }
+ } else {
+ this.show(true);
+ }
+};
+
+/**
+ * Even if the user has not clicked on 'Got it' in the intro, we only show it
+ * once per session.
+ * Warning this is slightly messed up because this.DeveloperToolbar is not the
+ * same as this.DeveloperToolbar when in browser.js context.
+ */
+DeveloperToolbar.introShownThisSession = false;
+
+/**
+ * Show the developer toolbar
+ */
+DeveloperToolbar.prototype.show = function (focus) {
+ if (this._showPromise != null) {
+ return this._showPromise;
+ }
+
+ this._showPromise = Task.spawn((function* () {
+ // hide() is async, so ensure we don't need to wait for hide() to
+ // finish. We unconditionally yield here, even if _hidePromise is
+ // null, so that the spawn call returns a promise before starting
+ // to do any real work.
+ yield this._hidePromise;
+
+ this.createToolbar();
+
+ Services.prefs.setBoolPref("devtools.toolbar.visible", true);
+
+ this._telemetry.toolOpened("developertoolbar");
+
+ this._notify(NOTIFICATIONS.LOAD);
+
+ this._input = this._doc.querySelector(".gclitoolbar-input-node");
+
+ // Initializing GCLI can only be done when we've got content windows to
+ // write to, so this needs to be done asynchronously.
+ let panelPromises = [
+ TooltipPanel.create(this),
+ OutputPanel.create(this)
+ ];
+ let panels = yield promise.all(panelPromises);
+
+ [ this.tooltipPanel, this.outputPanel ] = panels;
+
+ this._doc.getElementById("menu_devToolbar").setAttribute("checked", "true");
+
+ this.target = TargetFactory.forTab(this._chromeWindow.gBrowser.selectedTab);
+ const options = {
+ environment: CommandUtils.createEnvironment(this, "target"),
+ document: this.outputPanel.document,
+ };
+ let requisition = yield CommandUtils.createRequisition(this.target, options);
+ this.requisition = requisition;
+
+ // The <textbox> `value` may still be undefined on the XUL binding if
+ // we fetch it early
+ let value = this._input.value || "";
+ yield this.requisition.update(value);
+
+ const Inputter = require("gcli/mozui/inputter").Inputter;
+ const Completer = require("gcli/mozui/completer").Completer;
+ const Tooltip = require("gcli/mozui/tooltip").Tooltip;
+ const FocusManager = require("gcli/ui/focus").FocusManager;
+
+ this.onOutput = this.requisition.commandOutputManager.onOutput;
+
+ this.focusManager = new FocusManager(this._doc, requisition.system.settings);
+
+ this.inputter = new Inputter({
+ requisition: this.requisition,
+ focusManager: this.focusManager,
+ element: this._input,
+ });
+
+ this.completer = new Completer({
+ requisition: this.requisition,
+ inputter: this.inputter,
+ backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"),
+ element: this._doc.querySelector(".gclitoolbar-complete-node"),
+ });
+
+ this.tooltip = new Tooltip({
+ requisition: this.requisition,
+ focusManager: this.focusManager,
+ inputter: this.inputter,
+ element: this.tooltipPanel.hintElement,
+ });
+
+ this.inputter.tooltip = this.tooltip;
+
+ this.focusManager.addMonitoredElement(this.outputPanel._frame);
+ this.focusManager.addMonitoredElement(this._element);
+
+ this.focusManager.onVisibilityChange.add(this.outputPanel._visibilityChanged,
+ this.outputPanel);
+ this.focusManager.onVisibilityChange.add(this.tooltipPanel._visibilityChanged,
+ this.tooltipPanel);
+ this.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
+
+ let tabbrowser = this._chromeWindow.gBrowser;
+ tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
+ tabbrowser.tabContainer.addEventListener("TabClose", this, false);
+ tabbrowser.addEventListener("load", this, true);
+ tabbrowser.addEventListener("beforeunload", this, true);
+
+ gDevTools.on("toolbox-ready", this._onToolboxReady);
+ gDevTools.on("toolbox-destroyed", this._onToolboxDestroyed);
+
+ this._initErrorsCount(tabbrowser.selectedTab);
+
+ this._element.hidden = false;
+
+ if (focus) {
+ // If the toolbar was just inserted, the <textbox> may still have
+ // its binding in process of being applied and not be focusable yet
+ let waitForBinding = () => {
+ // Bail out if the toolbar has been destroyed in the meantime
+ if (!this._input) {
+ return;
+ }
+ // mInputField is a xbl field of <xul:textbox>
+ if (typeof this._input.mInputField != "undefined") {
+ this._input.focus();
+ this._notify(NOTIFICATIONS.SHOW);
+ } else {
+ this._input.ownerDocument.defaultView.setTimeout(waitForBinding, 50);
+ }
+ };
+ waitForBinding();
+ } else {
+ this._notify(NOTIFICATIONS.SHOW);
+ }
+
+ if (!DeveloperToolbar.introShownThisSession) {
+ let intro = require("gcli/ui/intro");
+ intro.maybeShowIntro(this.requisition.commandOutputManager,
+ this.requisition.conversionContext,
+ this.outputPanel);
+ DeveloperToolbar.introShownThisSession = true;
+ }
+
+ this._showPromise = null;
+ }).bind(this));
+
+ return this._showPromise;
+};
+
+/**
+ * Hide the developer toolbar.
+ */
+DeveloperToolbar.prototype.hide = function () {
+ // If we're already in the process of hiding, just use the other promise
+ if (this._hidePromise != null) {
+ return this._hidePromise;
+ }
+
+ // show() is async, so ensure we don't need to wait for show() to finish
+ let waitPromise = this._showPromise || promise.resolve();
+
+ this._hidePromise = waitPromise.then(() => {
+ this._element.hidden = true;
+
+ Services.prefs.setBoolPref("devtools.toolbar.visible", false);
+
+ this._doc.getElementById("menu_devToolbar").setAttribute("checked", "false");
+ this.destroy();
+
+ this._telemetry.toolClosed("developertoolbar");
+ this._notify(NOTIFICATIONS.HIDE);
+
+ this._hidePromise = null;
+ });
+
+ return this._hidePromise;
+};
+
+/**
+ * Initialize the listeners needed for tracking the number of errors for a given
+ * tab.
+ *
+ * @private
+ * @param nsIDOMNode tab the xul:tab for which you want to track the number of
+ * errors.
+ */
+DeveloperToolbar.prototype._initErrorsCount = function (tab) {
+ let tabId = tab.linkedPanel;
+ if (tabId in this._errorsCount) {
+ this._updateErrorsCount();
+ return;
+ }
+
+ let window = tab.linkedBrowser.contentWindow;
+ let listener = new ConsoleServiceListener(window, {
+ onConsoleServiceMessage: this._onPageError.bind(this, tabId),
+ });
+ listener.init();
+
+ this._errorListeners[tabId] = listener;
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+
+ let messages = listener.getCachedMessages();
+ messages.forEach(this._onPageError.bind(this, tabId));
+
+ this._updateErrorsCount();
+};
+
+/**
+ * Stop the listeners needed for tracking the number of errors for a given
+ * tab.
+ *
+ * @private
+ * @param nsIDOMNode tab the xul:tab for which you want to stop tracking the
+ * number of errors.
+ */
+DeveloperToolbar.prototype._stopErrorsCount = function (tab) {
+ let tabId = tab.linkedPanel;
+ if (!(tabId in this._errorsCount) || !(tabId in this._warningsCount)) {
+ this._updateErrorsCount();
+ return;
+ }
+
+ this._errorListeners[tabId].destroy();
+ delete this._errorListeners[tabId];
+ delete this._errorsCount[tabId];
+ delete this._warningsCount[tabId];
+
+ this._updateErrorsCount();
+};
+
+/**
+ * Hide the developer toolbar
+ */
+DeveloperToolbar.prototype.destroy = function () {
+ if (this._input == null) {
+ // Already destroyed
+ return;
+ }
+
+ let tabbrowser = this._chromeWindow.gBrowser;
+ tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
+ tabbrowser.tabContainer.removeEventListener("TabClose", this, false);
+ tabbrowser.removeEventListener("load", this, true);
+ tabbrowser.removeEventListener("beforeunload", this, true);
+
+ gDevTools.off("toolbox-ready", this._onToolboxReady);
+ gDevTools.off("toolbox-destroyed", this._onToolboxDestroyed);
+
+ Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
+
+ this.focusManager.removeMonitoredElement(this.outputPanel._frame);
+ this.focusManager.removeMonitoredElement(this._element);
+
+ this.focusManager.onVisibilityChange.remove(this.outputPanel._visibilityChanged,
+ this.outputPanel);
+ this.focusManager.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged,
+ this.tooltipPanel);
+ this.onOutput.remove(this.outputPanel._outputChanged, this.outputPanel);
+
+ this.tooltip.destroy();
+ this.completer.destroy();
+ this.inputter.destroy();
+ this.focusManager.destroy();
+
+ this.outputPanel.destroy();
+ this.tooltipPanel.destroy();
+ delete this._input;
+
+ CommandUtils.destroyRequisition(this.requisition, this.target);
+ this.target = undefined;
+
+ this._element.remove();
+ delete this._element;
+};
+
+/**
+ * Utility for sending notifications
+ * @param topic a NOTIFICATION constant
+ */
+DeveloperToolbar.prototype._notify = function (topic) {
+ let data = { toolbar: this };
+ data.wrappedJSObject = data;
+ Services.obs.notifyObservers(data, topic, null);
+};
+
+/**
+ * Update various parts of the UI when the current tab changes
+ */
+DeveloperToolbar.prototype.handleEvent = function (ev) {
+ if (ev.type == "TabSelect" || ev.type == "load") {
+ if (this.visible) {
+ let tab = this._chromeWindow.gBrowser.selectedTab;
+ this.target = TargetFactory.forTab(tab);
+ gcliInit.getSystem(this.target).then(system => {
+ this.requisition.system = system;
+ }, error => {
+ if (!this._chromeWindow.gBrowser.getBrowserForTab(tab)) {
+ // The tab was closed, suppress the error and print a warning as the
+ // destroyed tab was likely the cause.
+ console.warn("An error occurred as the tab was closed while " +
+ "updating Developer Toolbar state. The error was: ", error);
+ return;
+ }
+
+ // Propagate other errors as they're more likely to cause real issues
+ // and thus should cause tests to fail.
+ throw error;
+ });
+
+ if (ev.type == "TabSelect") {
+ let toolboxOpen = gDevToolsBrowser.hasToolboxOpened(this._chromeWindow);
+ this._errorCounterButton.setAttribute("checked", toolboxOpen);
+ this._initErrorsCount(ev.target);
+ }
+ }
+ } else if (ev.type == "TabClose") {
+ this._stopErrorsCount(ev.target);
+ } else if (ev.type == "beforeunload") {
+ this._onPageBeforeUnload(ev);
+ }
+};
+
+/**
+ * Update toolbox toggle button when toolbox goes on and off
+ */
+DeveloperToolbar.prototype._onToolboxReady = function () {
+ this._errorCounterButton.setAttribute("checked", "true");
+};
+DeveloperToolbar.prototype._onToolboxDestroyed = function () {
+ this._errorCounterButton.setAttribute("checked", "false");
+};
+
+/**
+ * Count a page error received for the currently selected tab. This
+ * method counts the JavaScript exceptions received and CSS errors/warnings.
+ *
+ * @private
+ * @param string tabId the ID of the tab from where the page error comes.
+ * @param object pageError the page error object received from the
+ * PageErrorListener.
+ */
+DeveloperToolbar.prototype._onPageError = function (tabId, pageError) {
+ if (pageError.category == "CSS Parser" ||
+ pageError.category == "CSS Loader") {
+ return;
+ }
+ if ((pageError.flags & pageError.warningFlag) ||
+ (pageError.flags & pageError.strictFlag)) {
+ this._warningsCount[tabId]++;
+ } else {
+ this._errorsCount[tabId]++;
+ }
+ this._updateErrorsCount(tabId);
+};
+
+/**
+ * The |beforeunload| event handler. This function resets the errors count when
+ * a different page starts loading.
+ *
+ * @private
+ * @param nsIDOMEvent ev the beforeunload DOM event.
+ */
+DeveloperToolbar.prototype._onPageBeforeUnload = function (ev) {
+ let window = ev.target.defaultView;
+ if (window.top !== window) {
+ return;
+ }
+
+ let tabs = this._chromeWindow.gBrowser.tabs;
+ Array.prototype.some.call(tabs, function (tab) {
+ if (tab.linkedBrowser.contentWindow === window) {
+ let tabId = tab.linkedPanel;
+ if (tabId in this._errorsCount || tabId in this._warningsCount) {
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+ this._updateErrorsCount(tabId);
+ }
+ return true;
+ }
+ return false;
+ }, this);
+};
+
+/**
+ * Update the page errors count displayed in the Web Console button for the
+ * currently selected tab.
+ *
+ * @private
+ * @param string [changedTabId] Optional. The tab ID that had its page errors
+ * count changed. If this is provided and it doesn't match the currently
+ * selected tab, then the button is not updated.
+ */
+DeveloperToolbar.prototype._updateErrorsCount = function (changedTabId) {
+ let tabId = this._chromeWindow.gBrowser.selectedTab.linkedPanel;
+ if (changedTabId && tabId != changedTabId) {
+ return;
+ }
+
+ let errors = this._errorsCount[tabId];
+ let warnings = this._warningsCount[tabId];
+ let btn = this._errorCounterButton;
+ if (errors) {
+ let errorsText = L10N.getStr("toolboxToggleButton.errors");
+ errorsText = PluralForm.get(errors, errorsText).replace("#1", errors);
+
+ let warningsText = L10N.getStr("toolboxToggleButton.warnings");
+ warningsText = PluralForm.get(warnings, warningsText).replace("#1", warnings);
+
+ let tooltiptext = L10N.getFormatStr("toolboxToggleButton.tooltip",
+ errorsText, warningsText);
+
+ btn.setAttribute("error-count", errors);
+ btn.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ btn.removeAttribute("error-count");
+ btn.setAttribute("tooltiptext", btn._defaultTooltipText);
+ }
+
+ this.emit("errors-counter-updated");
+};
+
+/**
+ * Reset the errors counter for the given tab.
+ *
+ * @param nsIDOMElement tab The xul:tab for which you want to reset the page
+ * errors counters.
+ */
+DeveloperToolbar.prototype.resetErrorsCount = function (tab) {
+ let tabId = tab.linkedPanel;
+ if (tabId in this._errorsCount || tabId in this._warningsCount) {
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+ this._updateErrorsCount(tabId);
+ }
+};
+
+/**
+ * Creating a OutputPanel is asynchronous
+ */
+function OutputPanel() {
+ throw new Error("Use OutputPanel.create()");
+}
+
+/**
+ * Panel to handle command line output.
+ *
+ * There is a tooltip bug on Windows and OSX that prevents tooltips from being
+ * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
+ * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
+ * We now use a tooltip on Linux and a panel on OSX & Windows.
+ *
+ * If a panel has no content and no height it is not shown when openPopup is
+ * called on Windows and OSX (bug 692348) ... this prevents the panel from
+ * appearing the first time it is shown. Setting the panel's height to 1px
+ * before calling openPopup works around this issue as we resize it ourselves
+ * anyway.
+ *
+ * @param devtoolbar The parent DeveloperToolbar object
+ */
+OutputPanel.create = function (devtoolbar) {
+ let outputPanel = Object.create(OutputPanel.prototype);
+ return outputPanel._init(devtoolbar);
+};
+
+/**
+ * @private See OutputPanel.create
+ */
+OutputPanel.prototype._init = function (devtoolbar) {
+ this._devtoolbar = devtoolbar;
+ this._input = this._devtoolbar._input;
+ this._toolbar = this._devtoolbar._doc.getElementById("developer-toolbar");
+
+ /*
+ <tooltip|panel id="gcli-output"
+ noautofocus="true"
+ noautohide="true"
+ class="gcli-panel">
+ <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
+ id="gcli-output-frame"
+ src="chrome://devtools/content/commandline/commandlineoutput.xhtml"
+ sandbox="allow-same-origin"/>
+ </tooltip|panel>
+ */
+
+ // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ this._panel = this._devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
+
+ this._panel.id = "gcli-output";
+ this._panel.classList.add("gcli-panel");
+
+ if (isLinux) {
+ this.canHide = false;
+ this._onpopuphiding = this._onpopuphiding.bind(this);
+ this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
+ } else {
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("noautohide", "true");
+
+ // Bug 692348: On Windows and OSX if a panel has no content and no height
+ // openPopup fails to display it. Setting the height to 1px alows the panel
+ // to be displayed before has content or a real height i.e. the first time
+ // it is displayed.
+ this._panel.setAttribute("height", "1px");
+ }
+
+ this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
+
+ this._frame = this._devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
+ this._frame.id = "gcli-output-frame";
+ this._frame.setAttribute("src", "chrome://devtools/content/commandline/commandlineoutput.xhtml");
+ this._frame.setAttribute("sandbox", "allow-same-origin");
+ this._panel.appendChild(this._frame);
+
+ this.displayedOutput = undefined;
+
+ this._update = this._update.bind(this);
+
+ // Wire up the element from the iframe, and resolve the promise
+ let deferred = defer();
+ let onload = () => {
+ this._frame.removeEventListener("load", onload, true);
+
+ this.document = this._frame.contentDocument;
+ this._copyTheme();
+
+ this._div = this.document.getElementById("gcli-output-root");
+ this._div.classList.add("gcli-row-out");
+ this._div.setAttribute("aria-live", "assertive");
+
+ let styles = this._toolbar.ownerDocument.defaultView
+ .getComputedStyle(this._toolbar);
+ this._div.setAttribute("dir", styles.direction);
+
+ deferred.resolve(this);
+ };
+ this._frame.addEventListener("load", onload, true);
+
+ return deferred.promise;
+};
+
+/* Copy the current devtools theme attribute into the iframe,
+ so it can be styled correctly. */
+OutputPanel.prototype._copyTheme = function () {
+ if (this.document) {
+ let theme =
+ this._devtoolbar._doc.documentElement.getAttribute("devtoolstheme");
+ this.document.documentElement.setAttribute("devtoolstheme", theme);
+ }
+};
+
+/**
+ * Prevent the popup from hiding if it is not permitted via this.canHide.
+ */
+OutputPanel.prototype._onpopuphiding = function (ev) {
+ // TODO: When we switch back from tooltip to panel we can remove this hack:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ if (isLinux && !this.canHide) {
+ ev.preventDefault();
+ }
+};
+
+/**
+ * Display the OutputPanel.
+ */
+OutputPanel.prototype.show = function () {
+ if (isLinux) {
+ this.canHide = false;
+ }
+
+ // We need to reset the iframe size in order for future size calculations to
+ // be correct
+ this._frame.style.minHeight = this._frame.style.maxHeight = 0;
+ this._frame.style.minWidth = 0;
+
+ this._copyTheme();
+ this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null);
+ this._resize();
+
+ this._input.focus();
+};
+
+/**
+ * Internal helper to set the height of the output panel to fit the available
+ * content;
+ */
+OutputPanel.prototype._resize = function () {
+ if (this._panel == null || this.document == null || !this._panel.state == "closed") {
+ return;
+ }
+
+ // Set max panel width to match any content with a max of the width of the
+ // browser window.
+ let maxWidth = this._panel.ownerDocument.documentElement.clientWidth;
+
+ // Adjust max width according to OS.
+ // We'd like to put this in CSS but we can't:
+ // body { width: calc(min(-5px, max-content)); }
+ // #_panel { max-width: -5px; }
+ switch (Services.appinfo.OS) {
+ case "Linux":
+ maxWidth -= 5;
+ break;
+ case "Darwin":
+ maxWidth -= 25;
+ break;
+ case "WINNT":
+ maxWidth -= 5;
+ break;
+ }
+
+ this.document.body.style.width = "-moz-max-content";
+ let style = this._frame.contentWindow.getComputedStyle(this.document.body);
+ let frameWidth = parseInt(style.width, 10);
+ let width = Math.min(maxWidth, frameWidth);
+ this.document.body.style.width = width + "px";
+
+ // Set the width of the iframe.
+ this._frame.style.minWidth = width + "px";
+ this._panel.style.maxWidth = maxWidth + "px";
+
+ // browserAdjustment is used to correct the panel height according to the
+ // browsers borders etc.
+ const browserAdjustment = 15;
+
+ // Set max panel height to match any content with a max of the height of the
+ // browser window.
+ let maxHeight =
+ this._panel.ownerDocument.documentElement.clientHeight - browserAdjustment;
+ let height = Math.min(maxHeight, this.document.documentElement.scrollHeight);
+
+ // Set the height of the iframe. Setting iframe.height does not work.
+ this._frame.style.minHeight = this._frame.style.maxHeight = height + "px";
+
+ // Set the height and width of the panel to match the iframe.
+ this._panel.sizeTo(width, height);
+
+ // Move the panel to the correct position in the case that it has been
+ // positioned incorrectly.
+ let screenX = this._input.boxObject.screenX;
+ let screenY = this._toolbar.boxObject.screenY;
+ this._panel.moveTo(screenX, screenY - height);
+};
+
+/**
+ * Called by GCLI when a command is executed.
+ */
+OutputPanel.prototype._outputChanged = function (ev) {
+ if (ev.output.hidden) {
+ return;
+ }
+
+ this.remove();
+
+ this.displayedOutput = ev.output;
+
+ if (this.displayedOutput.completed) {
+ this._update();
+ } else {
+ this.displayedOutput.promise.then(this._update, this._update)
+ .then(null, console.error);
+ }
+};
+
+/**
+ * Called when displayed Output says it's changed or from outputChanged, which
+ * happens when there is a new displayed Output.
+ */
+OutputPanel.prototype._update = function () {
+ // destroy has been called, bail out
+ if (this._div == null) {
+ return;
+ }
+
+ // Empty this._div
+ while (this._div.hasChildNodes()) {
+ this._div.removeChild(this._div.firstChild);
+ }
+
+ if (this.displayedOutput.data != null) {
+ let context = this._devtoolbar.requisition.conversionContext;
+ this.displayedOutput.convert("dom", context).then(node => {
+ if (node == null) {
+ return;
+ }
+
+ while (this._div.hasChildNodes()) {
+ this._div.removeChild(this._div.firstChild);
+ }
+
+ let links = node.querySelectorAll("*[href]");
+ for (let i = 0; i < links.length; i++) {
+ links[i].setAttribute("target", "_blank");
+ }
+
+ this._div.appendChild(node);
+ this.show();
+ });
+ }
+};
+
+/**
+ * Detach listeners from the currently displayed Output.
+ */
+OutputPanel.prototype.remove = function () {
+ if (isLinux) {
+ this.canHide = true;
+ }
+
+ if (this._panel && this._panel.hidePopup) {
+ this._panel.hidePopup();
+ }
+
+ if (this.displayedOutput) {
+ delete this.displayedOutput;
+ }
+};
+
+/**
+ * Detach listeners from the currently displayed Output.
+ */
+OutputPanel.prototype.destroy = function () {
+ this.remove();
+
+ this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
+
+ this._panel.removeChild(this._frame);
+ this._toolbar.parentElement.removeChild(this._panel);
+
+ delete this._devtoolbar;
+ delete this._input;
+ delete this._toolbar;
+ delete this._onpopuphiding;
+ delete this._panel;
+ delete this._frame;
+ delete this._content;
+ delete this._div;
+ delete this.document;
+};
+
+/**
+ * Called by GCLI to indicate that we should show or hide one either the
+ * tooltip panel or the output panel.
+ */
+OutputPanel.prototype._visibilityChanged = function (ev) {
+ if (ev.outputVisible === true) {
+ // this.show is called by _outputChanged
+ } else {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ this._panel.hidePopup();
+ }
+};
+
+/**
+ * Creating a TooltipPanel is asynchronous
+ */
+function TooltipPanel() {
+ throw new Error("Use TooltipPanel.create()");
+}
+
+/**
+ * Panel to handle tooltips.
+ *
+ * There is a tooltip bug on Windows and OSX that prevents tooltips from being
+ * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
+ * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
+ * We now use a tooltip on Linux and a panel on OSX & Windows.
+ *
+ * If a panel has no content and no height it is not shown when openPopup is
+ * called on Windows and OSX (bug 692348) ... this prevents the panel from
+ * appearing the first time it is shown. Setting the panel's height to 1px
+ * before calling openPopup works around this issue as we resize it ourselves
+ * anyway.
+ *
+ * @param devtoolbar The parent DeveloperToolbar object
+ */
+TooltipPanel.create = function (devtoolbar) {
+ let tooltipPanel = Object.create(TooltipPanel.prototype);
+ return tooltipPanel._init(devtoolbar);
+};
+
+/**
+ * @private See TooltipPanel.create
+ */
+TooltipPanel.prototype._init = function (devtoolbar) {
+ let deferred = defer();
+
+ this._devtoolbar = devtoolbar;
+ this._input = devtoolbar._doc.querySelector(".gclitoolbar-input-node");
+ this._toolbar = devtoolbar._doc.querySelector("#developer-toolbar");
+ this._dimensions = { start: 0, end: 0 };
+
+ /*
+ <tooltip|panel id="gcli-tooltip"
+ type="arrow"
+ noautofocus="true"
+ noautohide="true"
+ class="gcli-panel">
+ <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
+ id="gcli-tooltip-frame"
+ src="chrome://devtools/content/commandline/commandlinetooltip.xhtml"
+ flex="1"
+ sandbox="allow-same-origin"/>
+ </tooltip|panel>
+ */
+
+ // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ this._panel = devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
+
+ this._panel.id = "gcli-tooltip";
+ this._panel.classList.add("gcli-panel");
+
+ if (isLinux) {
+ this.canHide = false;
+ this._onpopuphiding = this._onpopuphiding.bind(this);
+ this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
+ } else {
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("noautohide", "true");
+
+ // Bug 692348: On Windows and OSX if a panel has no content and no height
+ // openPopup fails to display it. Setting the height to 1px alows the panel
+ // to be displayed before has content or a real height i.e. the first time
+ // it is displayed.
+ this._panel.setAttribute("height", "1px");
+ }
+
+ this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
+
+ this._frame = devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
+ this._frame.id = "gcli-tooltip-frame";
+ this._frame.setAttribute("src", "chrome://devtools/content/commandline/commandlinetooltip.xhtml");
+ this._frame.setAttribute("flex", "1");
+ this._frame.setAttribute("sandbox", "allow-same-origin");
+ this._panel.appendChild(this._frame);
+
+ /**
+ * Wire up the element from the iframe, and resolve the promise.
+ */
+ let onload = () => {
+ this._frame.removeEventListener("load", onload, true);
+
+ this.document = this._frame.contentDocument;
+ this._copyTheme();
+ this.hintElement = this.document.getElementById("gcli-tooltip-root");
+ this._connector = this.document.getElementById("gcli-tooltip-connector");
+
+ let styles = this._toolbar.ownerDocument.defaultView
+ .getComputedStyle(this._toolbar);
+ this.hintElement.setAttribute("dir", styles.direction);
+
+ deferred.resolve(this);
+ };
+ this._frame.addEventListener("load", onload, true);
+
+ return deferred.promise;
+};
+
+/* Copy the current devtools theme attribute into the iframe,
+ so it can be styled correctly. */
+TooltipPanel.prototype._copyTheme = function () {
+ if (this.document) {
+ let theme =
+ this._devtoolbar._doc.documentElement.getAttribute("devtoolstheme");
+ this.document.documentElement.setAttribute("devtoolstheme", theme);
+ }
+};
+
+/**
+ * Prevent the popup from hiding if it is not permitted via this.canHide.
+ */
+TooltipPanel.prototype._onpopuphiding = function (ev) {
+ // TODO: When we switch back from tooltip to panel we can remove this hack:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ if (isLinux && !this.canHide) {
+ ev.preventDefault();
+ }
+};
+
+/**
+ * Display the TooltipPanel.
+ */
+TooltipPanel.prototype.show = function (dimensions) {
+ if (!dimensions) {
+ dimensions = { start: 0, end: 0 };
+ }
+ this._dimensions = dimensions;
+
+ // This is nasty, but displaying the panel causes it to re-flow, which can
+ // change the size it should be, so we need to resize the iframe after the
+ // panel has displayed
+ this._panel.ownerDocument.defaultView.setTimeout(() => {
+ this._resize();
+ }, 0);
+
+ if (isLinux) {
+ this.canHide = false;
+ }
+
+ this._copyTheme();
+ this._resize();
+ this._panel.openPopup(this._input, "before_start", dimensions.start * 10, 0,
+ false, false, null);
+ this._input.focus();
+};
+
+/**
+ * One option is to spend lots of time taking an average width of characters
+ * in the current font, dynamically, and weighting for the frequency of use of
+ * various characters, or even to render the given string off screen, and then
+ * measure the width.
+ * Or we could do this...
+ */
+const AVE_CHAR_WIDTH = 4.5;
+
+/**
+ * Display the TooltipPanel.
+ */
+TooltipPanel.prototype._resize = function () {
+ if (this._panel == null || this.document == null || !this._panel.state == "closed") {
+ return;
+ }
+
+ let offset = 10 + Math.floor(this._dimensions.start * AVE_CHAR_WIDTH);
+ this._panel.style.marginLeft = offset + "px";
+
+ /*
+ // Bug 744906: UX review - Not sure if we want this code to fatten connector
+ // with param width
+ let width = Math.floor(this._dimensions.end * AVE_CHAR_WIDTH);
+ width = Math.min(width, 100);
+ width = Math.max(width, 10);
+ this._connector.style.width = width + "px";
+ */
+
+ this._frame.height = this.document.body.scrollHeight;
+};
+
+/**
+ * Hide the TooltipPanel.
+ */
+TooltipPanel.prototype.remove = function () {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ if (this._panel && this._panel.hidePopup) {
+ this._panel.hidePopup();
+ }
+};
+
+/**
+ * Hide the TooltipPanel.
+ */
+TooltipPanel.prototype.destroy = function () {
+ this.remove();
+
+ this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
+
+ this._panel.removeChild(this._frame);
+ this._toolbar.parentElement.removeChild(this._panel);
+
+ delete this._connector;
+ delete this._dimensions;
+ delete this._input;
+ delete this._onpopuphiding;
+ delete this._panel;
+ delete this._frame;
+ delete this._toolbar;
+ delete this._content;
+ delete this.document;
+ delete this.hintElement;
+};
+
+/**
+ * Called by GCLI to indicate that we should show or hide one either the
+ * tooltip panel or the output panel.
+ */
+TooltipPanel.prototype._visibilityChanged = function (ev) {
+ if (ev.tooltipVisible === true) {
+ this.show(ev.dimensions);
+ } else {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ this._panel.hidePopup();
+ }
+};