devtools-browser.js (19873B)
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 5 "use strict"; 6 7 /** 8 * This is the main module loaded in Firefox desktop that handles browser 9 * windows and coordinates devtools around each window. 10 * 11 * This module is loaded lazily by devtools-clhandler.js, once the first 12 * browser window is ready (i.e. fired browser-delayed-startup-finished event) 13 */ 14 15 const lazy = {}; 16 ChromeUtils.defineESModuleGetters(lazy, { 17 BrowserToolboxLauncher: 18 "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs", 19 }); 20 21 const { 22 gDevTools, 23 } = require("resource://devtools/client/framework/devtools.js"); 24 const { 25 getTheme, 26 addThemeObserver, 27 removeThemeObserver, 28 } = require("resource://devtools/client/shared/theme.js"); 29 30 // Load toolbox lazily as it needs gDevTools to be fully initialized 31 loader.lazyRequireGetter( 32 this, 33 "Toolbox", 34 "resource://devtools/client/framework/toolbox.js", 35 true 36 ); 37 loader.lazyRequireGetter( 38 this, 39 "DevToolsServer", 40 "resource://devtools/server/devtools-server.js", 41 true 42 ); 43 loader.lazyRequireGetter( 44 this, 45 "BrowserMenus", 46 "resource://devtools/client/framework/browser-menus.js" 47 ); 48 loader.lazyRequireGetter( 49 this, 50 "appendStyleSheet", 51 "resource://devtools/client/shared/stylesheet-utils.js", 52 true 53 ); 54 loader.lazyRequireGetter( 55 this, 56 "ResponsiveUIManager", 57 "resource://devtools/client/responsive/manager.js" 58 ); 59 60 const BROWSER_STYLESHEET_URL = "chrome://devtools/skin/devtools-browser.css"; 61 62 const DEVTOOLS_F12_ENABLED_PREF = "devtools.f12_enabled"; 63 64 /** 65 * gDevToolsBrowser exposes functions to connect the gDevTools instance with a 66 * Firefox instance. 67 */ 68 var gDevToolsBrowser = (exports.gDevToolsBrowser = { 69 /** 70 * A record of the windows whose menus we altered, so we can undo the changes 71 * as the window is closed 72 */ 73 _trackedBrowserWindows: new Set(), 74 75 /** 76 * WeakMap keeping track of the devtools-browser stylesheets loaded in the various 77 * tracked windows. 78 */ 79 _browserStyleSheets: new WeakMap(), 80 81 /** 82 * This function is for the benefit of Tools:DevToolbox in 83 * browser/base/content/browser-sets.inc and should not be used outside 84 * of there 85 */ 86 // used by browser-sets.inc, command 87 toggleToolboxCommand(gBrowser, startTime) { 88 const toolbox = gDevTools.getToolboxForTab(gBrowser.selectedTab); 89 90 // If a toolbox exists, using toggle from the Main window : 91 // - should close a docked toolbox 92 // - should focus a windowed toolbox 93 const isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW; 94 if (isDocked) { 95 gDevTools.closeToolboxForTab(gBrowser.selectedTab); 96 } else { 97 gDevTools.showToolboxForTab(gBrowser.selectedTab, { startTime }); 98 } 99 }, 100 101 /** 102 * This function ensures the right commands are enabled in a window, 103 * depending on their relevant prefs. It gets run when a window is registered, 104 * or when any of the devtools prefs change. 105 */ 106 updateCommandAvailability(win) { 107 const doc = win.document; 108 109 function toggleMenuItem(id, isEnabled) { 110 const cmd = doc.getElementById(id); 111 cmd.hidden = !isEnabled; 112 if (isEnabled) { 113 cmd.removeAttribute("disabled"); 114 } else { 115 cmd.setAttribute("disabled", "true"); 116 } 117 } 118 119 // Enable Browser Toolbox? 120 const chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled"); 121 const devtoolsRemoteEnabled = Services.prefs.getBoolPref( 122 "devtools.debugger.remote-enabled" 123 ); 124 const remoteEnabled = chromeEnabled && devtoolsRemoteEnabled; 125 toggleMenuItem("menu_browserToolbox", remoteEnabled); 126 127 if (Services.prefs.getBoolPref("devtools.policy.disabled", false)) { 128 toggleMenuItem("menu_devToolbox", false); 129 toggleMenuItem("menu_devtools_remotedebugging", false); 130 toggleMenuItem("menu_browserToolbox", false); 131 toggleMenuItem("menu_browserConsole", false); 132 toggleMenuItem("menu_responsiveUI", false); 133 toggleMenuItem("menu_eyedropper", false); 134 toggleMenuItem("extensionsForDevelopers", false); 135 } 136 }, 137 138 /** 139 * This function makes sure that the "devtoolstheme" attribute is set on the 140 * browser window to make it possible to change colors on elements in the 141 * browser (like the splitter between the toolbox and web content). 142 */ 143 updateDevtoolsThemeAttribute(win) { 144 // Set an attribute on root element of each window to make it possible 145 // to change colors based on the selected devtools theme. 146 let devtoolsTheme = getTheme(); 147 if (devtoolsTheme != "dark") { 148 devtoolsTheme = "light"; 149 } 150 win.document.documentElement.setAttribute("devtoolstheme", devtoolsTheme); 151 }, 152 153 observe(subject, topic, prefName) { 154 switch (topic) { 155 case "browser-delayed-startup-finished": 156 this._registerBrowserWindow(subject); 157 break; 158 case "nsPref:changed": 159 if (prefName.endsWith("enabled")) { 160 for (const win of this._trackedBrowserWindows) { 161 this.updateCommandAvailability(win); 162 } 163 } 164 break; 165 case "quit-application": 166 gDevToolsBrowser.destroy({ shuttingDown: true }); 167 break; 168 case "devtools:loader:destroy": 169 // This event is fired when the devtools loader unloads, which happens 170 // only when the add-on workflow ask devtools to be reloaded. 171 if (subject.wrappedJSObject == require("@loader/unload")) { 172 gDevToolsBrowser.destroy({ shuttingDown: false }); 173 } 174 break; 175 } 176 }, 177 178 _observersRegistered: false, 179 180 /** 181 * This function is for the benefit of Tools:{toolId} commands, 182 * triggered from the WebDeveloper menu and keyboard shortcuts. 183 * 184 * selectToolCommand's behavior: 185 * - if the current page is about:devtools-toolbox 186 * we select the targeted tool 187 * - if the toolbox is closed, 188 * we open the toolbox and select the tool 189 * - if the toolbox is open, and the targeted tool is not selected, 190 * we select it 191 * - if the toolbox is open, and the targeted tool is selected, 192 * and the host is NOT a window, we close the toolbox 193 * - if the toolbox is open, and the targeted tool is selected, 194 * and the host is a window, we raise the toolbox window 195 * 196 * Used when: - registering a new tool 197 * - new xul window, to add menu items 198 */ 199 async selectToolCommand(win, toolId, startTime) { 200 if (gDevToolsBrowser._isAboutDevtoolsToolbox(win)) { 201 const toolbox = gDevToolsBrowser._getAboutDevtoolsToolbox(win); 202 await toolbox.selectTool(toolId, "key_shortcut"); 203 return; 204 } 205 206 const tab = win.gBrowser.selectedTab; 207 const toolbox = gDevTools.getToolboxForTab(tab); 208 const toolDefinition = gDevTools.getToolDefinition(toolId); 209 210 if ( 211 toolbox && 212 (toolbox.currentToolId == toolId || 213 (toolId == "webconsole" && toolbox.splitConsole)) 214 ) { 215 toolbox.fireCustomKey(toolId); 216 217 if ( 218 toolDefinition.preventClosingOnKey || 219 toolbox.hostType == Toolbox.HostType.WINDOW 220 ) { 221 if (!toolDefinition.preventRaisingOnKey) { 222 await toolbox.raise(); 223 } 224 } else { 225 await toolbox.destroy(); 226 } 227 gDevTools.emit("select-tool-command", toolId); 228 } else { 229 await gDevTools 230 .showToolboxForTab(tab, { 231 raise: !toolDefinition.preventRaisingOnKey, 232 startTime, 233 toolId, 234 }) 235 .then(newToolbox => { 236 newToolbox.fireCustomKey(toolId); 237 gDevTools.emit("select-tool-command", toolId); 238 }); 239 } 240 }, 241 242 /** 243 * Called by devtools/client/devtools-startup.js when a key shortcut is pressed 244 * 245 * @param {Window} window 246 * The top level browser window from which the key shortcut is pressed. 247 * @param {object} key 248 * Key object describing the key shortcut being pressed. It comes 249 * from devtools-startup.js's KeyShortcuts array. The useful fields here 250 * are: 251 * - `toolId` used to identify a toolbox's panel like inspector or webconsole, 252 * - `id` used to identify any other key shortcuts like about:debugging 253 * @param {number} startTime 254 * Optional, indicates the time at which the key event fired. This is a 255 * `ChromeUtils.now()` timing. 256 */ 257 async onKeyShortcut(window, key, startTime) { 258 // Avoid to open devtools when the about:devtools-toolbox page is showing 259 // on the window now. 260 if ( 261 gDevToolsBrowser._isAboutDevtoolsToolbox(window) && 262 (key.id === "toggleToolbox" || key.id === "toggleToolboxF12") 263 ) { 264 return; 265 } 266 267 // If this is a toolbox's panel key shortcut, delegate to selectToolCommand 268 if (key.toolId) { 269 await gDevToolsBrowser.selectToolCommand(window, key.toolId, startTime); 270 return; 271 } 272 // Otherwise implement all other key shortcuts individually here 273 switch (key.id) { 274 case "toggleToolbox": 275 gDevToolsBrowser.toggleToolboxCommand(window.gBrowser, startTime); 276 break; 277 case "toggleToolboxF12": 278 if (Services.prefs.getBoolPref(DEVTOOLS_F12_ENABLED_PREF, true)) { 279 gDevToolsBrowser.toggleToolboxCommand(window.gBrowser, startTime); 280 } 281 break; 282 case "browserToolbox": 283 lazy.BrowserToolboxLauncher.init(); 284 break; 285 case "browserConsole": { 286 const { 287 BrowserConsoleManager, 288 } = require("resource://devtools/client/webconsole/browser-console-manager.js"); 289 BrowserConsoleManager.openBrowserConsoleOrFocus(); 290 break; 291 } 292 case "responsiveDesignMode": 293 ResponsiveUIManager.toggle(window, window.gBrowser.selectedTab, { 294 trigger: "shortcut", 295 }); 296 break; 297 case "javascriptTracingToggle": { 298 const toolbox = gDevTools.getToolboxForTab(window.gBrowser.selectedTab); 299 if (!toolbox) { 300 break; 301 } 302 await toolbox.commands.tracerCommand.toggle(); 303 break; 304 } 305 } 306 }, 307 308 /** 309 * Open a tab on "about:debugging", optionally pre-select a given tab. 310 */ 311 // Used by browser-sets.inc, command 312 openAboutDebugging(gBrowser, hash) { 313 const url = "about:debugging" + (hash ? "#" + hash : ""); 314 gBrowser.selectedTab = gBrowser.addTrustedTab(url); 315 }, 316 317 /** 318 * Add the devtools-browser stylesheet to browser window's document. Returns a promise. 319 * 320 * @param {Window} win 321 * The window on which the stylesheet should be added. 322 * @return {Promise} promise that resolves when the stylesheet is loaded (or rejects 323 * if it fails to load). 324 */ 325 loadBrowserStyleSheet(win) { 326 if (this._browserStyleSheets.has(win)) { 327 return Promise.resolve(); 328 } 329 330 const doc = win.document; 331 const { styleSheet, loadPromise } = appendStyleSheet( 332 doc, 333 BROWSER_STYLESHEET_URL 334 ); 335 this._browserStyleSheets.set(win, styleSheet); 336 return loadPromise; 337 }, 338 339 /** 340 * Add this DevTools's presence to a browser window's document 341 * 342 * @param {HTMLDocument} doc 343 * The document to which devtools should be hooked to. 344 */ 345 _registerBrowserWindow(win) { 346 if (gDevToolsBrowser._trackedBrowserWindows.has(win)) { 347 return; 348 } 349 if (!win.document.getElementById("menuWebDeveloperPopup")) { 350 // Menus etc. set up here are browser specific. 351 return; 352 } 353 gDevToolsBrowser._trackedBrowserWindows.add(win); 354 BrowserMenus.addMenus(win.document); 355 356 this.updateCommandAvailability(win); 357 this.updateDevtoolsThemeAttribute(win); 358 if (!this._observersRegistered) { 359 this._observersRegistered = true; 360 Services.prefs.addObserver("devtools.", this); 361 this._onThemeChanged = this._onThemeChanged.bind(this); 362 addThemeObserver(this._onThemeChanged); 363 } 364 365 win.addEventListener("unload", this); 366 367 const tabContainer = win.gBrowser.tabContainer; 368 tabContainer.addEventListener("TabSelect", this); 369 }, 370 371 _onThemeChanged() { 372 for (const win of this._trackedBrowserWindows) { 373 this.updateDevtoolsThemeAttribute(win); 374 } 375 }, 376 377 /** 378 * Add the menuitem for a tool to all open browser windows. 379 * 380 * @param {object} toolDefinition 381 * properties of the tool to add 382 */ 383 _addToolToWindows(toolDefinition) { 384 // No menu item or global shortcut is required for options panel. 385 if (!toolDefinition.inMenu) { 386 return; 387 } 388 389 // Skip if the tool is disabled. 390 try { 391 if ( 392 toolDefinition.visibilityswitch && 393 !Services.prefs.getBoolPref(toolDefinition.visibilityswitch) 394 ) { 395 return; 396 } 397 } catch (e) { 398 // Prevent breaking everything if the pref doesn't exists. 399 } 400 401 // We need to insert the new tool in the right place, which means knowing 402 // the tool that comes before the tool that we're trying to add 403 const allDefs = gDevTools.getToolDefinitionArray(); 404 let prevDef; 405 for (const def of allDefs) { 406 if (!def.inMenu) { 407 continue; 408 } 409 if (def === toolDefinition) { 410 break; 411 } 412 prevDef = def; 413 } 414 415 for (const win of gDevToolsBrowser._trackedBrowserWindows) { 416 BrowserMenus.insertToolMenuElements( 417 win.document, 418 toolDefinition, 419 prevDef 420 ); 421 // If we are on a page where devtools menu items are hidden such as 422 // about:devtools-toolbox, we need to call _updateMenuItems to update the 423 // visibility of the newly created menu item. 424 gDevToolsBrowser._updateMenuItems(win); 425 } 426 }, 427 428 hasToolboxOpened(win) { 429 const tab = win.gBrowser.selectedTab; 430 for (const commands of gDevTools._toolboxesPerCommands.keys()) { 431 if (commands.descriptorFront.localTab == tab) { 432 return true; 433 } 434 } 435 return false; 436 }, 437 438 /** 439 * Update developer tools menu items and the "Toggle Tools" checkbox. This is 440 * called when a toolbox is created or destroyed. 441 */ 442 _updateMenu() { 443 for (const win of gDevToolsBrowser._trackedBrowserWindows) { 444 gDevToolsBrowser._updateMenuItems(win); 445 } 446 }, 447 448 /** 449 * Update developer tools menu items and the "Toggle Tools" checkbox of XULWindow. 450 * 451 * @param {XULWindow} win 452 */ 453 _updateMenuItems(win) { 454 const menu = win.document.getElementById("menu_devToolbox"); 455 456 // Hide the "Toggle Tools" menu item if we are on about:devtools-toolbox. 457 menu.hidden = 458 gDevToolsBrowser._isAboutDevtoolsToolbox(win) || 459 Services.prefs.getBoolPref("devtools.policy.disabled", false); 460 461 // Add a checkmark for the "Toggle Tools" menu item if a toolbox is already opened. 462 const hasToolbox = gDevToolsBrowser.hasToolboxOpened(win); 463 if (hasToolbox) { 464 menu.setAttribute("checked", "true"); 465 } else { 466 menu.removeAttribute("checked"); 467 } 468 }, 469 470 /** 471 * Check whether the window is showing about:devtools-toolbox page or not. 472 * 473 * @param {XULWindow} win 474 * @return {boolean} true: about:devtools-toolbox is showing 475 * false: otherwise 476 */ 477 _isAboutDevtoolsToolbox(win) { 478 const currentURI = win.gBrowser.currentURI; 479 return ( 480 currentURI.scheme === "about" && 481 currentURI.filePath === "devtools-toolbox" 482 ); 483 }, 484 485 /** 486 * Retrieve the Toolbox instance loaded in the current page if the page is 487 * about:devtools-toolbox, null otherwise. 488 * 489 * @param {XULWindow} win 490 * The chrome window containing about:devtools-toolbox. Will match 491 * toolbox.topWindow. 492 * @return {Toolbox} The toolbox instance loaded in about:devtools-toolbox 493 */ 494 _getAboutDevtoolsToolbox(win) { 495 if (!gDevToolsBrowser._isAboutDevtoolsToolbox(win)) { 496 return null; 497 } 498 return gDevTools.getToolboxes().find(toolbox => toolbox.topWindow === win); 499 }, 500 501 /** 502 * Remove the menuitem for a tool to all open browser windows. 503 * 504 * @param {string} toolId 505 * id of the tool to remove 506 */ 507 _removeToolFromWindows(toolId) { 508 for (const win of gDevToolsBrowser._trackedBrowserWindows) { 509 BrowserMenus.removeToolFromMenu(toolId, win.document); 510 } 511 }, 512 513 /** 514 * Called on browser unload to remove menu entries, toolboxes and event 515 * listeners from the closed browser window. 516 * 517 * @param {XULWindow} win 518 * The window containing the menu entry 519 */ 520 _forgetBrowserWindow(win) { 521 if (!gDevToolsBrowser._trackedBrowserWindows.has(win)) { 522 return; 523 } 524 gDevToolsBrowser._trackedBrowserWindows.delete(win); 525 win.removeEventListener("unload", this); 526 527 BrowserMenus.removeMenus(win.document); 528 529 // Destroy toolboxes for closed window 530 for (const [commands, toolbox] of gDevTools._toolboxesPerCommands) { 531 if ( 532 commands.descriptorFront.localTab?.ownerDocument?.defaultView == win 533 ) { 534 toolbox.destroy(); 535 } 536 } 537 538 const styleSheet = this._browserStyleSheets.get(win); 539 if (styleSheet) { 540 styleSheet.remove(); 541 this._browserStyleSheets.delete(win); 542 } 543 544 const tabContainer = win.gBrowser.tabContainer; 545 tabContainer.removeEventListener("TabSelect", this); 546 }, 547 548 handleEvent(event) { 549 switch (event.type) { 550 case "TabSelect": 551 gDevToolsBrowser._updateMenu(); 552 break; 553 case "unload": 554 // top-level browser window unload 555 gDevToolsBrowser._forgetBrowserWindow(event.target.defaultView); 556 break; 557 } 558 }, 559 560 /** 561 * Either the DevTools Loader has been destroyed by the add-on contribution 562 * workflow, or firefox is shutting down. 563 564 * @param {boolean} shuttingDown 565 * True if firefox is currently shutting down. We may prevent doing 566 * some cleanups to speed it up. Otherwise everything need to be 567 * cleaned up in order to be able to load devtools again. 568 */ 569 destroy({ shuttingDown }) { 570 Services.prefs.removeObserver("devtools.", gDevToolsBrowser); 571 removeThemeObserver(this._onThemeChanged); 572 Services.obs.removeObserver( 573 gDevToolsBrowser, 574 "browser-delayed-startup-finished" 575 ); 576 Services.obs.removeObserver(gDevToolsBrowser, "quit-application"); 577 Services.obs.removeObserver(gDevToolsBrowser, "devtools:loader:destroy"); 578 579 for (const win of gDevToolsBrowser._trackedBrowserWindows) { 580 gDevToolsBrowser._forgetBrowserWindow(win); 581 } 582 583 // Remove scripts loaded in content process to support the Browser Content Toolbox. 584 DevToolsServer.removeContentServerScript(); 585 586 gDevTools.destroy({ shuttingDown }); 587 }, 588 }); 589 590 // Handle all already registered tools, 591 gDevTools 592 .getToolDefinitionArray() 593 .forEach(def => gDevToolsBrowser._addToolToWindows(def)); 594 // and the new ones. 595 gDevTools.on("tool-registered", function (toolId) { 596 const toolDefinition = gDevTools._tools.get(toolId); 597 // If the tool has been registered globally, add to all the 598 // available windows. 599 if (toolDefinition) { 600 gDevToolsBrowser._addToolToWindows(toolDefinition); 601 } 602 }); 603 604 gDevTools.on("tool-unregistered", function (toolId) { 605 gDevToolsBrowser._removeToolFromWindows(toolId); 606 }); 607 608 gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenu); 609 gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenu); 610 611 Services.obs.addObserver(gDevToolsBrowser, "quit-application"); 612 Services.obs.addObserver(gDevToolsBrowser, "browser-delayed-startup-finished"); 613 // Watch for module loader unload. Fires when the tools are reloaded. 614 Services.obs.addObserver(gDevToolsBrowser, "devtools:loader:destroy"); 615 616 // Fake end of browser window load event for all already opened windows 617 // that is already fully loaded. 618 for (const win of Services.wm.getEnumerator(gDevTools.chromeWindowType)) { 619 if (win.gBrowserInit?.delayedStartupFinished) { 620 gDevToolsBrowser._registerBrowserWindow(win); 621 } 622 }