devtools-browser.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811
  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. /**
  6. * This is the main module loaded in Firefox desktop that handles browser
  7. * windows and coordinates devtools around each window.
  8. *
  9. * This module is loaded lazily by devtools-startup.js, once the first
  10. * browser window is ready (i.e. fired browser-delayed-startup-finished event)
  11. **/
  12. const {Cc, Ci, Cu} = require("chrome");
  13. const Services = require("Services");
  14. const promise = require("promise");
  15. const defer = require("devtools/shared/defer");
  16. const Telemetry = require("devtools/client/shared/telemetry");
  17. const { gDevTools } = require("./devtools");
  18. const { when: unload } = require("sdk/system/unload");
  19. // Load target and toolbox lazily as they need gDevTools to be fully initialized
  20. loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
  21. loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
  22. loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
  23. loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
  24. loader.lazyRequireGetter(this, "BrowserMenus", "devtools/client/framework/browser-menus");
  25. loader.lazyImporter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
  26. #ifdef MOZ_AUSTRALIS
  27. loader.lazyImporter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm");
  28. #endif
  29. const {LocalizationHelper} = require("devtools/shared/l10n");
  30. const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
  31. const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
  32. const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
  33. const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
  34. const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
  35. /**
  36. * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
  37. * Firefox instance.
  38. */
  39. var gDevToolsBrowser = exports.gDevToolsBrowser = {
  40. /**
  41. * A record of the windows whose menus we altered, so we can undo the changes
  42. * as the window is closed
  43. */
  44. _trackedBrowserWindows: new Set(),
  45. _telemetry: new Telemetry(),
  46. _tabStats: {
  47. peakOpen: 0,
  48. peakPinned: 0,
  49. histOpen: [],
  50. histPinned: []
  51. },
  52. /**
  53. * This function is for the benefit of Tools:DevToolbox in
  54. * browser/base/content/browser-sets.inc and should not be used outside
  55. * of there
  56. */
  57. // used by browser-sets.inc, command
  58. toggleToolboxCommand: function (gBrowser) {
  59. let target = TargetFactory.forTab(gBrowser.selectedTab);
  60. let toolbox = gDevTools.getToolbox(target);
  61. // If a toolbox exists, using toggle from the Main window :
  62. // - should close a docked toolbox
  63. // - should focus a windowed toolbox
  64. let isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW;
  65. isDocked ? gDevTools.closeToolbox(target) : gDevTools.showToolbox(target);
  66. },
  67. /**
  68. * This function ensures the right commands are enabled in a window,
  69. * depending on their relevant prefs. It gets run when a window is registered,
  70. * or when any of the devtools prefs change.
  71. */
  72. updateCommandAvailability: function (win) {
  73. let doc = win.document;
  74. function toggleMenuItem(id, isEnabled) {
  75. let cmd = doc.getElementById(id);
  76. if (!cmd) {
  77. return;
  78. }
  79. if (isEnabled) {
  80. cmd.removeAttribute("disabled");
  81. cmd.removeAttribute("hidden");
  82. } else {
  83. cmd.setAttribute("disabled", "true");
  84. cmd.setAttribute("hidden", "true");
  85. }
  86. }
  87. let idEls = [];
  88. // Enable developer toolbar?
  89. let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled");
  90. idEls = [
  91. "appmenu_devToolbar",
  92. "menu_devToolbar"
  93. ];
  94. idEls.forEach(function (idEl) {
  95. toggleMenuItem(idEl, devToolbarEnabled);
  96. let focusEl = doc.getElementById(idEl);
  97. if (!focusEl) {
  98. return;
  99. }
  100. if (devToolbarEnabled) {
  101. focusEl.removeAttribute("disabled");
  102. } else {
  103. focusEl.setAttribute("disabled", "true");
  104. }
  105. });
  106. if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) {
  107. win.DeveloperToolbar.show(false).catch(console.error);
  108. }
  109. // Enable Browser Toolbox?
  110. let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
  111. let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
  112. let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled;
  113. idEls = [
  114. "appmenu_browserToolbox",
  115. "menu_browserToolbox"
  116. ];
  117. idEls.forEach(function (idEl) {
  118. toggleMenuItem(idEl, remoteEnabled);
  119. });
  120. idEls = [
  121. "appmenu_browserContentToolbox",
  122. "menu_browserContentToolbox"
  123. ];
  124. idEls.forEach(function (idEl) {
  125. toggleMenuItem(idEl, remoteEnabled && win.gMultiProcessBrowser);
  126. });
  127. // Enable DevTools connection screen, if the preference allows this.
  128. idEls = [
  129. "appmenu_devtools_connect",
  130. "menu_devtools_connect"
  131. ];
  132. idEls.forEach(function (idEl) {
  133. toggleMenuItem(idEl, devtoolsRemoteEnabled);
  134. });
  135. },
  136. observe: function (subject, topic, prefName) {
  137. switch (topic) {
  138. case "browser-delayed-startup-finished":
  139. this._registerBrowserWindow(subject);
  140. break;
  141. case "nsPref:changed":
  142. if (prefName.endsWith("enabled")) {
  143. for (let win of this._trackedBrowserWindows) {
  144. this.updateCommandAvailability(win);
  145. }
  146. }
  147. break;
  148. }
  149. },
  150. _prefObserverRegistered: false,
  151. ensurePrefObserver: function () {
  152. if (!this._prefObserverRegistered) {
  153. this._prefObserverRegistered = true;
  154. Services.prefs.addObserver("devtools.", this, false);
  155. }
  156. },
  157. /**
  158. * This function is for the benefit of Tools:{toolId} commands,
  159. * triggered from the WebDeveloper menu and keyboard shortcuts.
  160. *
  161. * selectToolCommand's behavior:
  162. * - if the toolbox is closed,
  163. * we open the toolbox and select the tool
  164. * - if the toolbox is open, and the targeted tool is not selected,
  165. * we select it
  166. * - if the toolbox is open, and the targeted tool is selected,
  167. * and the host is NOT a window, we close the toolbox
  168. * - if the toolbox is open, and the targeted tool is selected,
  169. * and the host is a window, we raise the toolbox window
  170. */
  171. // Used when: - registering a new tool
  172. // - new xul window, to add menu items
  173. selectToolCommand: function (gBrowser, toolId) {
  174. let target = TargetFactory.forTab(gBrowser.selectedTab);
  175. let toolbox = gDevTools.getToolbox(target);
  176. let toolDefinition = gDevTools.getToolDefinition(toolId);
  177. if (toolbox &&
  178. (toolbox.currentToolId == toolId ||
  179. (toolId == "webconsole" && toolbox.splitConsole)))
  180. {
  181. toolbox.fireCustomKey(toolId);
  182. if (toolDefinition.preventClosingOnKey || toolbox.hostType == Toolbox.HostType.WINDOW) {
  183. toolbox.raise();
  184. } else {
  185. gDevTools.closeToolbox(target);
  186. }
  187. gDevTools.emit("select-tool-command", toolId);
  188. } else {
  189. gDevTools.showToolbox(target, toolId).then(() => {
  190. let target = TargetFactory.forTab(gBrowser.selectedTab);
  191. let toolbox = gDevTools.getToolbox(target);
  192. toolbox.fireCustomKey(toolId);
  193. gDevTools.emit("select-tool-command", toolId);
  194. });
  195. }
  196. },
  197. /**
  198. * Open a tab on "about:debugging", optionally pre-select a given tab.
  199. */
  200. // Used by browser-sets.inc, command
  201. openAboutDebugging: function (gBrowser, hash) {
  202. let url = "about:debugging" + (hash ? "#" + hash : "");
  203. gBrowser.selectedTab = gBrowser.addTab(url);
  204. },
  205. /**
  206. * Open a tab to allow connects to a remote browser
  207. */
  208. // Used by browser-sets.inc, command
  209. openConnectScreen: function (gBrowser) {
  210. gBrowser.selectedTab = gBrowser.addTab("chrome://devtools/content/framework/connect/connect.xhtml");
  211. },
  212. /**
  213. * Open WebIDE
  214. */
  215. // Used by browser-sets.inc, command
  216. // itself, webide widget
  217. openWebIDE: function () {
  218. let win = Services.wm.getMostRecentWindow("devtools:webide");
  219. if (win) {
  220. win.focus();
  221. } else {
  222. Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
  223. }
  224. },
  225. _getContentProcessTarget: function (processId) {
  226. // Create a DebuggerServer in order to connect locally to it
  227. if (!DebuggerServer.initialized) {
  228. DebuggerServer.init();
  229. DebuggerServer.addBrowserActors();
  230. }
  231. DebuggerServer.allowChromeProcess = true;
  232. let transport = DebuggerServer.connectPipe();
  233. let client = new DebuggerClient(transport);
  234. let deferred = defer();
  235. client.connect().then(() => {
  236. client.getProcess(processId)
  237. .then(response => {
  238. let options = {
  239. form: response.form,
  240. client: client,
  241. chrome: true,
  242. isTabActor: false
  243. };
  244. return TargetFactory.forRemoteTab(options);
  245. })
  246. .then(target => {
  247. // Ensure closing the connection in order to cleanup
  248. // the debugger client and also the server created in the
  249. // content process
  250. target.on("close", () => {
  251. client.close();
  252. });
  253. deferred.resolve(target);
  254. });
  255. });
  256. return deferred.promise;
  257. },
  258. // Used by menus.js
  259. openContentProcessToolbox: function (gBrowser) {
  260. let { childCount } = Services.ppmm;
  261. // Get the process message manager for the current tab
  262. let mm = gBrowser.selectedBrowser.messageManager.processMessageManager;
  263. let processId = null;
  264. for (let i = 1; i < childCount; i++) {
  265. let child = Services.ppmm.getChildAt(i);
  266. if (child == mm) {
  267. processId = i;
  268. break;
  269. }
  270. }
  271. if (processId) {
  272. this._getContentProcessTarget(processId)
  273. .then(target => {
  274. // Display a new toolbox, in a new window, with debugger by default
  275. return gDevTools.showToolbox(target, "jsdebugger",
  276. Toolbox.HostType.WINDOW);
  277. });
  278. } else {
  279. let msg = L10N.getStr("toolbox.noContentProcessForTab.message");
  280. Services.prompt.alert(null, "", msg);
  281. }
  282. },
  283. /**
  284. * Install Developer widget
  285. */
  286. installDeveloperWidget: function () {
  287. #ifdef MOZ_AUSTRALIS
  288. let id = "developer-button";
  289. let widget = CustomizableUI.getWidget(id);
  290. if (widget && widget.provider == CustomizableUI.PROVIDER_API) {
  291. return;
  292. }
  293. CustomizableUI.createWidget({
  294. id: id,
  295. type: "view",
  296. viewId: "PanelUI-developer",
  297. shortcutId: "key_devToolboxMenuItem",
  298. tooltiptext: "developer-button.tooltiptext2",
  299. defaultArea: CustomizableUI.AREA_PANEL,
  300. onViewShowing: function (aEvent) {
  301. // Populate the subview with whatever menuitems are in the developer
  302. // menu. We skip menu elements, because the menu panel has no way
  303. // of dealing with those right now.
  304. let doc = aEvent.target.ownerDocument;
  305. let win = doc.defaultView;
  306. let menu = doc.getElementById("menuWebDeveloperPopup");
  307. let itemsToDisplay = [...menu.children];
  308. // Hardcode the addition of the "work offline" menuitem at the bottom:
  309. itemsToDisplay.push({localName: "menuseparator", getAttribute: () => {}});
  310. itemsToDisplay.push(doc.getElementById("goOfflineMenuitem"));
  311. let developerItems = doc.getElementById("PanelUI-developerItems");
  312. // Import private helpers from CustomizableWidgets
  313. let { clearSubview, fillSubviewFromMenuItems } =
  314. Cu.import("resource:///modules/CustomizableWidgets.jsm", {});
  315. clearSubview(developerItems);
  316. fillSubviewFromMenuItems(itemsToDisplay, developerItems);
  317. },
  318. onBeforeCreated: function (doc) {
  319. // Bug 1223127, CUI should make this easier to do.
  320. if (doc.getElementById("PanelUI-developerItems")) {
  321. return;
  322. }
  323. let view = doc.createElement("panelview");
  324. view.id = "PanelUI-developerItems";
  325. let panel = doc.createElement("vbox");
  326. panel.setAttribute("class", "panel-subview-body");
  327. view.appendChild(panel);
  328. doc.getElementById("PanelUI-multiView").appendChild(view);
  329. }
  330. });
  331. #else
  332. return;
  333. #endif
  334. },
  335. /**
  336. * Install WebIDE widget
  337. */
  338. // Used by itself
  339. installWebIDEWidget: function () {
  340. #ifdef MOZ_AUSTRALIS
  341. if (this.isWebIDEWidgetInstalled()) {
  342. return;
  343. }
  344. let defaultArea;
  345. if (Services.prefs.getBoolPref("devtools.webide.widget.inNavbarByDefault")) {
  346. defaultArea = CustomizableUI.AREA_NAVBAR;
  347. } else {
  348. defaultArea = CustomizableUI.AREA_PANEL;
  349. }
  350. CustomizableUI.createWidget({
  351. id: "webide-button",
  352. shortcutId: "key_webide",
  353. label: "devtools-webide-button2.label",
  354. tooltiptext: "devtools-webide-button2.tooltiptext",
  355. defaultArea: defaultArea,
  356. onCommand: function (aEvent) {
  357. gDevToolsBrowser.openWebIDE();
  358. }
  359. });
  360. #else
  361. return;
  362. #endif
  363. },
  364. isWebIDEWidgetInstalled: function () {
  365. #ifdef MOZ_AUSTRALIS
  366. let widgetWrapper = CustomizableUI.getWidget("webide-button");
  367. return !!(widgetWrapper && widgetWrapper.provider == CustomizableUI.PROVIDER_API);
  368. #else
  369. return false;
  370. #endif
  371. },
  372. /**
  373. * The deferred promise will be resolved by WebIDE's UI.init()
  374. */
  375. isWebIDEInitialized: defer(),
  376. /**
  377. * Uninstall WebIDE widget
  378. */
  379. uninstallWebIDEWidget: function () {
  380. #ifdef MOZ_AUSTRALIS
  381. if (this.isWebIDEWidgetInstalled()) {
  382. CustomizableUI.removeWidgetFromArea("webide-button");
  383. }
  384. CustomizableUI.destroyWidget("webide-button");
  385. #else
  386. return;
  387. #endif
  388. },
  389. /**
  390. * Move WebIDE widget to the navbar
  391. */
  392. // Used by webide.js
  393. moveWebIDEWidgetInNavbar: function () {
  394. #ifdef MOZ_AUSTRALIS
  395. CustomizableUI.addWidgetToArea("webide-button", CustomizableUI.AREA_NAVBAR);
  396. #else
  397. return;
  398. #endif
  399. },
  400. /**
  401. * Add this DevTools's presence to a browser window's document
  402. *
  403. * @param {XULDocument} doc
  404. * The document to which devtools should be hooked to.
  405. */
  406. _registerBrowserWindow: function (win) {
  407. if (gDevToolsBrowser._trackedBrowserWindows.has(win)) {
  408. return;
  409. }
  410. gDevToolsBrowser._trackedBrowserWindows.add(win);
  411. BrowserMenus.addMenus(win.document);
  412. // Register the Developer widget in the Hamburger menu or navbar
  413. // only once menus are registered as it depends on it.
  414. gDevToolsBrowser.installDeveloperWidget();
  415. // Inject lazily DeveloperToolbar on the chrome window
  416. loader.lazyGetter(win, "DeveloperToolbar", function () {
  417. let { DeveloperToolbar } = require("devtools/client/shared/developer-toolbar");
  418. return new DeveloperToolbar(win);
  419. });
  420. this.updateCommandAvailability(win);
  421. this.ensurePrefObserver();
  422. win.addEventListener("unload", this);
  423. let tabContainer = win.gBrowser.tabContainer;
  424. tabContainer.addEventListener("TabSelect", this, false);
  425. tabContainer.addEventListener("TabOpen", this, false);
  426. tabContainer.addEventListener("TabClose", this, false);
  427. tabContainer.addEventListener("TabPinned", this, false);
  428. tabContainer.addEventListener("TabUnpinned", this, false);
  429. },
  430. /**
  431. * Hook the JS debugger tool to the "Debug Script" button of the slow script
  432. * dialog.
  433. */
  434. setSlowScriptDebugHandler: function DT_setSlowScriptDebugHandler() {
  435. let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
  436. .getService(Ci.nsISlowScriptDebug);
  437. let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
  438. function slowScriptDebugHandler(aTab, aCallback) {
  439. let target = TargetFactory.forTab(aTab);
  440. gDevTools.showToolbox(target, "jsdebugger").then(toolbox => {
  441. let threadClient = toolbox.getCurrentPanel().panelWin.gThreadClient;
  442. // Break in place, which means resuming the debuggee thread and pausing
  443. // right before the next step happens.
  444. switch (threadClient.state) {
  445. case "paused":
  446. // When the debugger is already paused.
  447. threadClient.resumeThenPause();
  448. aCallback();
  449. break;
  450. case "attached":
  451. // When the debugger is already open.
  452. threadClient.interrupt(() => {
  453. threadClient.resumeThenPause();
  454. aCallback();
  455. });
  456. break;
  457. case "resuming":
  458. // The debugger is newly opened.
  459. threadClient.addOneTimeListener("resumed", () => {
  460. threadClient.interrupt(() => {
  461. threadClient.resumeThenPause();
  462. aCallback();
  463. });
  464. });
  465. break;
  466. default:
  467. throw Error("invalid thread client state in slow script debug handler: " +
  468. threadClient.state);
  469. }
  470. });
  471. }
  472. debugService.activationHandler = function (aWindow) {
  473. let chromeWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
  474. .getInterface(Ci.nsIWebNavigation)
  475. .QueryInterface(Ci.nsIDocShellTreeItem)
  476. .rootTreeItem
  477. .QueryInterface(Ci.nsIInterfaceRequestor)
  478. .getInterface(Ci.nsIDOMWindow)
  479. .QueryInterface(Ci.nsIDOMChromeWindow);
  480. let setupFinished = false;
  481. slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab,
  482. () => { setupFinished = true; });
  483. // Don't return from the interrupt handler until the debugger is brought
  484. // up; no reason to continue executing the slow script.
  485. let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
  486. .getInterface(Ci.nsIDOMWindowUtils);
  487. utils.enterModalState();
  488. while (!setupFinished) {
  489. tm.currentThread.processNextEvent(true);
  490. }
  491. utils.leaveModalState();
  492. };
  493. debugService.remoteActivationHandler = function (aBrowser, aCallback) {
  494. let chromeWindow = aBrowser.ownerDocument.defaultView;
  495. let tab = chromeWindow.gBrowser.getTabForBrowser(aBrowser);
  496. chromeWindow.gBrowser.selected = tab;
  497. function callback() {
  498. aCallback.finishDebuggerStartup();
  499. }
  500. slowScriptDebugHandler(tab, callback);
  501. };
  502. },
  503. /**
  504. * Unset the slow script debug handler.
  505. */
  506. unsetSlowScriptDebugHandler: function DT_unsetSlowScriptDebugHandler() {
  507. let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
  508. .getService(Ci.nsISlowScriptDebug);
  509. debugService.activationHandler = undefined;
  510. },
  511. /**
  512. * Add the menuitem for a tool to all open browser windows.
  513. *
  514. * @param {object} toolDefinition
  515. * properties of the tool to add
  516. */
  517. _addToolToWindows: function DT_addToolToWindows(toolDefinition) {
  518. // No menu item or global shortcut is required for options panel.
  519. if (!toolDefinition.inMenu) {
  520. return;
  521. }
  522. // Skip if the tool is disabled.
  523. try {
  524. if (toolDefinition.visibilityswitch &&
  525. !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) {
  526. return;
  527. }
  528. } catch (e) {}
  529. // We need to insert the new tool in the right place, which means knowing
  530. // the tool that comes before the tool that we're trying to add
  531. let allDefs = gDevTools.getToolDefinitionArray();
  532. let prevDef;
  533. for (let def of allDefs) {
  534. if (!def.inMenu) {
  535. continue;
  536. }
  537. if (def === toolDefinition) {
  538. break;
  539. }
  540. prevDef = def;
  541. }
  542. for (let win of gDevToolsBrowser._trackedBrowserWindows) {
  543. BrowserMenus.insertToolMenuElements(win.document, toolDefinition, prevDef);
  544. }
  545. if (toolDefinition.id === "jsdebugger") {
  546. gDevToolsBrowser.setSlowScriptDebugHandler();
  547. }
  548. },
  549. hasToolboxOpened: function (win) {
  550. let tab = win.gBrowser.selectedTab;
  551. for (let [target, toolbox] of gDevTools._toolboxes) {
  552. if (target.tab == tab) {
  553. return true;
  554. }
  555. }
  556. return false;
  557. },
  558. /**
  559. * Update the "Toggle Tools" checkbox in the developer tools menu. This is
  560. * called when a toolbox is created or destroyed.
  561. */
  562. _updateMenuCheckbox: function DT_updateMenuCheckbox() {
  563. for (let win of gDevToolsBrowser._trackedBrowserWindows) {
  564. let hasToolbox = gDevToolsBrowser.hasToolboxOpened(win);
  565. let idEls = [];
  566. idEls = [
  567. "appmenu_devToolbox",
  568. "menu_devToolbox"
  569. ];
  570. idEls.forEach(function (idEl) {
  571. let menu = win.document.getElementById(idEl);
  572. if (!menu) {
  573. return;
  574. }
  575. if (hasToolbox) {
  576. menu.setAttribute("checked", "true");
  577. } else {
  578. menu.removeAttribute("checked");
  579. }
  580. });
  581. }
  582. },
  583. /**
  584. * Remove the menuitem for a tool to all open browser windows.
  585. *
  586. * @param {string} toolId
  587. * id of the tool to remove
  588. */
  589. _removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
  590. for (let win of gDevToolsBrowser._trackedBrowserWindows) {
  591. BrowserMenus.removeToolFromMenu(toolId, win.document);
  592. }
  593. if (toolId === "jsdebugger") {
  594. gDevToolsBrowser.unsetSlowScriptDebugHandler();
  595. }
  596. },
  597. /**
  598. * Called on browser unload to remove menu entries, toolboxes and event
  599. * listeners from the closed browser window.
  600. *
  601. * @param {XULWindow} win
  602. * The window containing the menu entry
  603. */
  604. _forgetBrowserWindow: function (win) {
  605. if (!gDevToolsBrowser._trackedBrowserWindows.has(win)) {
  606. return;
  607. }
  608. gDevToolsBrowser._trackedBrowserWindows.delete(win);
  609. win.removeEventListener("unload", this);
  610. BrowserMenus.removeMenus(win.document);
  611. // Destroy toolboxes for closed window
  612. for (let [target, toolbox] of gDevTools._toolboxes) {
  613. if (toolbox.win.top == win) {
  614. toolbox.destroy();
  615. }
  616. }
  617. // Destroy the Developer toolbar if it has been accessed
  618. let desc = Object.getOwnPropertyDescriptor(win, "DeveloperToolbar");
  619. if (desc && !desc.get) {
  620. win.DeveloperToolbar.destroy();
  621. }
  622. let tabContainer = win.gBrowser.tabContainer;
  623. tabContainer.removeEventListener("TabSelect", this, false);
  624. tabContainer.removeEventListener("TabOpen", this, false);
  625. tabContainer.removeEventListener("TabClose", this, false);
  626. tabContainer.removeEventListener("TabPinned", this, false);
  627. tabContainer.removeEventListener("TabUnpinned", this, false);
  628. },
  629. handleEvent: function (event) {
  630. switch (event.type) {
  631. case "TabOpen":
  632. case "TabClose":
  633. case "TabPinned":
  634. case "TabUnpinned":
  635. let open = 0;
  636. let pinned = 0;
  637. for (let win of this._trackedBrowserWindows) {
  638. let tabContainer = win.gBrowser.tabContainer;
  639. let numPinnedTabs = win.gBrowser._numPinnedTabs || 0;
  640. let numTabs = tabContainer.itemCount - numPinnedTabs;
  641. open += numTabs;
  642. pinned += numPinnedTabs;
  643. }
  644. this._tabStats.histOpen.push(open);
  645. this._tabStats.histPinned.push(pinned);
  646. this._tabStats.peakOpen = Math.max(open, this._tabStats.peakOpen);
  647. this._tabStats.peakPinned = Math.max(pinned, this._tabStats.peakPinned);
  648. break;
  649. case "TabSelect":
  650. gDevToolsBrowser._updateMenuCheckbox();
  651. break;
  652. case "unload":
  653. // top-level browser window unload
  654. gDevToolsBrowser._forgetBrowserWindow(event.target.defaultView);
  655. break;
  656. }
  657. },
  658. _pingTelemetry: function () {
  659. let mean = function (arr) {
  660. if (arr.length === 0) {
  661. return 0;
  662. }
  663. let total = arr.reduce((a, b) => a + b);
  664. return Math.ceil(total / arr.length);
  665. };
  666. let tabStats = gDevToolsBrowser._tabStats;
  667. this._telemetry.log(TABS_OPEN_PEAK_HISTOGRAM, tabStats.peakOpen);
  668. this._telemetry.log(TABS_OPEN_AVG_HISTOGRAM, mean(tabStats.histOpen));
  669. this._telemetry.log(TABS_PINNED_PEAK_HISTOGRAM, tabStats.peakPinned);
  670. this._telemetry.log(TABS_PINNED_AVG_HISTOGRAM, mean(tabStats.histPinned));
  671. },
  672. /**
  673. * All browser windows have been closed, tidy up remaining objects.
  674. */
  675. destroy: function () {
  676. Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
  677. Services.obs.removeObserver(gDevToolsBrowser, "browser-delayed-startup-finished");
  678. Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
  679. gDevToolsBrowser._pingTelemetry();
  680. gDevToolsBrowser._telemetry = null;
  681. for (let win of gDevToolsBrowser._trackedBrowserWindows) {
  682. gDevToolsBrowser._forgetBrowserWindow(win);
  683. }
  684. },
  685. };
  686. // Handle all already registered tools,
  687. gDevTools.getToolDefinitionArray()
  688. .forEach(def => gDevToolsBrowser._addToolToWindows(def));
  689. // and the new ones.
  690. gDevTools.on("tool-registered", function (ev, toolId) {
  691. let toolDefinition = gDevTools._tools.get(toolId);
  692. gDevToolsBrowser._addToolToWindows(toolDefinition);
  693. });
  694. gDevTools.on("tool-unregistered", function (ev, toolId) {
  695. if (typeof toolId != "string") {
  696. toolId = toolId.id;
  697. }
  698. gDevToolsBrowser._removeToolFromWindows(toolId);
  699. });
  700. gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
  701. gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
  702. Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
  703. Services.obs.addObserver(gDevToolsBrowser, "browser-delayed-startup-finished", false);
  704. // Fake end of browser window load event for all already opened windows
  705. // that is already fully loaded.
  706. let enumerator = Services.wm.getEnumerator(gDevTools.chromeWindowType);
  707. while (enumerator.hasMoreElements()) {
  708. let win = enumerator.getNext();
  709. if (win.gBrowserInit && win.gBrowserInit.delayedStartupFinished) {
  710. gDevToolsBrowser._registerBrowserWindow(win);
  711. }
  712. }
  713. // Watch for module loader unload. Fires when the tools are reloaded.
  714. unload(function () {
  715. gDevToolsBrowser.destroy();
  716. });