developer-toolbar.js 44 KB

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