1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414 |
- /* 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;
- let checkboxValue = "true";
- let appmenuEl = this._doc.getElementById("appmenu_devToolbar");
- let menuEl = this._doc.getElementById("menu_devToolbar");
- if (appmenuEl) {
- appmenuEl.setAttribute("checked", checkboxValue);
- }
- if (menuEl) {
- menuEl.setAttribute("checked", checkboxValue);
- }
- 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);
- let checkboxValue = "false";
- let appmenuEl = this._doc.getElementById("appmenu_devToolbar");
- let menuEl = this._doc.getElementById("menu_devToolbar");
- if (appmenuEl) {
- appmenuEl.setAttribute("checked", checkboxValue);
- }
- if (menuEl) {
- menuEl.setAttribute("checked", checkboxValue);
- }
- 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();
- }
- };
|