toolbox.js (162638B)
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 const MAX_ORDINAL = 99; 8 const SPLITCONSOLE_OPEN_PREF = "devtools.toolbox.splitconsole.open"; 9 const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsole.enabled"; 10 const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight"; 11 const DEVTOOLS_ALWAYS_ON_TOP = "devtools.toolbox.alwaysOnTop"; 12 const DISABLE_AUTOHIDE_PREF = "ui.popup.disable_autohide"; 13 const PSEUDO_LOCALE_PREF = "intl.l10n.pseudo"; 14 const HTML_NS = "http://www.w3.org/1999/xhtml"; 15 const REGEX_4XX_5XX = /^[4,5]\d\d$/; 16 17 const BROWSERTOOLBOX_SCOPE_PREF = "devtools.browsertoolbox.scope"; 18 const BROWSERTOOLBOX_SCOPE_EVERYTHING = "everything"; 19 const BROWSERTOOLBOX_SCOPE_PARENTPROCESS = "parent-process"; 20 21 const { debounce } = require("resource://devtools/shared/debounce.js"); 22 const { throttle } = require("resource://devtools/shared/throttle.js"); 23 const { 24 safeAsyncMethod, 25 } = require("resource://devtools/shared/async-utils.js"); 26 var { gDevTools } = require("resource://devtools/client/framework/devtools.js"); 27 var EventEmitter = require("resource://devtools/shared/event-emitter.js"); 28 const Selection = require("resource://devtools/client/framework/selection.js"); 29 var Telemetry = require("resource://devtools/client/shared/telemetry.js"); 30 const { 31 getUnicodeUrl, 32 } = require("resource://devtools/client/shared/unicode-url.js"); 33 var { DOMHelpers } = require("resource://devtools/shared/dom-helpers.js"); 34 const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js"); 35 const { 36 FluentL10n, 37 } = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js"); 38 const { 39 START_IGNORE_ACTION, 40 } = require("resource://devtools/client/shared/redux/middleware/ignore.js"); 41 42 var Startup = Cc["@mozilla.org/devtools/startup-clh;1"].getService( 43 Ci.nsISupports 44 ).wrappedJSObject; 45 46 const { BrowserLoader } = ChromeUtils.importESModule( 47 "resource://devtools/shared/loader/browser-loader.sys.mjs" 48 ); 49 50 const { 51 MultiLocalizationHelper, 52 } = require("resource://devtools/shared/l10n.js"); 53 const L10N = new MultiLocalizationHelper( 54 "devtools/client/locales/toolbox.properties", 55 "chrome://branding/locale/brand.properties", 56 "devtools/client/locales/menus.properties" 57 ); 58 59 loader.lazyRequireGetter( 60 this, 61 "registerStoreObserver", 62 "resource://devtools/client/shared/redux/subscriber.js", 63 true 64 ); 65 loader.lazyRequireGetter( 66 this, 67 "createToolboxStore", 68 "resource://devtools/client/framework/store.js", 69 true 70 ); 71 loader.lazyRequireGetter( 72 this, 73 ["registerWalkerListeners", "removeTarget"], 74 "resource://devtools/client/framework/actions/index.js", 75 true 76 ); 77 loader.lazyRequireGetter( 78 this, 79 ["selectTarget"], 80 "resource://devtools/shared/commands/target/actions/targets.js", 81 true 82 ); 83 loader.lazyRequireGetter( 84 this, 85 "TRACER_LOG_METHODS", 86 "resource://devtools/shared/specs/tracer.js", 87 true 88 ); 89 90 const lazy = {}; 91 ChromeUtils.defineESModuleGetters(lazy, { 92 AppConstants: "resource://gre/modules/AppConstants.sys.mjs", 93 ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", 94 TYPES: "resource://devtools/shared/highlighters.mjs", 95 }); 96 loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js"); 97 loader.lazyRequireGetter( 98 this, 99 "KeyShortcuts", 100 "resource://devtools/client/shared/key-shortcuts.js" 101 ); 102 loader.lazyRequireGetter( 103 this, 104 "ZoomKeys", 105 "resource://devtools/client/shared/zoom-keys.js" 106 ); 107 loader.lazyRequireGetter( 108 this, 109 "ToolboxButtons", 110 "resource://devtools/client/definitions.js", 111 true 112 ); 113 loader.lazyRequireGetter( 114 this, 115 "SourceMapURLService", 116 "resource://devtools/client/framework/source-map-url-service.js", 117 true 118 ); 119 loader.lazyRequireGetter( 120 this, 121 "BrowserConsoleManager", 122 "resource://devtools/client/webconsole/browser-console-manager.js", 123 true 124 ); 125 loader.lazyRequireGetter( 126 this, 127 "viewSource", 128 "resource://devtools/client/shared/view-source.js" 129 ); 130 loader.lazyRequireGetter( 131 this, 132 "buildHarLog", 133 "resource://devtools/client/netmonitor/src/har/har-builder-utils.js", 134 true 135 ); 136 loader.lazyRequireGetter( 137 this, 138 "NetMonitorAPI", 139 "resource://devtools/client/netmonitor/src/api.js", 140 true 141 ); 142 loader.lazyRequireGetter( 143 this, 144 "sortPanelDefinitions", 145 "resource://devtools/client/framework/toolbox-tabs-order-manager.js", 146 true 147 ); 148 loader.lazyRequireGetter( 149 this, 150 "createEditContextMenu", 151 "resource://devtools/client/framework/toolbox-context-menu.js", 152 true 153 ); 154 loader.lazyRequireGetter( 155 this, 156 "getSelectedTarget", 157 "resource://devtools/shared/commands/target/selectors/targets.js", 158 true 159 ); 160 loader.lazyRequireGetter( 161 this, 162 "remoteClientManager", 163 "resource://devtools/client/shared/remote-debugging/remote-client-manager.js", 164 true 165 ); 166 loader.lazyRequireGetter( 167 this, 168 "ResponsiveUIManager", 169 "resource://devtools/client/responsive/manager.js" 170 ); 171 loader.lazyRequireGetter( 172 this, 173 "DevToolsUtils", 174 "resource://devtools/shared/DevToolsUtils.js" 175 ); 176 loader.lazyRequireGetter( 177 this, 178 "NodePicker", 179 "resource://devtools/client/inspector/node-picker.js" 180 ); 181 182 loader.lazyGetter(this, "domNodeConstants", () => { 183 return require("resource://devtools/shared/dom-node-constants.js"); 184 }); 185 186 loader.lazyRequireGetter( 187 this, 188 "NodeFront", 189 "resource://devtools/client/fronts/node.js", 190 true 191 ); 192 193 loader.lazyRequireGetter( 194 this, 195 "PICKER_TYPES", 196 "resource://devtools/shared/picker-constants.js" 197 ); 198 199 loader.lazyRequireGetter( 200 this, 201 "HarAutomation", 202 "resource://devtools/client/netmonitor/src/har/har-automation.js", 203 true 204 ); 205 206 loader.lazyRequireGetter( 207 this, 208 "getThreadOptions", 209 "resource://devtools/client/shared/thread-utils.js", 210 true 211 ); 212 loader.lazyRequireGetter( 213 this, 214 "SourceMapLoader", 215 "resource://devtools/client/shared/source-map-loader/index.js", 216 true 217 ); 218 loader.lazyRequireGetter( 219 this, 220 "openProfilerTab", 221 "resource://devtools/client/performance-new/shared/browser.js", 222 true 223 ); 224 loader.lazyGetter(this, "ProfilerBackground", () => { 225 return ChromeUtils.importESModule( 226 "resource://devtools/client/performance-new/shared/background.sys.mjs" 227 ); 228 }); 229 230 const BOOLEAN_CONFIGURATION_PREFS = { 231 "devtools.cache.disabled": { 232 name: "cacheDisabled", 233 }, 234 "devtools.custom-formatters.enabled": { 235 name: "customFormatters", 236 }, 237 "devtools.serviceWorkers.testing.enabled": { 238 name: "serviceWorkersTestingEnabled", 239 }, 240 "devtools.inspector.simple-highlighters-reduced-motion": { 241 name: "useSimpleHighlightersForReducedMotion", 242 }, 243 "devtools.debugger.features.overlay": { 244 name: "pauseOverlay", 245 thread: true, 246 }, 247 "devtools.debugger.features.javascript-tracing": { 248 name: "isTracerFeatureEnabled", 249 }, 250 }; 251 exports.BOOLEAN_CONFIGURATION_PREFS = BOOLEAN_CONFIGURATION_PREFS; 252 253 /** 254 * A "Toolbox" is the component that holds all the tools for one specific 255 * target. Visually, it's a document that includes the tools tabs and all 256 * the iframes where the tool panels will be living in. 257 */ 258 class Toolbox extends EventEmitter { 259 /** 260 * @param {object} options 261 * @param {object} options.commands 262 * The context to inspect identified by this commands. 263 * @param {string} options.selectedTool 264 * Tool to select initially 265 * @param {object} options.selectedToolOptions 266 * Object that will be passed to the panel init function 267 * @param {Toolbox.HostType} options.hostType 268 * Type of host that will host the toolbox (e.g. sidebar, window) 269 * @param {DOMWindow} options.contentWindow 270 * The window object of the toolbox document 271 * @param {string} options.frameId 272 * A unique identifier to differentiate toolbox documents from the 273 * chrome codebase when passing DOM messages 274 */ 275 constructor({ 276 commands, 277 selectedTool, 278 selectedToolOptions, 279 hostType, 280 contentWindow, 281 frameId, 282 }) { 283 super(); 284 285 this._win = contentWindow; 286 this.frameId = frameId; 287 this.selection = new Selection(); 288 this.telemetry = new Telemetry({ useSessionId: true }); 289 // This attribute helps identify one particular toolbox instance. 290 this.sessionId = this.telemetry.sessionId; 291 292 // This attribute is meant to be a public attribute on the Toolbox object 293 // It exposes commands modules listed in devtools/shared/commands/index.js 294 // which are an abstraction on top of RDP methods. 295 // See devtools/shared/commands/README.md 296 this.commands = commands; 297 this._descriptorFront = commands.descriptorFront; 298 299 // Map of the available DevTools WebExtensions: 300 // Map<extensionUUID, extensionName> 301 this._webExtensions = new Map(); 302 303 this._toolPanels = new Map(); 304 this._inspectorExtensionSidebars = new Map(); 305 306 this._netMonitorAPI = null; 307 308 // Map of frames (id => frame-info) and currently selected frame id. 309 this.frameMap = new Map(); 310 this.selectedFrameId = null; 311 312 // Number of targets currently paused 313 this._pausedTargets = new Set(); 314 315 /** 316 * KeyShortcuts instance specific to WINDOW host type. 317 * This is the key shortcuts that are only register when the toolbox 318 * is loaded in its own window. Otherwise, these shortcuts are typically 319 * registered by devtools-startup.js module. 320 */ 321 this._windowHostShortcuts = null; 322 323 // List of currently displayed panel's iframes 324 this._visibleIframes = new Set(); 325 326 this._toolRegistered = this._toolRegistered.bind(this); 327 this._toolUnregistered = this._toolUnregistered.bind(this); 328 this._refreshHostTitle = this._refreshHostTitle.bind(this); 329 this.toggleNoAutohide = this.toggleNoAutohide.bind(this); 330 this.toggleAlwaysOnTop = this.toggleAlwaysOnTop.bind(this); 331 this.disablePseudoLocale = () => this.changePseudoLocale("none"); 332 this.enableAccentedPseudoLocale = () => this.changePseudoLocale("accented"); 333 this.enableBidiPseudoLocale = () => this.changePseudoLocale("bidi"); 334 this._updateFrames = this._updateFrames.bind(this); 335 this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this); 336 this.closeToolbox = this.closeToolbox.bind(this); 337 this.destroy = this.destroy.bind(this); 338 this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this); 339 this._onFocus = this._onFocus.bind(this); 340 this._onBlur = this._onBlur.bind(this); 341 this._onBrowserMessage = this._onBrowserMessage.bind(this); 342 this._onTabsOrderUpdated = this._onTabsOrderUpdated.bind(this); 343 this._onToolbarFocus = this._onToolbarFocus.bind(this); 344 this._onToolbarArrowKeypress = this._onToolbarArrowKeypress.bind(this); 345 this._onPickerClick = this._onPickerClick.bind(this); 346 this._onPickerKeypress = this._onPickerKeypress.bind(this); 347 this._onPickerStarting = this._onPickerStarting.bind(this); 348 this._onPickerStarted = this._onPickerStarted.bind(this); 349 this._onPickerStopped = this._onPickerStopped.bind(this); 350 this._onPickerCanceled = this._onPickerCanceled.bind(this); 351 this._onPickerPicked = this._onPickerPicked.bind(this); 352 this._onPickerPreviewed = this._onPickerPreviewed.bind(this); 353 this._onInspectObject = this._onInspectObject.bind(this); 354 this._onNewSelectedNodeFront = this._onNewSelectedNodeFront.bind(this); 355 this._onToolSelected = this._onToolSelected.bind(this); 356 this._onContextMenu = this._onContextMenu.bind(this); 357 this._onMouseDown = this._onMouseDown.bind(this); 358 this.updateToolboxButtonsVisibility = 359 this.updateToolboxButtonsVisibility.bind(this); 360 this.updateToolboxButtons = this.updateToolboxButtons.bind(this); 361 this.selectTool = this.selectTool.bind(this); 362 this._pingTelemetrySelectTool = this._pingTelemetrySelectTool.bind(this); 363 this.toggleSplitConsole = this.toggleSplitConsole.bind(this); 364 this.toggleOptions = this.toggleOptions.bind(this); 365 this._onTargetAvailable = this._onTargetAvailable.bind(this); 366 this._onTargetDestroyed = this._onTargetDestroyed.bind(this); 367 this._onTargetSelected = this._onTargetSelected.bind(this); 368 this._onResourceAvailable = this._onResourceAvailable.bind(this); 369 this._onResourceUpdated = this._onResourceUpdated.bind(this); 370 this._onToolSelectedStopPicker = this._onToolSelectedStopPicker.bind(this); 371 372 // `component` might be null if the toolbox was destroying during the throttling 373 this._throttledSetToolboxButtons = throttle( 374 () => this.component?.setToolboxButtons(this.toolbarButtons), 375 500, 376 this 377 ); 378 379 this._debounceUpdateFocusedState = debounce( 380 () => { 381 this.component?.setFocusedState(this._isToolboxFocused); 382 }, 383 500, 384 this 385 ); 386 387 if (!selectedTool) { 388 selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); 389 } 390 this._defaultToolId = selectedTool; 391 this._defaultToolOptions = selectedToolOptions; 392 393 this._hostType = hostType; 394 395 this.isOpen = new Promise( 396 function (resolve) { 397 this._resolveIsOpen = resolve; 398 }.bind(this) 399 ); 400 401 this.on("host-changed", this._refreshHostTitle); 402 this.on("select", this._onToolSelected); 403 404 this.selection.on("new-node-front", this._onNewSelectedNodeFront); 405 406 gDevTools.on("tool-registered", this._toolRegistered); 407 gDevTools.on("tool-unregistered", this._toolUnregistered); 408 409 /** 410 * Get text direction for the current locale direction. 411 * 412 * `getComputedStyle` forces a synchronous reflow, so use a lazy getter in order to 413 * call it only once. 414 */ 415 loader.lazyGetter(this, "direction", () => { 416 const { documentElement } = this.doc; 417 const isRtl = 418 this.win.getComputedStyle(documentElement).direction === "rtl"; 419 return isRtl ? "rtl" : "ltr"; 420 }); 421 } 422 423 /** 424 * The toolbox can be 'hosted' either embedded in a browser window 425 * or in a separate window. 426 */ 427 static HostType = { 428 BOTTOM: "bottom", 429 RIGHT: "right", 430 LEFT: "left", 431 WINDOW: "window", 432 BROWSERTOOLBOX: "browsertoolbox", 433 // This is typically used by `about:debugging`, when opening toolbox in a new tab, 434 // via `about:devtools-toolbox` URLs. 435 PAGE: "page", 436 }; 437 438 _URL = "about:devtools-toolbox"; 439 440 _prefs = { 441 LAST_TOOL: "devtools.toolbox.selectedTool", 442 }; 443 444 get nodePicker() { 445 if (!this._nodePicker) { 446 this._nodePicker = new NodePicker(this.commands, this.selection); 447 this._nodePicker.on("picker-starting", this._onPickerStarting); 448 this._nodePicker.on("picker-started", this._onPickerStarted); 449 this._nodePicker.on("picker-stopped", this._onPickerStopped); 450 this._nodePicker.on("picker-node-canceled", this._onPickerCanceled); 451 this._nodePicker.on("picker-node-picked", this._onPickerPicked); 452 this._nodePicker.on("picker-node-previewed", this._onPickerPreviewed); 453 } 454 455 return this._nodePicker; 456 } 457 458 get store() { 459 if (!this._store) { 460 this._store = createToolboxStore(); 461 } 462 return this._store; 463 } 464 465 get currentToolId() { 466 return this._currentToolId; 467 } 468 469 set currentToolId(id) { 470 this._currentToolId = id; 471 this.component.setCurrentToolId(id); 472 } 473 474 get defaultToolId() { 475 return this._defaultToolId; 476 } 477 478 get panelDefinitions() { 479 return this._panelDefinitions; 480 } 481 482 set panelDefinitions(definitions) { 483 this._panelDefinitions = definitions; 484 this._combineAndSortPanelDefinitions(); 485 } 486 487 get visibleAdditionalTools() { 488 if (!this._visibleAdditionalTools) { 489 this._visibleAdditionalTools = []; 490 } 491 492 return this._visibleAdditionalTools; 493 } 494 495 set visibleAdditionalTools(tools) { 496 this._visibleAdditionalTools = tools; 497 if (this.isReady) { 498 this._combineAndSortPanelDefinitions(); 499 } 500 } 501 502 /** 503 * Combines the built-in panel definitions and the additional tool definitions that 504 * can be set by add-ons. 505 */ 506 _combineAndSortPanelDefinitions() { 507 let definitions = [ 508 ...this._panelDefinitions, 509 ...this.getVisibleAdditionalTools(), 510 ]; 511 definitions = sortPanelDefinitions(definitions); 512 this.component.setPanelDefinitions(definitions); 513 } 514 515 lastUsedToolId = null; 516 517 /** 518 * Returns a *copy* of the _toolPanels collection. 519 * 520 * @return {Map} panels 521 * All the running panels in the toolbox 522 */ 523 getToolPanels() { 524 return new Map(this._toolPanels); 525 } 526 527 /** 528 * Access the panel for a given tool 529 */ 530 getPanel(id) { 531 return this._toolPanels.get(id); 532 } 533 534 /** 535 * Get the panel instance for a given tool once it is ready. 536 * If the tool is already opened, the promise will resolve immediately, 537 * otherwise it will wait until the tool has been opened before resolving. 538 * 539 * Note that this does not open the tool, use selectTool if you'd 540 * like to select the tool right away. 541 * 542 * @param {string} id 543 * The id of the panel, for example "jsdebugger". 544 * @returns Promise 545 * A promise that resolves once the panel is ready. 546 */ 547 getPanelWhenReady(id) { 548 const panel = this.getPanel(id); 549 return new Promise(resolve => { 550 if (panel) { 551 resolve(panel); 552 } else { 553 this.on(id + "-ready", initializedPanel => { 554 resolve(initializedPanel); 555 }); 556 } 557 }); 558 } 559 560 /** 561 * This is a shortcut for getPanel(currentToolId) because it is much more 562 * likely that we're going to want to get the panel that we've just made 563 * visible 564 */ 565 getCurrentPanel() { 566 return this._toolPanels.get(this.currentToolId); 567 } 568 569 /** 570 * Get the current top level target the toolbox is debugging. 571 * 572 * This will only be defined *after* calling Toolbox.open(), 573 * after it has called `targetCommands.startListening`. 574 */ 575 get target() { 576 return this.commands.targetCommand.targetFront; 577 } 578 579 get threadFront() { 580 return this.commands.targetCommand.targetFront.threadFront; 581 } 582 583 /** 584 * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate 585 * tab. See HostType for more details. 586 */ 587 get hostType() { 588 return this._hostType; 589 } 590 591 /** 592 * Shortcut to the window containing the toolbox UI 593 */ 594 get win() { 595 return this._win; 596 } 597 598 /** 599 * When the toolbox is loaded in a frame with type="content", win.parent will not return 600 * the parent Chrome window. This getter should return the parent Chrome window 601 * regardless of the frame type. See Bug 1539979. 602 */ 603 get topWindow() { 604 return DevToolsUtils.getTopWindow(this.win); 605 } 606 607 get topDoc() { 608 return this.topWindow.document; 609 } 610 611 /** 612 * Shortcut to the document containing the toolbox UI 613 */ 614 get doc() { 615 return this.win.document; 616 } 617 618 /** 619 * Get the toggled state of the split console 620 */ 621 get splitConsole() { 622 return this._splitConsole; 623 } 624 625 /** 626 * Get the focused state of the split console 627 */ 628 isSplitConsoleFocused() { 629 if (!this._splitConsole) { 630 return false; 631 } 632 const focusedWin = Services.focus.focusedWindow; 633 return ( 634 focusedWin && 635 focusedWin === 636 this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow 637 ); 638 } 639 640 /** 641 * Get the enabled split console setting, and if it's not set, set it with updateIsSplitConsoleEnabled 642 * 643 * @returns {boolean} devtools.toolbox.splitconsole.enabled option 644 */ 645 isSplitConsoleEnabled() { 646 if (typeof this._splitConsoleEnabled !== "boolean") { 647 this.updateIsSplitConsoleEnabled(); 648 } 649 650 return this._splitConsoleEnabled; 651 } 652 653 get isBrowserToolbox() { 654 return this.hostType === Toolbox.HostType.BROWSERTOOLBOX; 655 } 656 657 get isMultiProcessBrowserToolbox() { 658 return this.isBrowserToolbox; 659 } 660 661 /** 662 * Set a given target as selected (which may impact the console evaluation context selector). 663 * 664 * @param {string} targetActorID: The actorID of the target we want to select. 665 */ 666 selectTarget(targetActorID) { 667 if (this.getSelectedTargetFront()?.actorID !== targetActorID) { 668 // The selected target is managed by the TargetCommand's store. 669 // So dispatch this action against that other store. 670 this.commands.targetCommand.store.dispatch(selectTarget(targetActorID)); 671 } 672 } 673 674 /** 675 * @returns {ThreadFront|null} The selected thread front, or null if there is none. 676 */ 677 getSelectedTargetFront() { 678 // The selected target is managed by the TargetCommand's store. 679 // So pull the state from that other store. 680 const selectedTarget = getSelectedTarget( 681 this.commands.targetCommand.store.getState() 682 ); 683 if (!selectedTarget) { 684 return null; 685 } 686 687 return this.commands.client.getFrontByID(selectedTarget.actorID); 688 } 689 690 /** 691 * For now, the debugger isn't hooked to TargetCommand's store 692 * to display its thread list. So manually forward target selection change 693 * to the debugger via a dedicated action 694 */ 695 _onTargetCommandStateChange(state, oldState) { 696 if (getSelectedTarget(state) !== getSelectedTarget(oldState)) { 697 const dbg = this.getPanel("jsdebugger"); 698 if (!dbg) { 699 return; 700 } 701 702 const threadActorID = getSelectedTarget(state)?.threadFront?.actorID; 703 if (!threadActorID) { 704 return; 705 } 706 707 dbg.selectThread(threadActorID); 708 } 709 } 710 711 /** 712 * Called on each new THREAD_STATE resource 713 * 714 * @param {object} resource The THREAD_STATE resource 715 */ 716 _onThreadStateChanged(resource) { 717 if (resource.state == "paused") { 718 this._onTargetPaused(resource.targetFront, resource.why.type); 719 } else if (resource.state == "resumed") { 720 this._onTargetResumed(resource.targetFront); 721 } 722 } 723 724 /** 725 * This listener is called by TracerCommand, sooner than the JSTRACER_STATE resource. 726 * This is called when the frontend toggles the tracer, before the server started interpreting the request. 727 * This allows to open the console before we start receiving traces. 728 */ 729 async onTracerToggled() { 730 const { tracerCommand } = this.commands; 731 if (!tracerCommand.isTracingEnabled) { 732 return; 733 } 734 const { logMethod } = this.commands.tracerCommand.getTracingOptions(); 735 if ( 736 logMethod == TRACER_LOG_METHODS.CONSOLE && 737 this.currentToolId !== "webconsole" 738 ) { 739 await this.openSplitConsole({ focusConsoleInput: false }); 740 } else if (logMethod == TRACER_LOG_METHODS.DEBUGGER_SIDEBAR) { 741 const panel = await this.selectTool("jsdebugger"); 742 panel.showTracerSidebar(); 743 } 744 } 745 746 /** 747 * Called on each new JSTRACER_STATE resource 748 * 749 * @param {object} resource The JSTRACER_STATE resource 750 */ 751 async _onTracingStateChanged(resource) { 752 const { profile } = resource; 753 if (!profile) { 754 return; 755 } 756 const browser = await openProfilerTab({ defaultPanel: "stack-chart" }); 757 758 const profileCaptureResult = { 759 type: "SUCCESS", 760 profile, 761 }; 762 ProfilerBackground.registerProfileCaptureForBrowser( 763 browser, 764 profileCaptureResult, 765 null 766 ); 767 } 768 769 /** 770 * Called whenever a given target got its execution paused. 771 * 772 * Be careful, this method is synchronous, but highlightTool, raise, selectTool 773 * are all async. 774 * 775 * @param {TargetFront} targetFront 776 * @param {string} reason 777 * Reason why the execution paused 778 */ 779 _onTargetPaused(targetFront, reason) { 780 // Suppress interrupted events by default because the thread is 781 // paused/resumed a lot for various actions. 782 if (reason === "interrupted") { 783 return; 784 } 785 786 this.highlightTool("jsdebugger"); 787 788 if ( 789 reason === "debuggerStatement" || 790 reason === "mutationBreakpoint" || 791 reason === "eventBreakpoint" || 792 reason === "breakpoint" || 793 reason === "exception" || 794 reason === "resumeLimit" || 795 reason === "XHR" || 796 reason === "breakpointConditionThrown" 797 ) { 798 this.raise(); 799 this.selectTool("jsdebugger", reason); 800 // Each Target/Thread can be paused only once at a time, 801 // so, for each pause, we should have a related resumed event. 802 // But we may have multiple targets paused at the same time 803 this._pausedTargets.add(targetFront); 804 this.emit("toolbox-paused"); 805 } 806 } 807 808 /** 809 * Called whenever a given target got its execution resumed. 810 * 811 * @param {TargetFront} targetFront 812 */ 813 _onTargetResumed(targetFront) { 814 if (this.isHighlighted("jsdebugger")) { 815 this._pausedTargets.delete(targetFront); 816 if (this._pausedTargets.size == 0) { 817 this.emit("toolbox-resumed"); 818 this.unhighlightTool("jsdebugger"); 819 } 820 } 821 } 822 823 /** 824 * This method will be called for the top-level target, as well as any potential 825 * additional targets we may care about. 826 */ 827 async _onTargetAvailable({ targetFront, isTargetSwitching }) { 828 if (targetFront.isTopLevel) { 829 // Attach to a new top-level target. 830 // For now, register these event listeners only on the top level target 831 if (!targetFront.targetForm.ignoreSubFrames) { 832 targetFront.on("frame-update", this._updateFrames); 833 } 834 const consoleFront = await targetFront.getFront("console"); 835 consoleFront.on("inspectObject", this._onInspectObject); 836 } 837 838 // Walker listeners allow to monitor DOM Mutation breakpoint updates. 839 // All targets should be monitored. 840 targetFront.watchFronts("inspector", async inspectorFront => { 841 registerWalkerListeners(this.store, inspectorFront.walker); 842 }); 843 844 if (targetFront.isTopLevel && isTargetSwitching) { 845 // These methods expect the target to be attached, which is guaranteed by the time 846 // _onTargetAvailable is called by the targetCommand. 847 await this._listFrames(); 848 // The target may have been destroyed while calling _listFrames if we navigate quickly 849 if (targetFront.isDestroyed()) { 850 return; 851 } 852 } 853 854 if (targetFront.targetForm.ignoreSubFrames) { 855 this._updateFrames({ 856 frames: [ 857 { 858 id: targetFront.actorID, 859 targetFront, 860 url: targetFront.url, 861 title: targetFront.title, 862 isTopLevel: targetFront.isTopLevel, 863 }, 864 ], 865 }); 866 } 867 868 // If a new popup is debugged, automagically switch the toolbox to become 869 // an independant window so that we can easily keep debugging the new tab. 870 // Only do that if that's not the current top level, otherwise it means 871 // we opened a toolbox dedicated to the popup. 872 if ( 873 targetFront.targetForm.isPopup && 874 !targetFront.isTopLevel && 875 this._descriptorFront.isLocalTab 876 ) { 877 await this.switchHostToTab(targetFront.targetForm.browsingContextID); 878 } 879 } 880 881 async _onTargetSelected({ targetFront }) { 882 this._updateFrames({ selected: targetFront.actorID }); 883 this.selectTarget(targetFront.actorID); 884 this._refreshHostTitle(); 885 } 886 887 _onTargetDestroyed({ targetFront }) { 888 removeTarget(this.store, targetFront); 889 890 if (targetFront.isTopLevel) { 891 const consoleFront = targetFront.getCachedFront("console"); 892 // If the target has already been destroyed, its console front will 893 // also already be destroyed and so we won't be able to retrieve it. 894 // Nor is it important to clear its listener as fronts automatically clears 895 // all their listeners on destroy. 896 if (consoleFront) { 897 consoleFront.off("inspectObject", this._onInspectObject); 898 } 899 targetFront.off("frame-update", this._updateFrames); 900 } else if (this.selection) { 901 this.selection.onTargetDestroyed(targetFront); 902 } 903 904 // When navigating the old (top level) target can get destroyed before the thread state changed 905 // event for the target is received, so it gets lost. This currently happens with bf-cache 906 // navigations when paused, so lets make sure we resumed if not. 907 // 908 // We should also resume if a paused non-top-level target is destroyed 909 if (targetFront.isTopLevel || this._pausedTargets.has(targetFront)) { 910 this._onTargetResumed(targetFront); 911 } 912 913 if (targetFront.targetForm.ignoreSubFrames) { 914 this._updateFrames({ 915 frames: [ 916 { 917 // The Target Front may already be destroyed and `actorID` be null. 918 id: targetFront.persistedActorID, 919 destroy: true, 920 }, 921 ], 922 }); 923 } 924 } 925 926 _onTargetThreadFrontResumeWrongOrder() { 927 const box = this.getNotificationBox(); 928 box.appendNotification( 929 L10N.getStr("toolbox.resumeOrderWarning"), 930 "wrong-resume-order", 931 "", 932 box.PRIORITY_WARNING_HIGH 933 ); 934 } 935 936 /** 937 * Open the toolbox 938 */ 939 async open() { 940 try { 941 const isToolboxURL = this.win.location.href.startsWith(this._URL); 942 if (isToolboxURL) { 943 // Update the URL so that onceDOMReady watch for the right url. 944 this._URL = this.win.location.href; 945 } 946 947 // Mount toolbox React components and update all its state that can be updated synchronously. 948 this.onReactLoaded = this._initializeReactComponent(); 949 950 this.commands.targetCommand.on( 951 "target-thread-wrong-order-on-resume", 952 this._onTargetThreadFrontResumeWrongOrder.bind(this) 953 ); 954 registerStoreObserver( 955 this.commands.targetCommand.store, 956 this._onTargetCommandStateChange.bind(this) 957 ); 958 959 // Optimization: fire up a few other things before waiting on 960 // the iframe being ready (makes startup faster) 961 await this.commands.targetCommand.startListening(); 962 963 // Transfer settings early, before watching resources as it may impact them. 964 // (this is the case for custom formatter pref and console messages) 965 await this._listenAndApplyConfigurationPref(); 966 967 // The targetCommand is created right before this code. 968 // It means that this call to watchTargets is the first, 969 // and we are registering the first target listener, which means 970 // Toolbox._onTargetAvailable will be called first, before any other 971 // onTargetAvailable listener that might be registered on targetCommand. 972 await this.commands.targetCommand.watchTargets({ 973 types: this.commands.targetCommand.ALL_TYPES, 974 onAvailable: this._onTargetAvailable, 975 onSelected: this._onTargetSelected, 976 onDestroyed: this._onTargetDestroyed, 977 }); 978 979 const watchedResources = [ 980 // Watch for console API messages, errors and network events in order to populate 981 // the error count icon in the toolbox. 982 this.commands.resourceCommand.TYPES.CONSOLE_MESSAGE, 983 this.commands.resourceCommand.TYPES.ERROR_MESSAGE, 984 this.commands.resourceCommand.TYPES.DOCUMENT_EVENT, 985 this.commands.resourceCommand.TYPES.THREAD_STATE, 986 ]; 987 988 let tracerInitialization; 989 if ( 990 Services.prefs.getBoolPref( 991 "devtools.debugger.features.javascript-tracing", 992 false 993 ) 994 ) { 995 watchedResources.push( 996 this.commands.resourceCommand.TYPES.JSTRACER_STATE 997 ); 998 tracerInitialization = this.commands.tracerCommand.initialize(); 999 this.onTracerToggled = this.onTracerToggled.bind(this); 1000 this.commands.tracerCommand.on("toggle", this.onTracerToggled); 1001 } 1002 1003 if (!this.isBrowserToolbox) { 1004 // Independently of watching network event resources for the error count icon, 1005 // we need to start tracking network activity on toolbox open for targets such 1006 // as tabs, in order to ensure there is always at least one listener existing 1007 // for network events across the lifetime of the various panels, so stopping 1008 // the resource command from clearing out its cache of network event resources. 1009 watchedResources.push( 1010 this.commands.resourceCommand.TYPES.NETWORK_EVENT 1011 ); 1012 } 1013 1014 const onResourcesWatched = this.commands.resourceCommand.watchResources( 1015 watchedResources, 1016 { 1017 onAvailable: this._onResourceAvailable, 1018 onUpdated: this._onResourceUpdated, 1019 } 1020 ); 1021 1022 await this.onReactLoaded; 1023 1024 this.isReady = true; 1025 1026 const framesPromise = this._listFrames(); 1027 1028 Services.prefs.addObserver( 1029 BROWSERTOOLBOX_SCOPE_PREF, 1030 this._refreshHostTitle 1031 ); 1032 1033 this._buildDockOptions(); 1034 this._buildInitialPanelDefinitions(); 1035 this._setDebugTargetData(); 1036 1037 this._addWindowListeners(); 1038 this._addChromeEventHandlerEvents(); 1039 1040 // Get the tab bar of the ToolboxController to attach the "keypress" event listener to. 1041 this._tabBar = this.doc.querySelector(".devtools-tabbar"); 1042 this._tabBar.addEventListener("keypress", this._onToolbarArrowKeypress); 1043 1044 this._componentMount.setAttribute( 1045 "aria-label", 1046 L10N.getStr("toolbox.label") 1047 ); 1048 1049 this.webconsolePanel = this.doc.querySelector( 1050 "#toolbox-panel-webconsole" 1051 ); 1052 this.doc 1053 .getElementById("toolbox-console-splitter") 1054 .addEventListener("command", this._saveSplitConsoleHeight); 1055 1056 this._buildButtons(); 1057 1058 this._pingTelemetry(); 1059 1060 // The isToolSupported check needs to happen after the target is 1061 // remoted, otherwise we could have done it in the toolbox constructor 1062 // (bug 1072764). 1063 const toolDef = gDevTools.getToolDefinition(this._defaultToolId); 1064 if (!toolDef || !toolDef.isToolSupported(this)) { 1065 this._defaultToolId = "webconsole"; 1066 } 1067 1068 // Update all ToolboxController state that can only be done asynchronously 1069 await this._setInitialMeatballState(); 1070 1071 // Start rendering the toolbox toolbar before selecting the tool, as the tools 1072 // can take a few hundred milliseconds seconds to start up. 1073 // 1074 // Delay React rendering as Toolbox.open is synchronous. 1075 // Even if this involve promises, it is synchronous. Toolbox.open already loads 1076 // react modules and freeze the event loop for a significant time. 1077 // requestIdleCallback allows releasing it to allow user events to be processed. 1078 // Use 16ms maximum delay to allow one frame to be rendered at 60FPS 1079 // (1000ms/60FPS=16ms) 1080 this.win.requestIdleCallback( 1081 () => { 1082 this.component.setCanRender(); 1083 }, 1084 { timeout: 16 } 1085 ); 1086 1087 await this.selectTool( 1088 this._defaultToolId, 1089 "initial_panel", 1090 this._defaultToolOptions 1091 ); 1092 1093 // Wait until the original tool is selected so that the split 1094 // console input will receive focus. 1095 let splitConsolePromise = Promise.resolve(); 1096 if (Services.prefs.getBoolPref(SPLITCONSOLE_OPEN_PREF)) { 1097 splitConsolePromise = this.openSplitConsole(); 1098 this.telemetry.addEventProperty( 1099 this.topWindow, 1100 "open", 1101 "tools", 1102 null, 1103 "splitconsole", 1104 true 1105 ); 1106 } else { 1107 this.telemetry.addEventProperty( 1108 this.topWindow, 1109 "open", 1110 "tools", 1111 null, 1112 "splitconsole", 1113 false 1114 ); 1115 } 1116 1117 await Promise.all([ 1118 splitConsolePromise, 1119 framesPromise, 1120 onResourcesWatched, 1121 tracerInitialization, 1122 ]); 1123 1124 // We do not expect the focus to be restored when using about:debugging toolboxes 1125 // Otherwise, when reloading the toolbox, the debugged tab will be focused. 1126 if (this.hostType !== Toolbox.HostType.PAGE) { 1127 // Request the actor to restore the focus to the content page once the 1128 // target is detached. This typically happens when the console closes. 1129 // We restore the focus as it may have been stolen by the console input. 1130 await this.commands.targetConfigurationCommand.updateConfiguration({ 1131 restoreFocus: true, 1132 }); 1133 } 1134 1135 await this.initHarAutomation(); 1136 1137 this.emit("ready"); 1138 this._resolveIsOpen(); 1139 } catch (error) { 1140 console.error( 1141 "Exception while opening the toolbox", 1142 String(error), 1143 error 1144 ); 1145 // While the exception stack is correctly printed in the Browser console when 1146 // passing `e` to console.error, it is not on the stdout, so print it via dump. 1147 dump(error.stack + "\n"); 1148 if (error.clientPacket) { 1149 dump( 1150 "Client packet:" + JSON.stringify(error.clientPacket, null, 2) + "\n" 1151 ); 1152 } 1153 if (error.serverPacket) { 1154 dump( 1155 "Server packet:" + JSON.stringify(error.serverPacket, null, 2) + "\n" 1156 ); 1157 } 1158 1159 try { 1160 // React may not be fully loaded yet and still waiting for Fluent or toolbox.xhtml document load. 1161 // Wait for it in order to have a functional AppErrorBoundary 1162 await this.onReactLoaded; 1163 1164 // If React managed to load, try to display the exception to the user via AppErrorBoundary component. 1165 // But ignore the exception if the React component itself thrown while rendering (errorInfo is defined) 1166 if (this._appBoundary && !this._appBoundary.state.errorInfo) { 1167 this._appBoundary.setState({ 1168 errorMsg: error.toString(), 1169 errorStack: error.stack, 1170 errorInfo: { 1171 clientPacket: error.clientPacket, 1172 serverPacket: error.serverPacket, 1173 }, 1174 toolbox: this, 1175 }); 1176 } 1177 } catch (e) { 1178 // Ignore any further error related to AppErrorBoundary as it would prevent closing the toolbox. 1179 // The exception was already logged to stdout. 1180 } 1181 } 1182 } 1183 1184 /** 1185 * Retrieve the ChromeEventHandler associated to the toolbox frame. 1186 * When DevTools are loaded in a content frame, this will return the containing chrome 1187 * frame. Events from nested frames will bubble up to this chrome frame, which allows to 1188 * listen to events from nested frames. 1189 */ 1190 getChromeEventHandler() { 1191 if (!this.win || !this.win.docShell) { 1192 return null; 1193 } 1194 return this.win.docShell.chromeEventHandler; 1195 } 1196 1197 /** 1198 * Attach events on the chromeEventHandler for the current window. When loaded in a 1199 * frame with type set to "content", events will not bubble across frames. The 1200 * chromeEventHandler does not have this limitation and will catch all events triggered 1201 * on any of the frames under the devtools document. 1202 * 1203 * Events relying on the chromeEventHandler need to be added and removed at specific 1204 * moments in the lifecycle of the toolbox, so all the events relying on it should be 1205 * grouped here. 1206 */ 1207 _addChromeEventHandlerEvents() { 1208 // win.docShell.chromeEventHandler might not be accessible anymore when removing the 1209 // events, so we can't rely on a dynamic getter here. 1210 // Keep a reference on the chromeEventHandler used to addEventListener to be sure we 1211 // can remove the listeners afterwards. 1212 this._chromeEventHandler = this.getChromeEventHandler(); 1213 if (!this._chromeEventHandler) { 1214 return; 1215 } 1216 1217 // Add shortcuts and window-host-shortcuts that use the ChromeEventHandler as target. 1218 this._addShortcuts(); 1219 this._addWindowHostShortcuts(); 1220 1221 this._chromeEventHandler.addEventListener( 1222 "keypress", 1223 this._splitConsoleOnKeypress 1224 ); 1225 this._chromeEventHandler.addEventListener("focus", this._onFocus, true); 1226 this._chromeEventHandler.addEventListener("blur", this._onBlur, true); 1227 this._chromeEventHandler.addEventListener( 1228 "contextmenu", 1229 this._onContextMenu 1230 ); 1231 this._chromeEventHandler.addEventListener("mousedown", this._onMouseDown); 1232 } 1233 1234 _removeChromeEventHandlerEvents() { 1235 if (!this._chromeEventHandler) { 1236 return; 1237 } 1238 1239 // Remove shortcuts and window-host-shortcuts that use the ChromeEventHandler as 1240 // target. 1241 this._removeShortcuts(); 1242 this._removeWindowHostShortcuts(); 1243 1244 this._chromeEventHandler.removeEventListener( 1245 "keypress", 1246 this._splitConsoleOnKeypress 1247 ); 1248 this._chromeEventHandler.removeEventListener("focus", this._onFocus, true); 1249 this._chromeEventHandler.removeEventListener("focus", this._onBlur, true); 1250 this._chromeEventHandler.removeEventListener( 1251 "contextmenu", 1252 this._onContextMenu 1253 ); 1254 this._chromeEventHandler.removeEventListener( 1255 "mousedown", 1256 this._onMouseDown 1257 ); 1258 1259 this._chromeEventHandler = null; 1260 } 1261 1262 _addShortcuts() { 1263 // Create shortcuts instance for the toolbox 1264 if (!this.shortcuts) { 1265 this.shortcuts = new KeyShortcuts({ 1266 window: this.doc.defaultView, 1267 // The toolbox key shortcuts should be triggered from any frame in DevTools. 1268 // Use the chromeEventHandler as the target to catch events from all frames. 1269 target: this.getChromeEventHandler(), 1270 }); 1271 } 1272 1273 // Listen for the shortcut key to show the frame list 1274 this.shortcuts.on(L10N.getStr("toolbox.showFrames.key"), event => { 1275 if (event.target.id === "command-button-frames") { 1276 event.target.click(); 1277 } 1278 }); 1279 1280 // Listen for tool navigation shortcuts. 1281 this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"), event => { 1282 this.selectNextTool(); 1283 event.preventDefault(); 1284 }); 1285 this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"), event => { 1286 this.selectPreviousTool(); 1287 event.preventDefault(); 1288 }); 1289 this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"), event => { 1290 this.switchToPreviousHost(); 1291 event.preventDefault(); 1292 }); 1293 1294 // List for Help/Settings key. 1295 this.shortcuts.on(L10N.getStr("toolbox.help.key"), this.toggleOptions); 1296 1297 if (!this.isBrowserToolbox) { 1298 // Listen for Reload shortcuts 1299 [ 1300 ["reload", false], 1301 ["reload2", false], 1302 ["forceReload", true], 1303 ["forceReload2", true], 1304 ].forEach(([id, bypassCache]) => { 1305 const key = L10N.getStr("toolbox." + id + ".key"); 1306 this.shortcuts.on(key, event => { 1307 this.reload(bypassCache); 1308 1309 // Prevent Firefox shortcuts from reloading the page 1310 event.preventDefault(); 1311 }); 1312 }); 1313 } 1314 1315 // Add zoom-related shortcuts. 1316 if (this.hostType != Toolbox.HostType.PAGE) { 1317 // When the toolbox is rendered in a tab (ie host type is PAGE), the 1318 // zoom should be handled by the default browser shortcuts. 1319 ZoomKeys.register(this.win, this.shortcuts); 1320 } 1321 } 1322 1323 /** 1324 * Reload the debugged context. 1325 * 1326 * @param {boolean} bypassCache 1327 * If true, bypass any cache when reloading. 1328 */ 1329 async reload(bypassCache) { 1330 const box = this.getNotificationBox(); 1331 const notification = box.getNotificationWithValue("reload-error"); 1332 if (notification) { 1333 notification.close(); 1334 } 1335 1336 // When reloading a Web Extension, the top level target isn't destroyed. 1337 // Which prevents some panels (like console and netmonitor) from being correctly cleared. 1338 const consolePanel = this.getPanel("webconsole"); 1339 if (consolePanel) { 1340 // Navigation to a null URL will be translated into a reload message 1341 // when persist log is enabled. 1342 consolePanel.hud.ui.handleWillNavigate({ 1343 timeStamp: new Date(), 1344 url: null, 1345 }); 1346 } 1347 const netPanel = this.getPanel("netmonitor"); 1348 if (netPanel) { 1349 // Fake a navigation, which will clear the netmonitor, if persists is disabled. 1350 netPanel.panelWin.connector.willNavigate(); 1351 } 1352 1353 try { 1354 await this.commands.targetCommand.reloadTopLevelTarget(bypassCache); 1355 } catch (e) { 1356 let { message } = e; 1357 1358 // Remove Protocol.JS exception header to focus on the likely manifest error 1359 message = message.replace("Protocol error (SyntaxError):", ""); 1360 1361 box.appendNotification( 1362 L10N.getFormatStr("toolbox.errorOnReload", message), 1363 "reload-error", 1364 "", 1365 box.PRIORITY_CRITICAL_HIGH 1366 ); 1367 } 1368 } 1369 1370 _removeShortcuts() { 1371 if (this.shortcuts) { 1372 this.shortcuts.destroy(); 1373 this.shortcuts = null; 1374 } 1375 } 1376 1377 /** 1378 * Adds the keys and commands to the Toolbox Window in window mode. 1379 */ 1380 _addWindowHostShortcuts() { 1381 if (this.hostType != Toolbox.HostType.WINDOW) { 1382 // Those shortcuts are only valid for host type WINDOW. 1383 return; 1384 } 1385 1386 if (!this._windowHostShortcuts) { 1387 this._windowHostShortcuts = new KeyShortcuts({ 1388 window: this.win, 1389 // The window host key shortcuts should be triggered from any frame in DevTools. 1390 // Use the chromeEventHandler as the target to catch events from all frames. 1391 target: this.getChromeEventHandler(), 1392 }); 1393 } 1394 1395 const shortcuts = this._windowHostShortcuts; 1396 1397 for (const item of Startup.KeyShortcuts) { 1398 const { id, toolId, shortcut, modifiers } = item; 1399 const electronKey = KeyShortcuts.parseXulKey(modifiers, shortcut); 1400 1401 if (id == "browserConsole") { 1402 // Add key for toggling the browser console from the detached window 1403 shortcuts.on(electronKey, () => { 1404 BrowserConsoleManager.toggleBrowserConsole(); 1405 }); 1406 } else if (toolId) { 1407 // KeyShortcuts contain tool-specific and global key shortcuts, 1408 // here we only need to copy shortcut specific to each tool. 1409 shortcuts.on(electronKey, () => { 1410 this.selectTool(toolId, "key_shortcut").then(() => 1411 this.fireCustomKey(toolId) 1412 ); 1413 }); 1414 } 1415 } 1416 1417 // CmdOrCtrl+W is registered only when the toolbox is running in 1418 // detached window. In the other case the entire browser tab 1419 // is closed when the user uses this shortcut. 1420 shortcuts.on(L10N.getStr("toolbox.closeToolbox.key"), this.closeToolbox); 1421 1422 // The others are only registered in window host type as for other hosts, 1423 // these keys are already registered by devtools-startup.js 1424 shortcuts.on( 1425 L10N.getStr("toolbox.toggleToolboxF12.key"), 1426 this.closeToolbox 1427 ); 1428 if (lazy.AppConstants.platform == "macosx") { 1429 shortcuts.on( 1430 L10N.getStr("toolbox.toggleToolboxOSX.key"), 1431 this.closeToolbox 1432 ); 1433 } else { 1434 shortcuts.on(L10N.getStr("toolbox.toggleToolbox.key"), this.closeToolbox); 1435 } 1436 } 1437 1438 _removeWindowHostShortcuts() { 1439 if (this._windowHostShortcuts) { 1440 this._windowHostShortcuts.destroy(); 1441 this._windowHostShortcuts = null; 1442 } 1443 } 1444 1445 _onContextMenu(e) { 1446 // Handle context menu events in standard input elements: <input> and <textarea>. 1447 // Also support for custom input elements using .devtools-input class 1448 // (e.g. CodeMirror instances). 1449 const isInInput = 1450 e.originalTarget.closest("input[type=text]") || 1451 e.originalTarget.closest("input[type=search]") || 1452 e.originalTarget.closest("input:not([type])") || 1453 e.originalTarget.closest(".devtools-input") || 1454 e.originalTarget.closest("textarea"); 1455 1456 const doc = e.originalTarget.ownerDocument; 1457 const isHTMLPanel = doc.documentElement.namespaceURI === HTML_NS; 1458 1459 if ( 1460 // Context-menu events on input elements will use a custom context menu. 1461 isInInput || 1462 // Context-menu events from HTML panels should not trigger the default 1463 // browser context menu for HTML documents. 1464 isHTMLPanel 1465 ) { 1466 e.stopPropagation(); 1467 e.preventDefault(); 1468 } 1469 1470 if (isInInput) { 1471 this.openTextBoxContextMenu(e.screenX, e.screenY); 1472 } 1473 } 1474 1475 _onMouseDown(e) { 1476 const isMiddleClick = e.button === 1; 1477 if (isMiddleClick) { 1478 // Middle clicks will trigger the scroll lock feature to turn on. 1479 // When the DevTools toolbox was running in an <iframe>, this behavior was 1480 // disabled by default. When running in a <browser> element, we now need 1481 // to catch and preventDefault() on those events. 1482 e.preventDefault(); 1483 } 1484 } 1485 1486 _getDebugTargetData() { 1487 const url = new URL(this.win.location); 1488 const remoteId = url.searchParams.get("remoteId"); 1489 const runtimeInfo = remoteClientManager.getRuntimeInfoByRemoteId(remoteId); 1490 const connectionType = 1491 remoteClientManager.getConnectionTypeByRemoteId(remoteId); 1492 1493 return { 1494 connectionType, 1495 runtimeInfo, 1496 descriptorType: this._descriptorFront.descriptorType, 1497 descriptorName: this._descriptorFront.name, 1498 }; 1499 } 1500 1501 isDebugTargetFenix() { 1502 return this._getDebugTargetData()?.runtimeInfo?.isFenix; 1503 } 1504 1505 /** 1506 * loading React modules when needed (to avoid performance penalties 1507 * during Firefox start up time). 1508 */ 1509 get React() { 1510 return this.browserRequire("devtools/client/shared/vendor/react"); 1511 } 1512 1513 get ReactDOM() { 1514 return this.browserRequire("devtools/client/shared/vendor/react-dom"); 1515 } 1516 1517 get ReactRedux() { 1518 return this.browserRequire("devtools/client/shared/vendor/react-redux"); 1519 } 1520 1521 get ToolboxController() { 1522 return this.browserRequire( 1523 "devtools/client/framework/components/ToolboxController" 1524 ); 1525 } 1526 1527 get AppErrorBoundary() { 1528 return this.browserRequire( 1529 "resource://devtools/client/shared/components/AppErrorBoundary.js" 1530 ); 1531 } 1532 1533 /** 1534 * A common access point for the client-side mapping service for source maps that 1535 * any panel can use. This is a "low-level" API that connects to 1536 * the source map worker. 1537 */ 1538 get sourceMapLoader() { 1539 if (this._sourceMapLoader) { 1540 return this._sourceMapLoader; 1541 } 1542 this._sourceMapLoader = new SourceMapLoader(this.commands.targetCommand); 1543 return this._sourceMapLoader; 1544 } 1545 1546 /** 1547 * Expose the "Parser" debugger worker to both webconsole and debugger. 1548 * 1549 * Note that the Browser Console will also self-instantiate it as it doesn't involve a toolbox. 1550 */ 1551 get parserWorker() { 1552 if (this._parserWorker) { 1553 return this._parserWorker; 1554 } 1555 1556 const { 1557 ParserDispatcher, 1558 } = require("resource://devtools/client/debugger/src/workers/parser/index.js"); 1559 1560 this._parserWorker = new ParserDispatcher(); 1561 return this._parserWorker; 1562 } 1563 1564 /** 1565 * Clients wishing to use source maps but that want the toolbox to 1566 * track the source and style sheet actor mapping can use this 1567 * source map service. This is a higher-level service than the one 1568 * returned by |sourceMapLoader|, in that it automatically tracks 1569 * source and style sheet actor IDs. 1570 */ 1571 get sourceMapURLService() { 1572 if (this._sourceMapURLService) { 1573 return this._sourceMapURLService; 1574 } 1575 this._sourceMapURLService = new SourceMapURLService( 1576 this.commands, 1577 this.sourceMapLoader 1578 ); 1579 return this._sourceMapURLService; 1580 } 1581 1582 // Return HostType id for telemetry 1583 _getTelemetryHostId() { 1584 switch (this.hostType) { 1585 case Toolbox.HostType.BOTTOM: 1586 return 0; 1587 case Toolbox.HostType.RIGHT: 1588 return 1; 1589 case Toolbox.HostType.WINDOW: 1590 return 2; 1591 case Toolbox.HostType.BROWSERTOOLBOX: 1592 return 3; 1593 case Toolbox.HostType.LEFT: 1594 return 4; 1595 case Toolbox.HostType.PAGE: 1596 return 5; 1597 default: 1598 return 9; 1599 } 1600 } 1601 1602 // Return HostType string for telemetry 1603 _getTelemetryHostString() { 1604 switch (this.hostType) { 1605 case Toolbox.HostType.BOTTOM: 1606 return "bottom"; 1607 case Toolbox.HostType.LEFT: 1608 return "left"; 1609 case Toolbox.HostType.RIGHT: 1610 return "right"; 1611 case Toolbox.HostType.WINDOW: 1612 return "window"; 1613 case Toolbox.HostType.PAGE: 1614 return "page"; 1615 case Toolbox.HostType.BROWSERTOOLBOX: 1616 return "other"; 1617 default: 1618 return "bottom"; 1619 } 1620 } 1621 1622 _pingTelemetry() { 1623 Services.prefs.setBoolPref("devtools.everOpened", true); 1624 this.telemetry.toolOpened("toolbox", this); 1625 1626 Glean.devtools.toolboxHost.accumulateSingleSample( 1627 this._getTelemetryHostId() 1628 ); 1629 1630 // Log current theme. The question we want to answer is: 1631 // "What proportion of users use which themes?" 1632 const currentTheme = Services.prefs.getCharPref("devtools.theme"); 1633 Glean.devtools.currentTheme[currentTheme].add(1); 1634 1635 const browserWin = this.topWindow; 1636 this.telemetry.preparePendingEvent(browserWin, "open", "tools", null, [ 1637 "entrypoint", 1638 "first_panel", 1639 "host", 1640 "shortcut", 1641 "splitconsole", 1642 "width", 1643 ]); 1644 this.telemetry.addEventProperty( 1645 browserWin, 1646 "open", 1647 "tools", 1648 null, 1649 "host", 1650 this._getTelemetryHostString() 1651 ); 1652 } 1653 1654 /** 1655 * Create a simple object to store the state of a toolbox button. The checked state of 1656 * a button can be updated arbitrarily outside of the scope of the toolbar and its 1657 * controllers. In order to simplify this interaction this object emits an 1658 * "updatechecked" event any time the isChecked value is updated, allowing any consuming 1659 * components to listen and respond to updates. 1660 * 1661 * @param {object} options: 1662 * 1663 * @property {string} id - The id of the button or command. 1664 * @property {string} className - An optional additional className for the button. 1665 * @property {string} description - The value that will display as a tooltip and in 1666 * the options panel for enabling/disabling. 1667 * @property {boolean} disabled - An optional disabled state for the button. 1668 * @property {Function} onClick - The function to run when the button is activated by 1669 * click or keyboard shortcut. First argument will be the 'click' 1670 * event, and second argument is the toolbox instance. 1671 * @property {boolean} isInStartContainer - Buttons can either be placed at the start 1672 * of the toolbar, or at the end. 1673 * @property {Function} setup - Function run immediately to listen for events changing 1674 * whenever the button is checked or unchecked. The toolbox object 1675 * is passed as first argument and a callback is passed as second 1676 * argument, to be called whenever the checked state changes. 1677 * @property {Function} teardown - Function run on toolbox close to let a chance to 1678 * unregister listeners set when `setup` was called and avoid 1679 * memory leaks. The same arguments than `setup` function are 1680 * passed to `teardown`. 1681 * @property {Function} isToolSupported - Function to automatically enable/disable 1682 * the button based on the toolbox. If the toolbox don't support 1683 * the button feature, this method should return false. 1684 * @property {Function} isCurrentlyVisible - Function to automatically 1685 * hide/show the button based on current state. 1686 * @property {Function} isChecked - Optional function called to known if the button 1687 * is toggled or not. The function should return true when 1688 * the button should be displayed as toggled on. 1689 */ 1690 _createButtonState(options) { 1691 let isCheckedValue = false; 1692 const { 1693 id, 1694 className, 1695 description, 1696 disabled, 1697 onClick, 1698 isInStartContainer, 1699 setup, 1700 teardown, 1701 isToolSupported, 1702 isCurrentlyVisible, 1703 isChecked, 1704 isToggle, 1705 onKeyDown, 1706 experimentalURL, 1707 } = options; 1708 const toolbox = this; 1709 const button = { 1710 id, 1711 className, 1712 description, 1713 disabled, 1714 async onClick(event) { 1715 if (typeof onClick == "function") { 1716 await onClick(event, toolbox); 1717 button.emit("updatechecked"); 1718 } 1719 }, 1720 onKeyDown(event) { 1721 if (typeof onKeyDown == "function") { 1722 onKeyDown(event, toolbox); 1723 } 1724 }, 1725 isToolSupported, 1726 isCurrentlyVisible, 1727 get isChecked() { 1728 if (typeof isChecked == "function") { 1729 return isChecked(toolbox); 1730 } 1731 return isCheckedValue; 1732 }, 1733 set isChecked(value) { 1734 // Note that if options.isChecked is given, this is ignored 1735 isCheckedValue = value; 1736 this.emit("updatechecked"); 1737 }, 1738 isToggle, 1739 // The preference for having this button visible. 1740 visibilityswitch: `devtools.${id}.enabled`, 1741 // The toolbar has a container at the start and end of the toolbar for 1742 // holding buttons. By default the buttons are placed in the end container. 1743 isInStartContainer: !!isInStartContainer, 1744 experimentalURL, 1745 getContextMenu() { 1746 if (options.getContextMenu) { 1747 return options.getContextMenu(toolbox); 1748 } 1749 return null; 1750 }, 1751 }; 1752 if (typeof setup == "function") { 1753 // Use async function as tracer's definition requires an async function to be passed 1754 // for "toggle" event listener. 1755 const onChange = async () => { 1756 button.emit("updatechecked"); 1757 }; 1758 setup(this, onChange); 1759 // Save a reference to the cleanup method that will unregister the onChange 1760 // callback. Immediately bind the function argument so that we don't have to 1761 // also save a reference to them. 1762 button.teardown = teardown.bind(options, this, onChange); 1763 } 1764 button.isVisible = this._commandIsVisible(button); 1765 1766 EventEmitter.decorate(button); 1767 1768 return button; 1769 } 1770 1771 _splitConsoleOnKeypress(e) { 1772 if (e.keyCode !== KeyCodes.DOM_VK_ESCAPE || !this.isSplitConsoleEnabled()) { 1773 return; 1774 } 1775 1776 const currentPanel = this.getCurrentPanel(); 1777 if ( 1778 typeof currentPanel.onToolboxChromeEventHandlerEscapeKeyDown === 1779 "function" 1780 ) { 1781 const ac = new this.win.AbortController(); 1782 currentPanel.onToolboxChromeEventHandlerEscapeKeyDown(ac); 1783 if (ac.signal.aborted) { 1784 return; 1785 } 1786 } 1787 1788 this.toggleSplitConsole(); 1789 // If the debugger is paused, don't let the ESC key stop any pending navigation. 1790 // If the host is page, don't let the ESC stop the load of the webconsole frame. 1791 if ( 1792 this.threadFront.state == "paused" || 1793 this.hostType === Toolbox.HostType.PAGE 1794 ) { 1795 e.preventDefault(); 1796 } 1797 } 1798 1799 /** 1800 * Add a shortcut key that should work when a split console 1801 * has focus to the toolbox. 1802 * 1803 * @param {string} key 1804 * The electron key shortcut. 1805 * @param {Function} handler 1806 * The callback that should be called when the provided key shortcut is pressed. 1807 * @param {string} whichTool 1808 * The tool the key belongs to. The corresponding handler will only be triggered 1809 * if this tool is active. 1810 */ 1811 useKeyWithSplitConsole(key, handler, whichTool) { 1812 this.shortcuts.on(key, event => { 1813 if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) { 1814 handler(); 1815 event.preventDefault(); 1816 } 1817 }); 1818 } 1819 1820 _addWindowListeners() { 1821 this.win.addEventListener("unload", this.destroy); 1822 this.win.addEventListener("message", this._onBrowserMessage, true); 1823 } 1824 1825 _removeWindowListeners() { 1826 // The host iframe's contentDocument may already be gone. 1827 if (this.win) { 1828 this.win.removeEventListener("unload", this.destroy); 1829 this.win.removeEventListener("message", this._onBrowserMessage, true); 1830 } 1831 } 1832 1833 // Called whenever the chrome send a message 1834 _onBrowserMessage(event) { 1835 if (event.data?.name === "switched-host") { 1836 this._onSwitchedHost(event.data); 1837 } 1838 if (event.data?.name === "switched-host-to-tab") { 1839 this._onSwitchedHostToTab(event.data.browsingContextID); 1840 } 1841 if (event.data?.name === "host-raised") { 1842 this.emit("host-raised"); 1843 } 1844 } 1845 1846 _saveSplitConsoleHeight() { 1847 const height = parseInt(this.webconsolePanel.style.height, 10); 1848 if (!isNaN(height)) { 1849 Services.prefs.setIntPref(SPLITCONSOLE_HEIGHT_PREF, height); 1850 } 1851 } 1852 1853 /** 1854 * Make sure that the console is showing up properly based on all the 1855 * possible conditions. 1856 * 1) If the console tab is selected, then regardless of split state 1857 * it should take up the full height of the deck, and we should 1858 * hide the deck and splitter. 1859 * 2) If the console tab is not selected and it is split, then we should 1860 * show the splitter, deck, and console. 1861 * 3) If the console tab is not selected and it is *not* split, 1862 * then we should hide the console and splitter, and show the deck 1863 * at full height. 1864 */ 1865 _refreshConsoleDisplay() { 1866 const deck = this.doc.getElementById("toolbox-deck"); 1867 const webconsolePanel = this.webconsolePanel; 1868 const splitter = this.doc.getElementById("toolbox-console-splitter"); 1869 const openedConsolePanel = this.currentToolId === "webconsole"; 1870 1871 if (openedConsolePanel) { 1872 deck.setAttribute("hidden", ""); 1873 deck.removeAttribute("expanded"); 1874 splitter.hidden = true; 1875 webconsolePanel.removeAttribute("hidden"); 1876 webconsolePanel.setAttribute("expanded", ""); 1877 } else { 1878 deck.removeAttribute("hidden"); 1879 deck.toggleAttribute("expanded", !this.splitConsole); 1880 splitter.hidden = !this.splitConsole; 1881 webconsolePanel.collapsed = !this.splitConsole; 1882 webconsolePanel.removeAttribute("expanded"); 1883 } 1884 1885 // Either restore the last known split console height, if in split console mode, 1886 // or ensure there is no height set to prevent shrinking the regular console. 1887 this.webconsolePanel.style.height = openedConsolePanel 1888 ? "" 1889 : Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF) + "px"; 1890 } 1891 1892 /** 1893 * Handle any custom key events. Returns true if there was a custom key 1894 * binding run. 1895 * 1896 * @param {string} toolId Which tool to run the command on (skip if not 1897 * current) 1898 */ 1899 fireCustomKey(toolId) { 1900 const toolDefinition = gDevTools.getToolDefinition(toolId); 1901 1902 if ( 1903 toolDefinition.onkey && 1904 (this.currentToolId === toolId || 1905 (toolId == "webconsole" && this.splitConsole)) 1906 ) { 1907 toolDefinition.onkey(this.getCurrentPanel(), this); 1908 } 1909 } 1910 1911 /** 1912 * Build the notification box as soon as needed. 1913 */ 1914 get notificationBox() { 1915 if (!this._notificationBox) { 1916 let { NotificationBox, PriorityLevels } = this.browserRequire( 1917 "devtools/client/shared/components/NotificationBox" 1918 ); 1919 1920 NotificationBox = this.React.createFactory(NotificationBox); 1921 1922 // Render NotificationBox and assign priority levels to it. 1923 const box = this.doc.getElementById("toolbox-notificationbox"); 1924 this._notificationBox = Object.assign( 1925 this.ReactDOM.render(NotificationBox({ wrapping: true }), box), 1926 PriorityLevels 1927 ); 1928 } 1929 return this._notificationBox; 1930 } 1931 1932 /** 1933 * Build the options for changing hosts. Called every time 1934 * the host changes. 1935 */ 1936 _buildDockOptions() { 1937 if (!this._descriptorFront.isLocalTab) { 1938 this.component.setDockOptionsEnabled(false); 1939 this.component.setCanCloseToolbox(false); 1940 return; 1941 } 1942 1943 this.component.setDockOptionsEnabled(true); 1944 this.component.setCanCloseToolbox( 1945 this.hostType !== Toolbox.HostType.WINDOW 1946 ); 1947 1948 const hostTypes = []; 1949 for (const type in Toolbox.HostType) { 1950 const position = Toolbox.HostType[type]; 1951 if ( 1952 position == Toolbox.HostType.BROWSERTOOLBOX || 1953 position == Toolbox.HostType.PAGE 1954 ) { 1955 continue; 1956 } 1957 1958 hostTypes.push({ 1959 position, 1960 switchHost: this.switchHost.bind(this, position), 1961 }); 1962 } 1963 1964 this.component.setCurrentHostType(this.hostType); 1965 this.component.setHostTypes(hostTypes); 1966 } 1967 1968 postMessage(msg) { 1969 // We sometime try to send messages in middle of destroy(), where the 1970 // toolbox iframe may already be detached. 1971 if (!this._destroyer) { 1972 // Toolbox document is still chrome and disallow identifying message 1973 // origin via event.source as it is null. So use a custom id. 1974 msg.frameId = this.frameId; 1975 this.topWindow.postMessage(msg, "*"); 1976 } 1977 } 1978 1979 /** 1980 * This will fetch the panel definitions from the constants in definitions module 1981 * and populate the state within the ToolboxController component. 1982 */ 1983 async _buildInitialPanelDefinitions() { 1984 // Get the initial list of tab definitions. This list can be amended at a later time 1985 // by tools registering themselves. 1986 const definitions = gDevTools.getToolDefinitionArray(); 1987 definitions.forEach(definition => this._buildPanelForTool(definition)); 1988 1989 // Get the definitions that will only affect the main tab area. 1990 this.panelDefinitions = definitions.filter( 1991 definition => 1992 definition.isToolSupported(this) && definition.id !== "options" 1993 ); 1994 } 1995 1996 async _setInitialMeatballState() { 1997 let disableAutohide, pseudoLocale; 1998 // Popup auto-hide disabling is only available in browser toolbox and webextension toolboxes. 1999 if ( 2000 this.isBrowserToolbox || 2001 this._descriptorFront.isWebExtensionDescriptor 2002 ) { 2003 disableAutohide = await this._isDisableAutohideEnabled(); 2004 } 2005 // Pseudo locale items are only displayed in the browser toolbox 2006 if (this.isBrowserToolbox) { 2007 pseudoLocale = await this.getPseudoLocale(); 2008 } 2009 // Parallelize the asynchronous calls, so that the DOM is only updated once when 2010 // updating the React components. 2011 if (typeof disableAutohide == "boolean") { 2012 this.component.setDisableAutohide(disableAutohide); 2013 } 2014 if (typeof pseudoLocale == "string") { 2015 this.component.setPseudoLocale(pseudoLocale); 2016 } 2017 if ( 2018 this._descriptorFront.isWebExtensionDescriptor && 2019 this.hostType === Toolbox.HostType.WINDOW 2020 ) { 2021 const alwaysOnTop = Services.prefs.getBoolPref( 2022 DEVTOOLS_ALWAYS_ON_TOP, 2023 false 2024 ); 2025 this.component.setAlwaysOnTop(alwaysOnTop); 2026 } 2027 } 2028 2029 /** 2030 * Initiate toolbox React components and all it's properties. Do the initial render. 2031 */ 2032 async _initializeReactComponent() { 2033 // Kick off async loading the Fluent bundles. 2034 const fluentL10n = new FluentL10n(); 2035 const fluentInitPromise = fluentL10n.init(["devtools/client/toolbox.ftl"]); 2036 2037 // To avoid any possible artifact, wait for the document to be fully loaded 2038 // before creating the Browser Loader based on toolbox window object. 2039 await new Promise(resolve => { 2040 DOMHelpers.onceDOMReady( 2041 this.win, 2042 () => { 2043 resolve(); 2044 }, 2045 this._URL 2046 ); 2047 }); 2048 2049 // Setup the Toolbox Browser Loader, used to load React component modules 2050 // which expect to be loaded with toolbox.xhtml document as global scope. 2051 this.browserRequire = BrowserLoader({ 2052 window: this.win, 2053 useOnlyShared: true, 2054 }).require; 2055 2056 // Wait for the bundles to be ready to use 2057 await fluentInitPromise; 2058 const fluentBundles = fluentL10n.getBundles(); 2059 2060 // ToolboxController is wrapped into AppErrorBoundary in order to nicely 2061 // show any exception that may happen in React updates/renders. 2062 const element = this.React.createElement( 2063 this.AppErrorBoundary, 2064 { 2065 componentName: "General", 2066 panel: L10N.getStr("webDeveloperToolsMenu.label"), 2067 }, 2068 this.React.createElement(this.ToolboxController, { 2069 ref: r => { 2070 this.component = r; 2071 }, 2072 L10N, 2073 fluentBundles, 2074 currentToolId: this.currentToolId, 2075 selectTool: this.selectTool, 2076 toggleOptions: this.toggleOptions, 2077 toggleSplitConsole: this.toggleSplitConsole, 2078 toggleNoAutohide: this.toggleNoAutohide, 2079 toggleAlwaysOnTop: this.toggleAlwaysOnTop, 2080 disablePseudoLocale: this.disablePseudoLocale, 2081 enableAccentedPseudoLocale: this.enableAccentedPseudoLocale, 2082 enableBidiPseudoLocale: this.enableBidiPseudoLocale, 2083 closeToolbox: this.closeToolbox, 2084 focusButton: this._onToolbarFocus, 2085 toolbox: this, 2086 onTabsOrderUpdated: this._onTabsOrderUpdated, 2087 }) 2088 ); 2089 2090 // Get the DOM element to mount the React components to. 2091 this._componentMount = this.doc.getElementById("toolbox-toolbar-mount"); 2092 this._appBoundary = this.ReactDOM.render(element, this._componentMount); 2093 } 2094 2095 /** 2096 * Reset tabindex attributes across all focusable elements inside the toolbar. 2097 * Only have one element with tabindex=0 at a time to make sure that tabbing 2098 * results in navigating away from the toolbar container. 2099 * 2100 * @param {FocusEvent} event 2101 */ 2102 _onToolbarFocus(id) { 2103 this.component.setFocusedButton(id); 2104 } 2105 2106 /** 2107 * On left/right arrow press, attempt to move the focus inside the toolbar to 2108 * the previous/next focusable element. This is not in the React component 2109 * as it is difficult to coordinate between different component elements. 2110 * The components are responsible for setting the correct tabindex value 2111 * for if they are the focused element. 2112 * 2113 * @param {KeyboardEvent} event 2114 */ 2115 _onToolbarArrowKeypress(event) { 2116 const { key, target, ctrlKey, shiftKey, altKey, metaKey } = event; 2117 2118 // If any of the modifier keys are pressed do not attempt navigation as it 2119 // might conflict with global shortcuts (Bug 1327972). 2120 if (ctrlKey || shiftKey || altKey || metaKey) { 2121 return; 2122 } 2123 2124 const buttons = [...this._tabBar.querySelectorAll("button")]; 2125 const curIndex = buttons.indexOf(target); 2126 2127 if (curIndex === -1) { 2128 console.warn( 2129 target + 2130 " is not found among Developer Tools tab bar " + 2131 "focusable elements." 2132 ); 2133 return; 2134 } 2135 2136 let newTarget; 2137 const firstTabIndex = 0; 2138 const lastTabIndex = buttons.length - 1; 2139 const nextOrLastTabIndex = Math.min(lastTabIndex, curIndex + 1); 2140 const previousOrFirstTabIndex = Math.max(firstTabIndex, curIndex - 1); 2141 const ltr = this.direction === "ltr"; 2142 2143 if (key === "ArrowLeft") { 2144 // Do nothing if already at the beginning. 2145 if ( 2146 (ltr && curIndex === firstTabIndex) || 2147 (!ltr && curIndex === lastTabIndex) 2148 ) { 2149 return; 2150 } 2151 newTarget = buttons[ltr ? previousOrFirstTabIndex : nextOrLastTabIndex]; 2152 } else if (key === "ArrowRight") { 2153 // Do nothing if already at the end. 2154 if ( 2155 (ltr && curIndex === lastTabIndex) || 2156 (!ltr && curIndex === firstTabIndex) 2157 ) { 2158 return; 2159 } 2160 newTarget = buttons[ltr ? nextOrLastTabIndex : previousOrFirstTabIndex]; 2161 } else { 2162 return; 2163 } 2164 2165 newTarget.focus(); 2166 2167 event.preventDefault(); 2168 event.stopPropagation(); 2169 } 2170 2171 /** 2172 * Add buttons to the UI as specified in devtools/client/definitions.js 2173 */ 2174 _buildButtons() { 2175 // Beyond the normal preference filtering 2176 this.toolbarButtons = [ 2177 this._buildErrorCountButton(), 2178 this._buildPickerButton(), 2179 this._buildFrameButton(), 2180 ]; 2181 2182 ToolboxButtons.forEach(definition => { 2183 const button = this._createButtonState(definition); 2184 this.toolbarButtons.push(button); 2185 }); 2186 2187 this.component.setToolboxButtons(this.toolbarButtons); 2188 } 2189 2190 /** 2191 * Button to select a frame for the inspector to target. 2192 */ 2193 _buildFrameButton() { 2194 this.frameButton = this._createButtonState({ 2195 id: "command-button-frames", 2196 description: L10N.getStr("toolbox.frames.tooltip"), 2197 isToolSupported: toolbox => { 2198 return toolbox.target.getTrait("frames"); 2199 }, 2200 isCurrentlyVisible: () => { 2201 const hasFrames = this.frameMap.size > 1; 2202 const isOnOptionsPanel = this.currentToolId === "options"; 2203 return hasFrames || isOnOptionsPanel; 2204 }, 2205 }); 2206 2207 return this.frameButton; 2208 } 2209 2210 /** 2211 * Button to display the number of errors. 2212 */ 2213 _buildErrorCountButton() { 2214 this.errorCountButton = this._createButtonState({ 2215 id: "command-button-errorcount", 2216 isInStartContainer: false, 2217 isToolSupported: () => true, 2218 description: L10N.getStr("toolbox.errorCountButton.description"), 2219 }); 2220 // Use updateErrorCountButton to set some properties so we don't have to repeat 2221 // the logic here. 2222 this.updateErrorCountButton(); 2223 2224 return this.errorCountButton; 2225 } 2226 2227 /** 2228 * Toggle the picker, but also decide whether or not the highlighter should 2229 * focus the window. This is only desirable when the toolbox is mounted to the 2230 * window. When devtools is free floating, then the target window should not 2231 * pop in front of the viewer when the picker is clicked. 2232 * 2233 * Note: Toggle picker can be overwritten by panel other than the inspector to 2234 * allow for custom picker behaviour. 2235 */ 2236 async _onPickerClick() { 2237 const focus = 2238 this.hostType === Toolbox.HostType.BOTTOM || 2239 this.hostType === Toolbox.HostType.LEFT || 2240 this.hostType === Toolbox.HostType.RIGHT; 2241 const currentPanel = this.getCurrentPanel(); 2242 if (currentPanel.togglePicker) { 2243 currentPanel.togglePicker(focus); 2244 } else { 2245 this.nodePicker.togglePicker(focus); 2246 } 2247 } 2248 2249 /** 2250 * If the picker is activated, then allow the Escape key to deactivate the 2251 * functionality instead of the default behavior of toggling the console. 2252 */ 2253 _onPickerKeypress(event) { 2254 if (event.keyCode === KeyCodes.DOM_VK_ESCAPE) { 2255 const currentPanel = this.getCurrentPanel(); 2256 if (currentPanel.cancelPicker) { 2257 currentPanel.cancelPicker(); 2258 } else { 2259 this.nodePicker.stop({ canceled: true }); 2260 } 2261 // Stop the console from toggling. 2262 event.stopImmediatePropagation(); 2263 } 2264 } 2265 2266 async _onPickerStarting() { 2267 if (this.isDestroying()) { 2268 return; 2269 } 2270 this.tellRDMAboutPickerState(true, PICKER_TYPES.ELEMENT); 2271 this.pickerButton.isChecked = true; 2272 await this.selectTool("inspector", "inspect_dom"); 2273 // turn off color picker when node picker is starting 2274 this.getPanel("inspector").hideEyeDropper(); 2275 this.on("select", this._onToolSelectedStopPicker); 2276 } 2277 2278 async _onPickerStarted() { 2279 this.doc.addEventListener("keypress", this._onPickerKeypress, true); 2280 } 2281 2282 _onPickerStopped() { 2283 if (this.isDestroying()) { 2284 return; 2285 } 2286 this.tellRDMAboutPickerState(false, PICKER_TYPES.ELEMENT); 2287 this.off("select", this._onToolSelectedStopPicker); 2288 this.doc.removeEventListener("keypress", this._onPickerKeypress, true); 2289 this.pickerButton.isChecked = false; 2290 } 2291 2292 _onToolSelectedStopPicker() { 2293 this.nodePicker.stop({ canceled: true }); 2294 } 2295 2296 /** 2297 * When the picker is canceled, make sure the toolbox 2298 * gets the focus. 2299 */ 2300 _onPickerCanceled() { 2301 if (this.hostType !== Toolbox.HostType.WINDOW) { 2302 this.win.focus(); 2303 } 2304 } 2305 2306 _onPickerPicked(nodeFront) { 2307 this.selection.setNodeFront(nodeFront, { reason: "picker-node-picked" }); 2308 } 2309 2310 _onPickerPreviewed(nodeFront) { 2311 this.selection.setNodeFront(nodeFront, { reason: "picker-node-previewed" }); 2312 } 2313 2314 /** 2315 * RDM sometimes simulates touch events. For this to work correctly at all times, it 2316 * needs to know when the picker is active or not. 2317 * This method communicates with the RDM Manager if it exists. 2318 * 2319 * @param {boolean} state 2320 * @param {string} pickerType 2321 * One of devtools/shared/picker-constants 2322 */ 2323 async tellRDMAboutPickerState(state, pickerType) { 2324 const { localTab } = this.commands.descriptorFront; 2325 2326 if (!ResponsiveUIManager.isActiveForTab(localTab)) { 2327 return; 2328 } 2329 2330 const ui = ResponsiveUIManager.getResponsiveUIForTab(localTab); 2331 await ui.setElementPickerState(state, pickerType); 2332 } 2333 2334 /** 2335 * The element picker button enables the ability to select a DOM node by clicking 2336 * it on the page. 2337 */ 2338 _buildPickerButton() { 2339 this.pickerButton = this._createButtonState({ 2340 id: "command-button-pick", 2341 className: this._getPickerAdditionalClassName(), 2342 description: this._getPickerTooltip(), 2343 onClick: this._onPickerClick, 2344 isInStartContainer: true, 2345 isToolSupported: toolbox => { 2346 return toolbox.target.getTrait("frames"); 2347 }, 2348 isToggle: true, 2349 }); 2350 2351 return this.pickerButton; 2352 } 2353 2354 _getPickerAdditionalClassName() { 2355 if (this.isDebugTargetFenix()) { 2356 return "remote-fenix"; 2357 } 2358 return null; 2359 } 2360 2361 /** 2362 * Get the tooltip for the element picker button. 2363 * It has multiple possible keyboard shortcuts for macOS. 2364 * 2365 * @return {string} 2366 */ 2367 _getPickerTooltip() { 2368 let shortcut = L10N.getStr("toolbox.elementPicker.key"); 2369 shortcut = KeyShortcuts.parseElectronKey(shortcut); 2370 shortcut = KeyShortcuts.stringifyShortcut(shortcut); 2371 const shortcutMac = L10N.getStr("toolbox.elementPicker.mac.key"); 2372 const isMac = Services.appinfo.OS === "Darwin"; 2373 2374 let label; 2375 if (this.isDebugTargetFenix()) { 2376 label = isMac 2377 ? "toolbox.androidElementPicker.mac.tooltip" 2378 : "toolbox.androidElementPicker.tooltip"; 2379 } else { 2380 label = isMac 2381 ? "toolbox.elementPicker.mac.tooltip" 2382 : "toolbox.elementPicker.tooltip"; 2383 } 2384 2385 return isMac 2386 ? L10N.getFormatStr(label, shortcut, shortcutMac) 2387 : L10N.getFormatStr(label, shortcut); 2388 } 2389 2390 async _listenAndApplyConfigurationPref() { 2391 this._onBooleanConfigurationPrefChange = 2392 this._onBooleanConfigurationPrefChange.bind(this); 2393 2394 // We have two configurations: 2395 // * target specific configurations, which are set on all target actors, themself easily accessible from any actor. 2396 // Most configurations should be set this way. 2397 // * thread specific configurations, which are set on directly on the thread actor. 2398 // Only configuration used by the thread actor should be set this way. 2399 const targetConfiguration = {}; 2400 2401 // Get the current thread settings from the prefs as well as debugger internal storage for breakpoints. 2402 const threadConfiguration = await getThreadOptions(); 2403 2404 for (const prefName in BOOLEAN_CONFIGURATION_PREFS) { 2405 const { name, thread } = BOOLEAN_CONFIGURATION_PREFS[prefName]; 2406 const value = Services.prefs.getBoolPref(prefName, false); 2407 2408 // Based on the pref name, this will be stored in either target or thread specific configuration 2409 if (thread) { 2410 threadConfiguration[name] = value; 2411 } else { 2412 targetConfiguration[name] = value; 2413 } 2414 2415 // Also listen for any future change 2416 Services.prefs.addObserver( 2417 prefName, 2418 this._onBooleanConfigurationPrefChange 2419 ); 2420 } 2421 2422 // Now communicate the configurations to the server 2423 await this.commands.targetConfigurationCommand.updateConfiguration( 2424 targetConfiguration 2425 ); 2426 await this.commands.threadConfigurationCommand.updateConfiguration( 2427 threadConfiguration 2428 ); 2429 } 2430 2431 /** 2432 * Called whenever a preference registered in BOOLEAN_CONFIGURATION_PREFS 2433 * changes. 2434 * This is used to communicate the new setting's value to the server. 2435 * 2436 * @param {string} subject 2437 * @param {string} topic 2438 * @param {string} prefName 2439 * The preference name which changed 2440 */ 2441 async _onBooleanConfigurationPrefChange(subject, topic, prefName) { 2442 const { name, thread } = BOOLEAN_CONFIGURATION_PREFS[prefName]; 2443 const value = Services.prefs.getBoolPref(prefName, false); 2444 2445 const configurationCommand = thread 2446 ? this.commands.threadConfigurationCommand 2447 : this.commands.targetConfigurationCommand; 2448 await configurationCommand.updateConfiguration({ 2449 [name]: value, 2450 }); 2451 2452 // This event is only emitted for tests in order to know when the setting has been applied by the backend. 2453 this.emitForTests("new-configuration-applied", prefName); 2454 } 2455 2456 /** 2457 * Update the visibility of the buttons. 2458 */ 2459 updateToolboxButtonsVisibility() { 2460 this.toolbarButtons.forEach(button => { 2461 button.isVisible = this._commandIsVisible(button); 2462 }); 2463 this.component.setToolboxButtons(this.toolbarButtons); 2464 } 2465 2466 /** 2467 * Update the buttons. 2468 */ 2469 updateToolboxButtons() { 2470 const inspectorFront = this.target.getCachedFront("inspector"); 2471 // two of the buttons have highlighters that need to be cleared 2472 // on will-navigate, otherwise we hold on to the stale highlighter 2473 const hasHighlighters = 2474 inspectorFront && 2475 (inspectorFront.hasHighlighter(lazy.TYPES.RULERS) || 2476 inspectorFront.hasHighlighter(lazy.TYPES.MEASURING)); 2477 if (hasHighlighters) { 2478 inspectorFront.destroyHighlighters(); 2479 this.component.setToolboxButtons(this.toolbarButtons); 2480 } 2481 } 2482 2483 /** 2484 * Visually update picker button. 2485 * This function is called on every "select" event. Newly selected panel can 2486 * update the visual state of the picker button such as disabled state, 2487 * additional CSS classes (className), and tooltip (description). 2488 */ 2489 updatePickerButton() { 2490 const button = this.pickerButton; 2491 const currentPanel = this.getCurrentPanel(); 2492 2493 if (currentPanel?.updatePickerButton) { 2494 currentPanel.updatePickerButton(); 2495 } else { 2496 // If the current panel doesn't define a custom updatePickerButton, 2497 // revert the button to its default state 2498 button.description = this._getPickerTooltip(); 2499 button.className = this._getPickerAdditionalClassName(); 2500 button.disabled = null; 2501 } 2502 } 2503 2504 /** 2505 * Update the visual state of the Frame picker button. 2506 */ 2507 updateFrameButton() { 2508 if (this.isDestroying()) { 2509 return; 2510 } 2511 2512 if (this.currentToolId === "options" && this.frameMap.size <= 1) { 2513 // If the button is only visible because the user is on the Options panel, disable 2514 // the button and set an appropriate description. 2515 this.frameButton.disabled = true; 2516 this.frameButton.description = L10N.getStr( 2517 "toolbox.frames.disabled.tooltip" 2518 ); 2519 } else { 2520 // Otherwise, enable the button and update the description. 2521 this.frameButton.disabled = false; 2522 this.frameButton.description = L10N.getStr("toolbox.frames.tooltip"); 2523 } 2524 2525 // Highlight the button when a child frame is selected and visible. 2526 const selectedFrame = this.frameMap.get(this.selectedFrameId) || {}; 2527 2528 // We need to do something a bit different to avoid some test failures. This function 2529 // can be called from onWillNavigate, and the current target might have this `traits` 2530 // property nullifed, which is unfortunate as that's what isToolSupported is checking, 2531 // so it will throw. 2532 // So here, we check first if the button isn't going to be visible anyway (it only checks 2533 // for this.frameMap size) so we don't call _commandIsVisible. 2534 const isVisible = !this.frameButton.isCurrentlyVisible() 2535 ? false 2536 : this._commandIsVisible(this.frameButton); 2537 2538 this.frameButton.isVisible = isVisible; 2539 2540 if (isVisible) { 2541 this.frameButton.isChecked = !selectedFrame.isTopLevel; 2542 } 2543 } 2544 2545 updateErrorCountButton() { 2546 this.errorCountButton.isVisible = 2547 this._commandIsVisible(this.errorCountButton) && this._errorCount > 0; 2548 this.errorCountButton.errorCount = this._errorCount; 2549 } 2550 2551 /** 2552 * Setup the _splitConsoleEnabled, reflecting the enabled/disabled state of the Enable Split 2553 * Console setting, and close the split console if it's open and the setting is turned off 2554 */ 2555 updateIsSplitConsoleEnabled() { 2556 this._splitConsoleEnabled = Services.prefs.getBoolPref( 2557 SPLITCONSOLE_ENABLED_PREF, 2558 true 2559 ); 2560 2561 if (!this._splitConsoleEnabled && this.splitConsole) { 2562 this.closeSplitConsole(); 2563 } 2564 } 2565 2566 /** 2567 * Ensure the visibility of each toolbox button matches the preference value. 2568 */ 2569 _commandIsVisible(button) { 2570 const { isToolSupported, isCurrentlyVisible, visibilityswitch } = button; 2571 2572 if (!Services.prefs.getBoolPref(visibilityswitch, true)) { 2573 return false; 2574 } 2575 2576 if (isToolSupported && !isToolSupported(this)) { 2577 return false; 2578 } 2579 2580 if (isCurrentlyVisible && !isCurrentlyVisible()) { 2581 return false; 2582 } 2583 2584 return true; 2585 } 2586 2587 /** 2588 * Build a panel for a tool definition. 2589 * 2590 * @param {string} toolDefinition 2591 * Tool definition of the tool to build a tab for. 2592 */ 2593 _buildPanelForTool(toolDefinition) { 2594 if (!toolDefinition.isToolSupported(this)) { 2595 return; 2596 } 2597 2598 const deck = this.doc.getElementById("toolbox-deck"); 2599 const id = toolDefinition.id; 2600 2601 if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) { 2602 toolDefinition.ordinal = MAX_ORDINAL; 2603 } 2604 2605 if (!toolDefinition.bgTheme) { 2606 toolDefinition.bgTheme = "theme-toolbar"; 2607 } 2608 const panel = this.doc.createXULElement("vbox"); 2609 panel.className = "toolbox-panel " + toolDefinition.bgTheme; 2610 2611 // There is already a container for the webconsole frame. 2612 if (!this.doc.getElementById("toolbox-panel-" + id)) { 2613 panel.id = "toolbox-panel-" + id; 2614 } 2615 2616 deck.appendChild(panel); 2617 } 2618 2619 /** 2620 * Lazily created map of the additional tools registered to this toolbox. 2621 * 2622 * @returns {Map<string, object>} 2623 * a map of the tools definitions registered to this 2624 * particular toolbox (the key is the toolId string, the value 2625 * is the tool definition plain javascript object). 2626 */ 2627 get additionalToolDefinitions() { 2628 if (!this._additionalToolDefinitions) { 2629 this._additionalToolDefinitions = new Map(); 2630 } 2631 2632 return this._additionalToolDefinitions; 2633 } 2634 2635 /** 2636 * Retrieve the array of the additional tools registered to this toolbox. 2637 * 2638 * @return {Array<object>} 2639 * the array of additional tool definitions registered on this toolbox. 2640 */ 2641 getAdditionalTools() { 2642 if (this._additionalToolDefinitions) { 2643 return Array.from(this.additionalToolDefinitions.values()); 2644 } 2645 return []; 2646 } 2647 2648 /** 2649 * Get the additional tools that have been registered and are visible. 2650 * 2651 * @return {Array<object>} 2652 * the array of additional tool definitions registered on this toolbox. 2653 */ 2654 getVisibleAdditionalTools() { 2655 return this.visibleAdditionalTools.map(toolId => 2656 this.additionalToolDefinitions.get(toolId) 2657 ); 2658 } 2659 2660 /** 2661 * Test the existence of a additional tools registered to this toolbox by tool id. 2662 * 2663 * @param {string} toolId 2664 * the id of the tool to test for existence. 2665 * 2666 * @return {boolean} 2667 */ 2668 hasAdditionalTool(toolId) { 2669 return this.additionalToolDefinitions.has(toolId); 2670 } 2671 2672 /** 2673 * Register and load an additional tool on this particular toolbox. 2674 * 2675 * @param {object} definition 2676 * the additional tool definition to register and add to this toolbox. 2677 */ 2678 addAdditionalTool(definition) { 2679 if (!definition.id) { 2680 throw new Error("Tool definition id is missing"); 2681 } 2682 2683 if (this.isToolRegistered(definition.id)) { 2684 throw new Error("Tool definition already registered: " + definition.id); 2685 } 2686 2687 this.additionalToolDefinitions.set(definition.id, definition); 2688 this.visibleAdditionalTools = [ 2689 ...this.visibleAdditionalTools, 2690 definition.id, 2691 ]; 2692 2693 const buildPanel = () => this._buildPanelForTool(definition); 2694 2695 if (this.isReady) { 2696 buildPanel(); 2697 } else { 2698 this.once("ready", buildPanel); 2699 } 2700 } 2701 2702 /** 2703 * Retrieve the registered inspector extension sidebars 2704 * (used by the inspector panel during its deferred initialization). 2705 */ 2706 get inspectorExtensionSidebars() { 2707 return this._inspectorExtensionSidebars; 2708 } 2709 2710 /** 2711 * Register an extension sidebar for the inspector panel. 2712 * 2713 * @param {string} id 2714 * An unique sidebar id 2715 * @param {object} options 2716 * @param {string} options.title 2717 * A title for the sidebar 2718 */ 2719 async registerInspectorExtensionSidebar(id, options) { 2720 this._inspectorExtensionSidebars.set(id, options); 2721 2722 // Defer the extension sidebar creation if the inspector 2723 // has not been created yet (and do not create the inspector 2724 // only to register an extension sidebar). 2725 if (!this.target.getCachedFront("inspector")) { 2726 return; 2727 } 2728 2729 const inspector = this.getPanel("inspector"); 2730 if (!inspector) { 2731 return; 2732 } 2733 2734 inspector.addExtensionSidebar(id, options); 2735 } 2736 2737 /** 2738 * Unregister an extension sidebar for the inspector panel. 2739 * 2740 * @param {string} id 2741 * An unique sidebar id 2742 */ 2743 unregisterInspectorExtensionSidebar(id) { 2744 // Unregister the sidebar from the toolbox if the toolbox is not already 2745 // being destroyed (otherwise we would trigger a re-rendering of the 2746 // inspector sidebar tabs while the toolbox is going away). 2747 if (this._destroyer) { 2748 return; 2749 } 2750 2751 const sidebarDef = this._inspectorExtensionSidebars.get(id); 2752 if (!sidebarDef) { 2753 return; 2754 } 2755 2756 this._inspectorExtensionSidebars.delete(id); 2757 2758 // Remove the created sidebar instance if the inspector panel 2759 // has been already created. 2760 if (!this.target.getCachedFront("inspector")) { 2761 return; 2762 } 2763 2764 const inspector = this.getPanel("inspector"); 2765 inspector.removeExtensionSidebar(id); 2766 } 2767 2768 /** 2769 * Unregister and unload an additional tool from this particular toolbox. 2770 * 2771 * @param {string} toolId 2772 * the id of the additional tool to unregister and remove. 2773 */ 2774 removeAdditionalTool(toolId) { 2775 // Early exit if the toolbox is already destroying itself. 2776 if (this._destroyer) { 2777 return; 2778 } 2779 2780 if (!this.hasAdditionalTool(toolId)) { 2781 throw new Error( 2782 "Tool definition not registered to this toolbox: " + toolId 2783 ); 2784 } 2785 2786 this.additionalToolDefinitions.delete(toolId); 2787 this.visibleAdditionalTools = this.visibleAdditionalTools.filter( 2788 id => id !== toolId 2789 ); 2790 this.unloadTool(toolId); 2791 } 2792 2793 /** 2794 * Ensure the tool with the given id is loaded. 2795 * 2796 * @param {string} id 2797 * The id of the tool to load. 2798 * @param {object} options 2799 * Object that will be passed to the panel `open` method. 2800 */ 2801 loadTool(id, options) { 2802 let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); 2803 if (iframe) { 2804 const panel = this._toolPanels.get(id); 2805 return new Promise(resolve => { 2806 if (panel) { 2807 resolve(panel); 2808 } else { 2809 this.once(id + "-ready", initializedPanel => { 2810 resolve(initializedPanel); 2811 }); 2812 } 2813 }); 2814 } 2815 2816 return new Promise((resolve, reject) => { 2817 // Retrieve the tool definition (from the global or the per-toolbox tool maps) 2818 const definition = this.getToolDefinition(id); 2819 2820 if (!definition) { 2821 reject(new Error("no such tool id " + id)); 2822 return; 2823 } 2824 2825 iframe = this.doc.createXULElement("iframe"); 2826 iframe.className = "toolbox-panel-iframe"; 2827 iframe.id = "toolbox-panel-iframe-" + id; 2828 iframe.setAttribute("flex", 1); 2829 iframe.setAttribute("forceOwnRefreshDriver", ""); 2830 iframe.tooltip = "aHTMLTooltip"; 2831 2832 gDevTools.emit(id + "-init", this, iframe); 2833 this.emit(id + "-init", iframe); 2834 2835 const onLoad = async () => { 2836 // Try to set the dir attribute as early as possible. 2837 this.setIframeDocumentDir(iframe); 2838 2839 // The build method should return a panel instance, so events can 2840 // be fired with the panel as an argument. However, in order to keep 2841 // backward compatibility with existing extensions do a check 2842 // for a promise return value. 2843 let built = definition.build(iframe.contentWindow, this, this.commands); 2844 2845 if (!(typeof built.then == "function")) { 2846 const panel = built; 2847 iframe.panel = panel; 2848 2849 // The panel instance is expected to fire (and listen to) various 2850 // framework events, so make sure it's properly decorated with 2851 // appropriate API (on, off, once, emit). 2852 // In this case we decorate panel instances directly returned by 2853 // the tool definition 'build' method. 2854 if (typeof panel.emit == "undefined") { 2855 EventEmitter.decorate(panel); 2856 } 2857 2858 gDevTools.emit(id + "-build", this, panel); 2859 this.emit(id + "-build", panel); 2860 2861 // The panel can implement an 'open' method for asynchronous 2862 // initialization sequence. 2863 if (typeof panel.open == "function") { 2864 built = panel.open(options); 2865 } else { 2866 built = new Promise(resolve => { 2867 resolve(panel); 2868 }); 2869 } 2870 } 2871 2872 // Wait till the panel is fully ready and fire 'ready' events. 2873 Promise.resolve(built).then(panel => { 2874 this._toolPanels.set(id, panel); 2875 2876 // Make sure to decorate panel object with event API also in case 2877 // where the tool definition 'build' method returns only a promise 2878 // and the actual panel instance is available as soon as the 2879 // promise is resolved. 2880 if (typeof panel.emit == "undefined") { 2881 EventEmitter.decorate(panel); 2882 } 2883 2884 gDevTools.emit(id + "-ready", this, panel); 2885 this.emit(id + "-ready", panel); 2886 2887 resolve(panel); 2888 }, console.error); 2889 }; 2890 2891 iframe.setAttribute("src", definition.url); 2892 if (definition.panelLabel) { 2893 iframe.setAttribute("aria-label", definition.panelLabel); 2894 } 2895 2896 // If no parent yet, append the frame into default location. 2897 if (!iframe.parentNode) { 2898 const vbox = this.doc.getElementById("toolbox-panel-" + id); 2899 vbox.appendChild(iframe); 2900 } 2901 2902 // Depending on the host, iframe.contentWindow is not always 2903 // defined at this moment. If it is not defined, we use an 2904 // event listener on the iframe DOM node. If it's defined, 2905 // we use the chromeEventHandler. We can't use a listener 2906 // on the DOM node every time because this won't work 2907 // if the (xul chrome) iframe is loaded in a content docshell. 2908 if (iframe.contentWindow) { 2909 const loadingUrl = definition.url || "about:blank"; 2910 DOMHelpers.onceDOMReady(iframe.contentWindow, onLoad, loadingUrl); 2911 } else { 2912 const callback = () => { 2913 iframe.removeEventListener("DOMContentLoaded", callback); 2914 onLoad(); 2915 }; 2916 2917 iframe.addEventListener("DOMContentLoaded", callback); 2918 } 2919 }); 2920 } 2921 2922 /** 2923 * Set the dir attribute on the content document element of the provided iframe. 2924 * 2925 * @param {IFrameElement} iframe 2926 */ 2927 setIframeDocumentDir(iframe) { 2928 const docEl = iframe.contentWindow?.document.documentElement; 2929 if (!docEl || docEl.namespaceURI !== HTML_NS) { 2930 // Bail out if the content window or document is not ready or if the document is not 2931 // HTML. 2932 return; 2933 } 2934 2935 if (docEl.hasAttribute("dir")) { 2936 // Set the dir attribute value only if dir is already present on the document. 2937 docEl.setAttribute("dir", this.direction); 2938 } 2939 } 2940 2941 /** 2942 * Mark all in collection as unselected; and id as selected 2943 * 2944 * @param {string} collection 2945 * DOM collection of items 2946 * @param {string} id 2947 * The Id of the item within the collection to select 2948 */ 2949 selectSingleNode(collection, id) { 2950 [...collection].forEach(node => { 2951 if (node.id === id) { 2952 node.setAttribute("selected", "true"); 2953 node.setAttribute("aria-selected", "true"); 2954 } else { 2955 node.removeAttribute("selected"); 2956 node.removeAttribute("aria-selected"); 2957 } 2958 // The webconsole panel is in a special location due to split console 2959 if (!node.id) { 2960 node = this.webconsolePanel; 2961 } 2962 2963 const iframe = node.querySelector(".toolbox-panel-iframe"); 2964 if (iframe) { 2965 let visible = node.id == id; 2966 // Prevents hiding the split-console if it is currently enabled 2967 if (node == this.webconsolePanel && this.splitConsole) { 2968 visible = true; 2969 } 2970 this.setIframeVisible(iframe, visible); 2971 } 2972 }); 2973 } 2974 2975 /** 2976 * Make a privileged iframe visible/hidden. 2977 * 2978 * For now, XUL Iframes loading chrome documents (i.e. <iframe type!="content" />) 2979 * can't be hidden at platform level. And so don't support 'visibilitychange' event. 2980 * 2981 * This helper workarounds that by at least being able to send these kind of events. 2982 * It will help panel react differently depending on them being displayed or in 2983 * background. 2984 */ 2985 setIframeVisible(iframe, visible) { 2986 // Ideally, we would use <xul:browser type="content"> element in order to have top level BrowsingContexts 2987 // for each panel. But: 2988 // 1) It looks like nested <xul:browser type="content"> aren't creating top level BCs 2989 // 2) Using type="content" against panels breaks a few things. 2990 // Also see bug 1405342 for outdated approach. 2991 // 2992 // So keep using the following workaround which only fakes `visibilitychange` 2993 // enough to make the `visiblityChangeHanderStore` to work. 2994 const win = iframe.contentWindow; 2995 const doc = win.document; 2996 if (visible && !this._visibleIframes.has(iframe)) { 2997 this._visibleIframes.add(iframe); 2998 2999 // Overload document's `visibilityState` attribute 3000 // Use defineProperty, as by default `document.visbilityState` is read only. 3001 Object.defineProperty(doc, "visibilityState", { 3002 get: () => { 3003 // Also acknowledge the toolbox visibility, which take the lead over 3004 // any panel visiblity 3005 return this.win?.browsingContext.isActive ? "visible" : "hidden"; 3006 }, 3007 configurable: true, 3008 }); 3009 } else if (!visible && this._visibleIframes.has(iframe)) { 3010 this._visibleIframes.delete(iframe); 3011 3012 Object.defineProperty(doc, "visibilityState", { 3013 value: "hidden", 3014 configurable: true, 3015 }); 3016 } else { 3017 return; 3018 } 3019 3020 // Fake the 'visibilitychange' event 3021 doc.dispatchEvent(new win.Event("visibilitychange")); 3022 } 3023 3024 /** 3025 * Switch to the tool with the given id 3026 * 3027 * @param {string} id 3028 * The id of the tool to switch to 3029 * @param {string} reason 3030 * Reason the tool was opened 3031 * @param {object} options 3032 * Object that will be passed to the panel 3033 */ 3034 selectTool(id, reason = "unknown", options) { 3035 this.emit("panel-changed"); 3036 3037 if (this.currentToolId == id) { 3038 const panel = this._toolPanels.get(id); 3039 if (panel) { 3040 // We have a panel instance, so the tool is already fully loaded. 3041 3042 // re-focus tool to get key events again 3043 this.focusTool(id); 3044 3045 // Return the existing panel in order to have a consistent return value. 3046 return Promise.resolve(panel); 3047 } 3048 // Otherwise, if there is no panel instance, it is still loading, 3049 // so we are racing another call to selectTool with the same id. 3050 return this.once("select").then(() => 3051 Promise.resolve(this._toolPanels.get(id)) 3052 ); 3053 } 3054 3055 if (!this.isReady) { 3056 throw new Error("Can't select tool, wait for toolbox 'ready' event"); 3057 } 3058 3059 // Check if the tool exists. 3060 if ( 3061 this.panelDefinitions.find(definition => definition.id === id) || 3062 id === "options" || 3063 this.additionalToolDefinitions.get(id) 3064 ) { 3065 if (this.currentToolId) { 3066 this.telemetry.toolClosed(this.currentToolId, this); 3067 } 3068 3069 this._pingTelemetrySelectTool(id, reason); 3070 } else { 3071 throw new Error("No tool found"); 3072 } 3073 3074 this.lastUsedToolId = this.currentToolId; 3075 this.currentToolId = id; 3076 this._refreshConsoleDisplay(); 3077 if (id != "options") { 3078 Services.prefs.setCharPref(this._prefs.LAST_TOOL, id); 3079 } 3080 3081 return this.loadTool(id, options).then(panel => { 3082 // If some other tool started being selected, 3083 // cancel any further operation, but still return the panel to the callsite. 3084 if (this.currentToolId != id) { 3085 return panel; 3086 } 3087 // Only select the panel once it is loaded to prevent showing it 3088 // while it is bootstrapping and prevent blinks 3089 const toolboxPanels = this.doc.querySelectorAll(".toolbox-panel"); 3090 this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id); 3091 3092 // focus the tool's frame to start receiving key events 3093 this.focusTool(id); 3094 3095 this.emit("select", id); 3096 this.emit(id + "-selected", panel); 3097 return panel; 3098 }); 3099 } 3100 3101 _pingTelemetrySelectTool(id, reason) { 3102 const width = Math.ceil(this.win.outerWidth / 50) * 50; 3103 const panelName = this.getTelemetryPanelNameOrOther(id); 3104 const prevPanelName = this.getTelemetryPanelNameOrOther(this.currentToolId); 3105 const cold = !this.getPanel(id); 3106 const pending = ["host", "width", "start_state", "panel_name", "cold"]; 3107 3108 // On first load this.currentToolId === undefined so we need to skip sending 3109 // a devtools.main.exit telemetry event. 3110 if (this.currentToolId) { 3111 this.telemetry.recordEvent("exit", prevPanelName, null, { 3112 host: this._hostType, 3113 width, 3114 panel_name: prevPanelName, 3115 next_panel: panelName, 3116 reason, 3117 }); 3118 } 3119 3120 this.telemetry.addEventProperties(this.topWindow, "open", "tools", null, { 3121 width, 3122 }); 3123 3124 if (id === "webconsole") { 3125 pending.push("message_count"); 3126 } 3127 3128 this.telemetry.preparePendingEvent(this, "enter", panelName, null, pending); 3129 3130 this.telemetry.addEventProperties(this, "enter", panelName, null, { 3131 host: this._hostType, 3132 start_state: reason, 3133 panel_name: panelName, 3134 cold, 3135 }); 3136 3137 if (reason !== "initial_panel") { 3138 const width = Math.ceil(this.win.outerWidth / 50) * 50; 3139 this.telemetry.addEventProperty( 3140 this, 3141 "enter", 3142 panelName, 3143 null, 3144 "width", 3145 width 3146 ); 3147 } 3148 3149 // Cold webconsole event message_count is handled in 3150 // devtools/client/webconsole/webconsole-wrapper.js 3151 if (!cold && id === "webconsole") { 3152 this.telemetry.addEventProperty( 3153 this, 3154 "enter", 3155 "webconsole", 3156 null, 3157 "message_count", 3158 0 3159 ); 3160 } 3161 3162 this.telemetry.toolOpened(id, this); 3163 } 3164 3165 /** 3166 * Focus a tool's panel by id 3167 * 3168 * @param {string} id 3169 * The id of tool to focus 3170 */ 3171 focusTool(id, state = true) { 3172 const iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); 3173 3174 if (state) { 3175 iframe.focus(); 3176 } else { 3177 iframe.blur(); 3178 } 3179 } 3180 3181 /** 3182 * Focus split console's input line 3183 */ 3184 focusConsoleInput() { 3185 const consolePanel = this.getPanel("webconsole"); 3186 if (consolePanel) { 3187 consolePanel.focusInput(); 3188 } 3189 } 3190 3191 /** 3192 * Disable all network logs in the console 3193 */ 3194 disableAllConsoleNetworkLogs() { 3195 const consolePanel = this.getPanel("webconsole"); 3196 if (consolePanel) { 3197 consolePanel.hud.ui.disableAllNetworkMessages(); 3198 } 3199 } 3200 3201 /** 3202 * If the console is split and we are focusing an element outside 3203 * of the console, then store the newly focused element, so that 3204 * it can be restored once the split console closes. 3205 * 3206 * @param Element originalTarget 3207 * The DOM Element that just got focused. 3208 */ 3209 _updateLastFocusedElementForSplitConsole(originalTarget) { 3210 // Ignore any non element nodes, or any elements contained 3211 // within the webconsole frame. 3212 const webconsoleURL = gDevTools.getToolDefinition("webconsole").url; 3213 if ( 3214 originalTarget.nodeType !== 1 || 3215 originalTarget.baseURI === webconsoleURL 3216 ) { 3217 return; 3218 } 3219 3220 this._lastFocusedElement = originalTarget; 3221 } 3222 3223 // Report if the toolbox is currently focused, 3224 // or the focus in elsewhere in the browser or another app. 3225 _isToolboxFocused = false; 3226 3227 _onFocus({ originalTarget }) { 3228 this._isToolboxFocused = true; 3229 this._debounceUpdateFocusedState(); 3230 3231 this._updateLastFocusedElementForSplitConsole(originalTarget); 3232 } 3233 3234 _onBlur() { 3235 this._isToolboxFocused = false; 3236 this._debounceUpdateFocusedState(); 3237 } 3238 3239 _onTabsOrderUpdated() { 3240 this._combineAndSortPanelDefinitions(); 3241 } 3242 3243 /** 3244 * Opens the split console. 3245 * 3246 * @param {boolean} focusConsoleInput 3247 * By default, the console input will be focused. 3248 * Pass false in order to prevent this. 3249 * 3250 * @returns {Promise} a promise that resolves once the tool has been 3251 * loaded and focused. 3252 */ 3253 openSplitConsole({ focusConsoleInput = true } = {}) { 3254 if (!this.isSplitConsoleEnabled()) { 3255 return this.selectTool( 3256 "webconsole", 3257 "use_in_console_with_disabled_split_console" 3258 ); 3259 } 3260 3261 this._splitConsole = true; 3262 Services.prefs.setBoolPref(SPLITCONSOLE_OPEN_PREF, true); 3263 this._refreshConsoleDisplay(); 3264 3265 // Ensure split console is visible if console was already loaded in background 3266 const iframe = this.webconsolePanel.querySelector(".toolbox-panel-iframe"); 3267 if (iframe) { 3268 this.setIframeVisible(iframe, true); 3269 } 3270 3271 return this.loadTool("webconsole").then(() => { 3272 if (!this.component) { 3273 return; 3274 } 3275 this.component.setIsSplitConsoleActive(true); 3276 this.telemetry.recordEvent("activate", "split_console", null, { 3277 host: this._getTelemetryHostString(), 3278 width: Math.ceil(this.win.outerWidth / 50) * 50, 3279 }); 3280 this.emit("split-console"); 3281 if (focusConsoleInput) { 3282 this.focusConsoleInput(); 3283 } 3284 }); 3285 } 3286 3287 /** 3288 * Closes the split console. 3289 * 3290 * @returns {Promise} a promise that resolves once the tool has been 3291 * closed. 3292 */ 3293 closeSplitConsole() { 3294 this._splitConsole = false; 3295 Services.prefs.setBoolPref(SPLITCONSOLE_OPEN_PREF, false); 3296 this._saveSplitConsoleHeight(); 3297 3298 this._refreshConsoleDisplay(); 3299 this.component.setIsSplitConsoleActive(false); 3300 3301 this.telemetry.recordEvent("deactivate", "split_console", null, { 3302 host: this._getTelemetryHostString(), 3303 width: Math.ceil(this.win.outerWidth / 50) * 50, 3304 }); 3305 3306 this.emit("split-console"); 3307 3308 if (this._lastFocusedElement) { 3309 this._lastFocusedElement.focus(); 3310 } 3311 return Promise.resolve(); 3312 } 3313 3314 /** 3315 * Toggles the split state of the webconsole. If the webconsole panel 3316 * is already selected then this command is ignored. 3317 * 3318 * @returns {Promise} a promise that resolves once the tool has been 3319 * opened or closed. 3320 */ 3321 toggleSplitConsole() { 3322 if (this.currentToolId !== "webconsole") { 3323 return this.splitConsole 3324 ? this.closeSplitConsole() 3325 : this.openSplitConsole(); 3326 } 3327 3328 return Promise.resolve(); 3329 } 3330 3331 /** 3332 * Toggles the options panel. 3333 * If the option panel is already selected then select the last selected panel. 3334 */ 3335 toggleOptions(event) { 3336 // Flip back to the last used panel if we are already 3337 // on the options panel. 3338 if ( 3339 this.currentToolId === "options" && 3340 gDevTools.getToolDefinition(this.lastUsedToolId) 3341 ) { 3342 this.selectTool(this.lastUsedToolId, "toggle_settings_off"); 3343 } else { 3344 this.selectTool("options", "toggle_settings_on"); 3345 } 3346 3347 // preventDefault will avoid a Linux only bug when the focus is on a text input 3348 // See Bug 1519087. 3349 event.preventDefault(); 3350 } 3351 3352 /** 3353 * Loads the tool next to the currently selected tool. 3354 */ 3355 selectNextTool() { 3356 const definitions = this.component.panelDefinitions; 3357 const index = definitions.findIndex(({ id }) => id === this.currentToolId); 3358 const definition = 3359 index === -1 || index >= definitions.length - 1 3360 ? definitions[0] 3361 : definitions[index + 1]; 3362 return this.selectTool(definition.id, "select_next_key"); 3363 } 3364 3365 /** 3366 * Loads the tool just left to the currently selected tool. 3367 */ 3368 selectPreviousTool() { 3369 const definitions = this.component.panelDefinitions; 3370 const index = definitions.findIndex(({ id }) => id === this.currentToolId); 3371 const definition = 3372 index === -1 || index < 1 3373 ? definitions[definitions.length - 1] 3374 : definitions[index - 1]; 3375 return this.selectTool(definition.id, "select_prev_key"); 3376 } 3377 3378 /** 3379 * Tells if the given tool is currently highlighted. 3380 * (doesn't mean selected, its tab header will be green) 3381 * 3382 * @param {string} id 3383 * The id of the tool to check. 3384 */ 3385 isHighlighted(id) { 3386 return this.component.state.highlightedTools.has(id); 3387 } 3388 3389 /** 3390 * Highlights the tool's tab if it is not the currently selected tool. 3391 * 3392 * @param {string} id 3393 * The id of the tool to highlight 3394 */ 3395 async highlightTool(id) { 3396 if (!this.component) { 3397 await this.isOpen; 3398 } 3399 this.component.highlightTool(id); 3400 } 3401 3402 /** 3403 * De-highlights the tool's tab. 3404 * 3405 * @param {string} id 3406 * The id of the tool to unhighlight 3407 */ 3408 async unhighlightTool(id) { 3409 if (!this.component) { 3410 await this.isOpen; 3411 } 3412 this.component.unhighlightTool(id); 3413 } 3414 3415 /** 3416 * Raise the toolbox host. 3417 */ 3418 raise() { 3419 this.postMessage({ name: "raise-host" }); 3420 3421 return this.once("host-raised"); 3422 } 3423 3424 /** 3425 * Fired when user just started navigating away to another web page. 3426 */ 3427 async _onWillNavigate({ isFrameSwitching } = {}) { 3428 // On navigate, the server will resume all paused threads, but due to an 3429 // issue which can cause loosing outgoing messages/RDP packets, the THREAD_STATE 3430 // resources for the resumed state might not get received. So let assume it happens 3431 // make use the UI is the appropriate state. 3432 if (this._pausedTargets.size > 0) { 3433 this.emit("toolbox-resumed"); 3434 this._pausedTargets.clear(); 3435 if (this.isHighlighted("jsdebugger")) { 3436 this.unhighlightTool("jsdebugger"); 3437 } 3438 } 3439 3440 // Clearing the error count and the iframe list as soon as we navigate 3441 this.setErrorCount(0); 3442 if (!isFrameSwitching) { 3443 this._updateFrames({ destroyAll: true }); 3444 } 3445 this.updateToolboxButtons(); 3446 const toolId = this.currentToolId; 3447 // For now, only inspector, webconsole, netmonitor and accessibility fire "reloaded" event 3448 if ( 3449 toolId != "inspector" && 3450 toolId != "webconsole" && 3451 toolId != "netmonitor" && 3452 toolId != "accessibility" 3453 ) { 3454 return; 3455 } 3456 3457 const start = this.win.performance.now(); 3458 const panel = this.getPanel(toolId); 3459 // Ignore the timing if the panel is still loading 3460 if (!panel) { 3461 return; 3462 } 3463 3464 await panel.once("reloaded"); 3465 // The toolbox may have been destroyed while the panel was reloading 3466 if (this.isDestroying()) { 3467 return; 3468 } 3469 const delay = this.win.performance.now() - start; 3470 Glean.devtools.toolboxPageReloadDelay[toolId].accumulateSingleSample(delay); 3471 } 3472 3473 /** 3474 * Refresh the host's title. 3475 */ 3476 _refreshHostTitle() { 3477 let title; 3478 3479 const { selectedTargetFront } = this.commands.targetCommand; 3480 if (this.target.isXpcShellTarget) { 3481 // This will only be displayed for local development and can remain 3482 // hardcoded in english. 3483 title = "XPCShell Toolbox"; 3484 } else if (this.isMultiProcessBrowserToolbox) { 3485 const scope = Services.prefs.getCharPref(BROWSERTOOLBOX_SCOPE_PREF); 3486 if (scope == BROWSERTOOLBOX_SCOPE_EVERYTHING) { 3487 title = L10N.getStr("toolbox.multiProcessBrowserToolboxTitle"); 3488 } else if (scope == BROWSERTOOLBOX_SCOPE_PARENTPROCESS) { 3489 title = L10N.getStr("toolbox.parentProcessBrowserToolboxTitle"); 3490 } else { 3491 throw new Error("Unsupported scope: " + scope); 3492 } 3493 } else if ( 3494 selectedTargetFront.name && 3495 selectedTargetFront.name != selectedTargetFront.url 3496 ) { 3497 // For Web Extensions, the target name may only be the pathname of the target URL. 3498 // In such case, only print the absolute target url. 3499 if ( 3500 this._descriptorFront.isWebExtensionDescriptor && 3501 selectedTargetFront.url.includes(selectedTargetFront.name) 3502 ) { 3503 title = L10N.getFormatStr( 3504 "toolbox.titleTemplate1", 3505 getUnicodeUrl(selectedTargetFront.url) 3506 ); 3507 } else { 3508 title = L10N.getFormatStr( 3509 "toolbox.titleTemplate2", 3510 selectedTargetFront.name, 3511 getUnicodeUrl(selectedTargetFront.url) 3512 ); 3513 } 3514 } else { 3515 title = L10N.getFormatStr( 3516 "toolbox.titleTemplate1", 3517 getUnicodeUrl(selectedTargetFront.url) 3518 ); 3519 } 3520 this.postMessage({ 3521 name: "set-host-title", 3522 title, 3523 }); 3524 } 3525 3526 /** 3527 * For a given URL, return its pathname. 3528 * This is handy for Web Extension as it should be the addon ID. 3529 * 3530 * @param {string} url 3531 * @return {string} pathname 3532 */ 3533 getExtensionPathName(url) { 3534 const parsedURL = URL.parse(url); 3535 if (!parsedURL) { 3536 // Return the url if unable to resolve the pathname. 3537 return url; 3538 } 3539 // Only moz-extension URL should be shortened into the URL pathname. 3540 if (!lazy.ExtensionUtils.isExtensionUrl(parsedURL)) { 3541 return url; 3542 } 3543 return parsedURL.pathname; 3544 } 3545 3546 /** 3547 * Returns an instance of the preference actor. This is a lazily initialized root 3548 * actor that persists preferences to the debuggee, instead of just to the DevTools 3549 * client. See the definition of the preference actor for more information. 3550 */ 3551 get preferenceFront() { 3552 if (!this._preferenceFrontRequest) { 3553 // Set the _preferenceFrontRequest property to allow the resetPreference toolbox 3554 // method to cleanup the preference set when the toolbox is closed. 3555 this._preferenceFrontRequest = 3556 this.commands.client.mainRoot.getFront("preference"); 3557 } 3558 return this._preferenceFrontRequest; 3559 } 3560 3561 /** 3562 * See: https://firefox-source-docs.mozilla.org/l10n/fluent/tutorial.html#manually-testing-ui-with-pseudolocalization 3563 * 3564 * @param {"bidi" | "accented" | "none"} pseudoLocale 3565 */ 3566 async changePseudoLocale(pseudoLocale) { 3567 await this.isOpen; 3568 const prefFront = await this.preferenceFront; 3569 if (pseudoLocale === "none") { 3570 await prefFront.clearUserPref(PSEUDO_LOCALE_PREF); 3571 } else { 3572 await prefFront.setCharPref(PSEUDO_LOCALE_PREF, pseudoLocale); 3573 } 3574 this.component.setPseudoLocale(pseudoLocale); 3575 this._pseudoLocaleChanged = true; 3576 } 3577 3578 /** 3579 * Returns the pseudo-locale when the target is browser chrome, otherwise undefined. 3580 * 3581 * @returns {"bidi" | "accented" | "none" | undefined} 3582 */ 3583 async getPseudoLocale() { 3584 if (!this.isBrowserToolbox) { 3585 return undefined; 3586 } 3587 3588 const prefFront = await this.preferenceFront; 3589 const locale = await prefFront.getCharPref(PSEUDO_LOCALE_PREF); 3590 3591 switch (locale) { 3592 case "bidi": 3593 case "accented": 3594 return locale; 3595 default: 3596 return "none"; 3597 } 3598 } 3599 3600 async toggleNoAutohide() { 3601 const front = await this.preferenceFront; 3602 3603 const toggledValue = !(await this._isDisableAutohideEnabled()); 3604 3605 front.setBoolPref(DISABLE_AUTOHIDE_PREF, toggledValue); 3606 3607 if ( 3608 this.isBrowserToolbox || 3609 this._descriptorFront.isWebExtensionDescriptor 3610 ) { 3611 this.component.setDisableAutohide(toggledValue); 3612 } 3613 this._autohideHasBeenToggled = true; 3614 } 3615 3616 /** 3617 * Toggling "always on top" behavior is a bit special. 3618 * 3619 * We toggle the preference and then destroy and re-create the toolbox 3620 * as there is no way to change this behavior on an existing window 3621 * (see bug 1788946). 3622 */ 3623 async toggleAlwaysOnTop() { 3624 const currentValue = Services.prefs.getBoolPref( 3625 DEVTOOLS_ALWAYS_ON_TOP, 3626 false 3627 ); 3628 Services.prefs.setBoolPref(DEVTOOLS_ALWAYS_ON_TOP, !currentValue); 3629 3630 const addonId = this._descriptorFront.id; 3631 await this.destroy(); 3632 gDevTools.showToolboxForWebExtension(addonId); 3633 } 3634 3635 async _isDisableAutohideEnabled() { 3636 if ( 3637 !this.isBrowserToolbox && 3638 !this._descriptorFront.isWebExtensionDescriptor 3639 ) { 3640 return false; 3641 } 3642 3643 const prefFront = await this.preferenceFront; 3644 return prefFront.getBoolPref(DISABLE_AUTOHIDE_PREF); 3645 } 3646 3647 async _listFrames() { 3648 if ( 3649 !this.target.getTrait("frames") || 3650 this.target.targetForm.ignoreSubFrames 3651 ) { 3652 // We are not targetting a regular WindowGlobalTargetActor (it can be either an 3653 // addon or browser toolbox actor), or EFT is enabled. 3654 return; 3655 } 3656 3657 try { 3658 const { frames } = await this.target.listFrames(); 3659 this._updateFrames({ frames }); 3660 } catch (e) { 3661 console.error("Error while listing frames", e); 3662 } 3663 } 3664 3665 /** 3666 * Called by the iframe picker when the user selected a frame. 3667 * 3668 * @param {string} frameIdOrTargetActorId 3669 */ 3670 onIframePickerFrameSelected(frameIdOrTargetActorId) { 3671 if (!this.frameMap.has(frameIdOrTargetActorId)) { 3672 console.error( 3673 `Can't focus on frame "${frameIdOrTargetActorId}", it is not a known frame` 3674 ); 3675 return; 3676 } 3677 3678 const frameInfo = this.frameMap.get(frameIdOrTargetActorId); 3679 // If there is no targetFront in the frameData, this means EFT is not enabled. 3680 // Send packet to the backend to select specified frame and wait for 'frameUpdate' 3681 // event packet to update the UI. 3682 if (!frameInfo.targetFront) { 3683 this.target.switchToFrame({ windowId: frameIdOrTargetActorId }); 3684 return; 3685 } 3686 3687 // Here, EFT is enabled, so we want to focus the toolbox on the specific targetFront 3688 // that was selected by the user. This will trigger this._onTargetSelected which will 3689 // take care of updating the iframe picker state. 3690 this.commands.targetCommand.selectTarget(frameInfo.targetFront); 3691 } 3692 3693 /** 3694 * Highlight a frame in the page 3695 * 3696 * @param {string} frameIdOrTargetActorId 3697 */ 3698 async onHighlightFrame(frameIdOrTargetActorId) { 3699 // Only enable frame highlighting when the top level document is targeted 3700 if (!this.rootFrameSelected) { 3701 return null; 3702 } 3703 3704 const frameInfo = this.frameMap.get(frameIdOrTargetActorId); 3705 if (!frameInfo) { 3706 return null; 3707 } 3708 3709 let nodeFront; 3710 if (frameInfo.targetFront) { 3711 const inspectorFront = await frameInfo.targetFront.getFront("inspector"); 3712 nodeFront = await inspectorFront.walker.documentElement(); 3713 } else { 3714 const inspectorFront = await this.target.getFront("inspector"); 3715 nodeFront = await inspectorFront.walker.getNodeActorFromWindowID( 3716 frameIdOrTargetActorId 3717 ); 3718 } 3719 const highlighter = this.getHighlighter(); 3720 return highlighter.highlight(nodeFront); 3721 } 3722 3723 /** 3724 * Handles changes in document frames. 3725 * 3726 * @param {object} data 3727 * @param {boolean} data.destroyAll: All frames have been destroyed. 3728 * @param {number} data.selected: A frame has been selected 3729 * @param {object} data.frameData: Some frame data were updated 3730 * @param {string} data.frameData.url: new frame URL (it might have been blank or about:blank) 3731 * @param {string} data.frameData.title: new frame title 3732 * @param {number | string} data.frameData.id: frame ID / targetFront actorID when EFT is enabled. 3733 * @param {Array<object>} data.frames: List of frames. Every frame can have: 3734 * @param {number | string} data.frames[].id: frame ID / targetFront actorID when EFT is enabled. 3735 * @param {string} data.frames[].url: frame URL 3736 * @param {string} data.frames[].title: frame title 3737 * @param {boolean} data.frames[].destroy: Set to true if destroyed 3738 * @param {boolean} data.frames[].isTopLevel: true for top level window 3739 */ 3740 _updateFrames(data) { 3741 // At the moment, frames `id` can either be outerWindowID (a Number), 3742 // or a targetActorID (a String). 3743 // In order to have the same type of data as a key of `frameMap`, we transform any 3744 // outerWindowID into a string. 3745 // This can be removed once EFT is enabled by default 3746 if (data.selected) { 3747 data.selected = data.selected.toString(); 3748 } else if (data.frameData) { 3749 data.frameData.id = data.frameData.id.toString(); 3750 } else if (data.frames) { 3751 data.frames.forEach(frame => { 3752 if (frame.id) { 3753 frame.id = frame.id.toString(); 3754 } 3755 }); 3756 } 3757 3758 // Store (synchronize) data about all existing frames on the backend 3759 if (data.destroyAll) { 3760 this.frameMap.clear(); 3761 this.selectedFrameId = null; 3762 } else if (data.selected) { 3763 // If we select the top level target, default back to no particular selected document. 3764 if (data.selected == this.target.actorID) { 3765 this.selectedFrameId = null; 3766 } else { 3767 this.selectedFrameId = data.selected; 3768 } 3769 } else if (data.frameData && this.frameMap.has(data.frameData.id)) { 3770 const existingFrameData = this.frameMap.get(data.frameData.id); 3771 if ( 3772 existingFrameData.title == data.frameData.title && 3773 existingFrameData.url == data.frameData.url 3774 ) { 3775 return; 3776 } 3777 3778 this.frameMap.set(data.frameData.id, { 3779 ...existingFrameData, 3780 url: data.frameData.url, 3781 title: data.frameData.title, 3782 }); 3783 } else if (data.frames) { 3784 data.frames.forEach(frame => { 3785 if (frame.destroy) { 3786 this.frameMap.delete(frame.id); 3787 3788 // Reset the currently selected frame if it's destroyed. 3789 if (this.selectedFrameId == frame.id) { 3790 this.selectedFrameId = null; 3791 } 3792 } else { 3793 this.frameMap.set(frame.id, frame); 3794 } 3795 }); 3796 } 3797 3798 // If there is no selected frame select the first top level 3799 // frame by default. Note that there might be more top level 3800 // frames in case of the BrowserToolbox. 3801 if (!this.selectedFrameId) { 3802 const frames = [...this.frameMap.values()]; 3803 const topFrames = frames.filter(frame => frame.isTopLevel); 3804 this.selectedFrameId = topFrames.length ? topFrames[0].id : null; 3805 } 3806 3807 // Debounce the update to avoid unnecessary flickering/rendering. 3808 if (!this.debouncedToolbarUpdate) { 3809 this.debouncedToolbarUpdate = debounce( 3810 () => { 3811 // Toolbox may have been destroyed in the meantime 3812 if (this.component) { 3813 this.component.setToolboxButtons(this.toolbarButtons); 3814 } 3815 this.debouncedToolbarUpdate = null; 3816 }, 3817 200, 3818 this 3819 ); 3820 } 3821 3822 const updateUiElements = () => { 3823 // We may need to hide/show the frames button now. 3824 this.updateFrameButton(); 3825 3826 if (this.debouncedToolbarUpdate) { 3827 this.debouncedToolbarUpdate(); 3828 } 3829 }; 3830 3831 // This may have been called before the toolbox is ready (= the dom elements for 3832 // the iframe picker don't exist yet). 3833 if (!this.isReady) { 3834 this.once("ready").then(() => updateUiElements); 3835 } else { 3836 updateUiElements(); 3837 } 3838 } 3839 3840 /** 3841 * Returns whether a root frame (with no parent frame) is selected. 3842 */ 3843 get rootFrameSelected() { 3844 // If the frame switcher is disabled, we won't have a selected frame ID. 3845 // In this case, we're always showing the root frame. 3846 if (!this.selectedFrameId) { 3847 return true; 3848 } 3849 3850 return this.frameMap.get(this.selectedFrameId).isTopLevel; 3851 } 3852 3853 /** 3854 * Switch to the last used host for the toolbox UI. 3855 */ 3856 switchToPreviousHost() { 3857 return this.switchHost("previous"); 3858 } 3859 3860 /** 3861 * Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window, 3862 * and focus the window when done. 3863 * 3864 * @param {string} hostType 3865 * The host type of the new host object 3866 */ 3867 switchHost(hostType) { 3868 if (hostType == this.hostType || !this._descriptorFront.isLocalTab) { 3869 return null; 3870 } 3871 3872 // chromeEventHandler will change after swapping hosts, remove events relying on it. 3873 this._removeChromeEventHandlerEvents(); 3874 3875 this.emit("host-will-change", hostType); 3876 3877 // ToolboxHostManager is going to call swapFrameLoaders which mess up with 3878 // focus. We have to blur before calling it in order to be able to restore 3879 // the focus after, in _onSwitchedHost. 3880 this.focusTool(this.currentToolId, false); 3881 3882 // Host code on the chrome side will send back a message once the host 3883 // switched 3884 this.postMessage({ 3885 name: "switch-host", 3886 hostType, 3887 }); 3888 3889 return this.once("host-changed"); 3890 } 3891 3892 /** 3893 * Request to Firefox UI to move the toolbox to another tab. 3894 * This is used when we move a toolbox to a new popup opened by the tab we were currently debugging. 3895 * We also move the toolbox back to the original tab we were debugging if we select it via Firefox tabs. 3896 * 3897 * @param {string} tabBrowsingContextID 3898 * The BrowsingContext ID of the tab we want to move to. 3899 * @returns {Promise<undefined>} 3900 * This will resolve only once we moved to the new tab. 3901 */ 3902 switchHostToTab(tabBrowsingContextID) { 3903 this.postMessage({ 3904 name: "switch-host-to-tab", 3905 tabBrowsingContextID, 3906 }); 3907 3908 return this.once("switched-host-to-tab"); 3909 } 3910 3911 _onSwitchedHost({ hostType }) { 3912 this._hostType = hostType; 3913 3914 this._buildDockOptions(); 3915 3916 // chromeEventHandler changed after swapping hosts, add again events relying on it. 3917 this._addChromeEventHandlerEvents(); 3918 3919 // We blurred the tools at start of switchHost, but also when clicking on 3920 // host switching button. We now have to restore the focus. 3921 this.focusTool(this.currentToolId, true); 3922 3923 this.emit("host-changed"); 3924 Glean.devtools.toolboxHost.accumulateSingleSample( 3925 this._getTelemetryHostId() 3926 ); 3927 3928 this.component.setCurrentHostType(hostType); 3929 } 3930 3931 /** 3932 * Event handler fired when the toolbox was moved to another tab. 3933 * This fires when the toolbox itself requests to be moved to another tab, 3934 * but also when we select the original tab where the toolbox originally was. 3935 * 3936 * @param {string} browsingContextID 3937 * The BrowsingContext ID of the tab the toolbox has been moved to. 3938 */ 3939 _onSwitchedHostToTab(browsingContextID) { 3940 const targets = this.commands.targetCommand.getAllTargets([ 3941 this.commands.targetCommand.TYPES.FRAME, 3942 ]); 3943 const target = targets.find( 3944 target => target.browsingContextID == browsingContextID 3945 ); 3946 3947 this.commands.targetCommand.selectTarget(target); 3948 3949 this.emit("switched-host-to-tab"); 3950 } 3951 3952 /** 3953 * Test the availability of a tool (both globally registered tools and 3954 * additional tools registered to this toolbox) by tool id. 3955 * 3956 * @param {string} toolId 3957 * Id of the tool definition to search in the per-toolbox or globally 3958 * registered tools. 3959 * 3960 * @returns {bool} 3961 * Returns true if the tool is registered globally or on this toolbox. 3962 */ 3963 isToolRegistered(toolId) { 3964 return !!this.getToolDefinition(toolId); 3965 } 3966 3967 /** 3968 * Return the tool definition registered globally or additional tools registered 3969 * to this toolbox. 3970 * 3971 * @param {string} toolId 3972 * Id of the tool definition to retrieve for the per-toolbox and globally 3973 * registered tools. 3974 * 3975 * @returns {object} 3976 * The plain javascript object that represents the requested tool definition. 3977 */ 3978 getToolDefinition(toolId) { 3979 return ( 3980 gDevTools.getToolDefinition(toolId) || 3981 this.additionalToolDefinitions.get(toolId) 3982 ); 3983 } 3984 3985 /** 3986 * Internal helper that removes a loaded tool from the toolbox, 3987 * it removes a loaded tool panel and tab from the toolbox without removing 3988 * its definition, so that it can still be listed in options and re-added later. 3989 * 3990 * @param {string} toolId 3991 * Id of the tool to be removed. 3992 */ 3993 unloadTool(toolId) { 3994 if (typeof toolId != "string") { 3995 throw new Error("Unexpected non-string toolId received."); 3996 } 3997 3998 if (this._toolPanels.has(toolId)) { 3999 const instance = this._toolPanels.get(toolId); 4000 instance.destroy(); 4001 this._toolPanels.delete(toolId); 4002 } 4003 4004 const panel = this.doc.getElementById("toolbox-panel-" + toolId); 4005 4006 // Select another tool. 4007 if (this.currentToolId == toolId) { 4008 const index = this.panelDefinitions.findIndex(({ id }) => id === toolId); 4009 const nextTool = this.panelDefinitions[index + 1]; 4010 const previousTool = this.panelDefinitions[index - 1]; 4011 let toolNameToSelect; 4012 4013 if (nextTool) { 4014 toolNameToSelect = nextTool.id; 4015 } 4016 if (previousTool) { 4017 toolNameToSelect = previousTool.id; 4018 } 4019 if (toolNameToSelect) { 4020 this.selectTool(toolNameToSelect, "tool_unloaded"); 4021 } 4022 } 4023 4024 // Remove this tool from the current panel definitions. 4025 this.panelDefinitions = this.panelDefinitions.filter( 4026 ({ id }) => id !== toolId 4027 ); 4028 this.visibleAdditionalTools = this.visibleAdditionalTools.filter( 4029 id => id !== toolId 4030 ); 4031 this._combineAndSortPanelDefinitions(); 4032 4033 if (panel) { 4034 panel.remove(); 4035 } 4036 4037 if (this.hostType == Toolbox.HostType.WINDOW) { 4038 const doc = this.win.parent.document; 4039 const key = doc.getElementById("key_" + toolId); 4040 if (key) { 4041 key.remove(); 4042 } 4043 } 4044 } 4045 4046 /** 4047 * Handler for the tool-registered event. 4048 * 4049 * @param {string} toolId 4050 * Id of the tool that was registered 4051 */ 4052 _toolRegistered(toolId) { 4053 // Tools can either be in the global devtools, or added to this specific toolbox 4054 // as an additional tool. 4055 let definition = gDevTools.getToolDefinition(toolId); 4056 let isAdditionalTool = false; 4057 if (!definition) { 4058 definition = this.additionalToolDefinitions.get(toolId); 4059 isAdditionalTool = true; 4060 } 4061 4062 if (definition.isToolSupported(this)) { 4063 if (isAdditionalTool) { 4064 this.visibleAdditionalTools = [...this.visibleAdditionalTools, toolId]; 4065 this._combineAndSortPanelDefinitions(); 4066 } else { 4067 this.panelDefinitions = this.panelDefinitions.concat(definition); 4068 } 4069 this._buildPanelForTool(definition); 4070 4071 // Emit the event so tools can listen to it from the toolbox level 4072 // instead of gDevTools. 4073 this.emit("tool-registered", toolId); 4074 } 4075 } 4076 4077 /** 4078 * Handler for the tool-unregistered event. 4079 * 4080 * @param {string} toolId 4081 * id of the tool that was unregistered 4082 */ 4083 _toolUnregistered(toolId) { 4084 this.unloadTool(toolId); 4085 4086 // Emit the event so tools can listen to it from the toolbox level 4087 // instead of gDevTools 4088 this.emit("tool-unregistered", toolId); 4089 } 4090 4091 /** 4092 * A helper function that returns an object containing methods to show and hide the 4093 * Box Model Highlighter on a given NodeFront or node grip (object with metadata which 4094 * can be used to obtain a NodeFront for a node), as well as helpers to listen to the 4095 * higligher show and hide events. The event helpers are used in tests where it is 4096 * cumbersome to load the Inspector panel in order to listen to highlighter events. 4097 * 4098 * @returns {object} an object of the following shape: 4099 * - {AsyncFunction} highlight: A function that will show a Box Model Highlighter 4100 * for the provided NodeFront or node grip. 4101 * - {AsyncFunction} unhighlight: A function that will hide any Box Model Highlighter 4102 * that is visible. If the `highlight` promise isn't settled yet, 4103 * it will wait until it's done and then unhighlight to prevent 4104 * zombie highlighters. 4105 * - {AsyncFunction} waitForHighlighterShown: Returns a promise which resolves with 4106 * the "highlighter-shown" event data once the highlighter is shown. 4107 * - {AsyncFunction} waitForHighlighterHidden: Returns a promise which resolves with 4108 * the "highlighter-hidden" event data once the highlighter is 4109 * hidden. 4110 */ 4111 getHighlighter() { 4112 let pendingHighlight; 4113 4114 /** 4115 * Return a promise wich resolves with a reference to the Inspector panel. 4116 * 4117 * @param {object} options: Options that will be passed to the inspector initialization 4118 */ 4119 const _getInspector = async options => { 4120 const inspector = this.getPanel("inspector"); 4121 if (inspector) { 4122 return inspector; 4123 } 4124 4125 return this.loadTool("inspector", options); 4126 }; 4127 4128 /** 4129 * Returns a promise which resolves when a Box Model Highlighter emits the given event 4130 * 4131 * @param {string} eventName 4132 * Name of the event to listen to. 4133 * @return {Promise} 4134 * Promise which resolves when the highlighter event occurs. 4135 * Resolves with the data payload attached to the event. 4136 */ 4137 async function _waitForHighlighterEvent(eventName) { 4138 const inspector = await _getInspector(); 4139 return new Promise(resolve => { 4140 function _handler(data) { 4141 if (data.type === inspector.highlighters.TYPES.BOXMODEL) { 4142 inspector.highlighters.off(eventName, _handler); 4143 resolve(data); 4144 } 4145 } 4146 4147 inspector.highlighters.on(eventName, _handler); 4148 }); 4149 } 4150 4151 return { 4152 // highlight might be triggered right before a test finishes. Wrap it 4153 // with safeAsyncMethod to avoid intermittents. 4154 highlight: this._safeAsyncAfterDestroy(async (object, options) => { 4155 pendingHighlight = (async () => { 4156 let nodeFront = object; 4157 4158 if (!(nodeFront instanceof NodeFront)) { 4159 const inspectorFront = await this.target.getFront("inspector"); 4160 nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(object); 4161 } 4162 4163 if (!nodeFront) { 4164 return null; 4165 } 4166 4167 const inspector = await _getInspector({ 4168 // if the inspector wasn't initialized yet, this will ensure that we select 4169 // the highlighted node; otherwise the default selected node might be in 4170 // another thread, which will ultimately select this other thread in the 4171 // debugger, and might confuse users (see Bug 1837480) 4172 defaultStartupNode: nodeFront, 4173 }); 4174 return inspector.highlighters.showHighlighterTypeForNode( 4175 inspector.highlighters.TYPES.BOXMODEL, 4176 nodeFront, 4177 options 4178 ); 4179 })(); 4180 return pendingHighlight; 4181 }), 4182 unhighlight: this._safeAsyncAfterDestroy(async () => { 4183 if (pendingHighlight) { 4184 await pendingHighlight; 4185 pendingHighlight = null; 4186 } 4187 4188 const inspector = await _getInspector(); 4189 return inspector.highlighters.hideHighlighterType( 4190 inspector.highlighters.TYPES.BOXMODEL 4191 ); 4192 }), 4193 4194 waitForHighlighterShown: this._safeAsyncAfterDestroy(async () => { 4195 return _waitForHighlighterEvent("highlighter-shown"); 4196 }), 4197 4198 waitForHighlighterHidden: this._safeAsyncAfterDestroy(async () => { 4199 return _waitForHighlighterEvent("highlighter-hidden"); 4200 }), 4201 }; 4202 } 4203 4204 /** 4205 * Shortcut to avoid throwing errors when an async method fails after toolbox 4206 * destroy. Should be used with methods that might be triggered by a user 4207 * input, regardless of the toolbox lifecycle. 4208 */ 4209 _safeAsyncAfterDestroy(fn) { 4210 return safeAsyncMethod(fn, () => !!this._destroyer); 4211 } 4212 4213 async _onNewSelectedNodeFront() { 4214 // Emit a "selection-changed" event when the toolbox.selection has been set 4215 // to a new node (or cleared). Currently used in the WebExtensions APIs (to 4216 // provide the `devtools.panels.elements.onSelectionChanged` event). 4217 this.emit("selection-changed"); 4218 4219 const targetFrontActorID = this.selection?.nodeFront?.targetFront?.actorID; 4220 if (targetFrontActorID) { 4221 this.selectTarget(targetFrontActorID); 4222 } 4223 } 4224 4225 _onToolSelected() { 4226 this._refreshHostTitle(); 4227 4228 this.updatePickerButton(); 4229 this.updateFrameButton(); 4230 this.updateErrorCountButton(); 4231 4232 // Calling setToolboxButtons in case the visibility of a button changed. 4233 this.component.setToolboxButtons(this.toolbarButtons); 4234 } 4235 4236 /** 4237 * Listener for "inspectObject" event on console top level target actor. 4238 */ 4239 _onInspectObject(packet) { 4240 this.inspectObjectActor(packet.objectActor, packet.inspectFromAnnotation); 4241 } 4242 4243 async inspectObjectActor(objectActor, inspectFromAnnotation) { 4244 const objectGrip = objectActor?.getGrip 4245 ? objectActor.getGrip() 4246 : objectActor; 4247 4248 if ( 4249 objectGrip.preview && 4250 objectGrip.preview.nodeType === domNodeConstants.ELEMENT_NODE 4251 ) { 4252 await this.viewElementInInspector(objectGrip, inspectFromAnnotation); 4253 return; 4254 } 4255 4256 if (objectGrip.class == "Function") { 4257 if (!objectGrip.location) { 4258 console.error("Missing location in Function objectGrip", objectGrip); 4259 return; 4260 } 4261 4262 const { url, line, column } = objectGrip.location; 4263 await this.viewSourceInDebugger(url, line, column); 4264 return; 4265 } 4266 4267 if (objectGrip.type !== "null" && objectGrip.type !== "undefined") { 4268 // Open then split console and inspect the object in the variables view, 4269 // when the objectActor doesn't represent an undefined or null value. 4270 if (this.currentToolId != "webconsole") { 4271 await this.openSplitConsole(); 4272 } 4273 4274 const panel = this.getPanel("webconsole"); 4275 panel.hud.ui.inspectObjectActor(objectActor); 4276 } 4277 } 4278 4279 /** 4280 * Get the toolbox's notification component 4281 * 4282 * @return The notification box component. 4283 */ 4284 getNotificationBox() { 4285 return this.notificationBox; 4286 } 4287 4288 async closeToolbox() { 4289 await this.destroy(); 4290 } 4291 4292 /** 4293 * Public API to check is the current toolbox is currently being destroyed. 4294 */ 4295 isDestroying() { 4296 return this._destroyer; 4297 } 4298 4299 /** 4300 * Remove all UI elements, detach from target and clear up 4301 */ 4302 destroy() { 4303 // If several things call destroy then we give them all the same 4304 // destruction promise so we're sure to destroy only once 4305 if (this._destroyer) { 4306 return this._destroyer; 4307 } 4308 4309 // This pattern allows to immediately return the destroyer promise. 4310 // See Bug 1602727 for more details. 4311 let destroyerResolve; 4312 this._destroyer = new Promise(r => (destroyerResolve = r)); 4313 this._destroyToolbox().then(destroyerResolve); 4314 4315 return this._destroyer; 4316 } 4317 4318 async _destroyToolbox() { 4319 this.emit("destroy"); 4320 4321 // This flag will be checked by Fronts in order to decide if they should 4322 // skip their destroy. 4323 this.commands.client.isToolboxDestroy = true; 4324 4325 this.off("select", this._onToolSelected); 4326 this.off("host-changed", this._refreshHostTitle); 4327 4328 gDevTools.off("tool-registered", this._toolRegistered); 4329 gDevTools.off("tool-unregistered", this._toolUnregistered); 4330 4331 for (const prefName in BOOLEAN_CONFIGURATION_PREFS) { 4332 Services.prefs.removeObserver( 4333 prefName, 4334 this._onBooleanConfigurationPrefChange 4335 ); 4336 } 4337 Services.prefs.removeObserver( 4338 BROWSERTOOLBOX_SCOPE_PREF, 4339 this._refreshHostTitle 4340 ); 4341 4342 // We normally handle toolClosed from selectTool() but in the event of the 4343 // toolbox closing we need to handle it here instead. 4344 this.telemetry.toolClosed(this.currentToolId, this); 4345 4346 this._lastFocusedElement = null; 4347 this._pausedTargets = null; 4348 4349 if (this._sourceMapLoader) { 4350 this._sourceMapLoader.destroy(); 4351 this._sourceMapLoader = null; 4352 } 4353 4354 if (this._parserWorker) { 4355 this._parserWorker.stop(); 4356 this._parserWorker = null; 4357 } 4358 4359 if (this.webconsolePanel) { 4360 this._saveSplitConsoleHeight(); 4361 this.webconsolePanel.removeEventListener( 4362 "resize", 4363 this._saveSplitConsoleHeight 4364 ); 4365 this.webconsolePanel = null; 4366 } 4367 if (this._tabBar) { 4368 this._tabBar.removeEventListener( 4369 "keypress", 4370 this._onToolbarArrowKeypress 4371 ); 4372 } 4373 if (this._componentMount) { 4374 this.ReactDOM.unmountComponentAtNode(this._componentMount); 4375 this.component = null; 4376 this._componentMount = null; 4377 this._tabBar = null; 4378 this._appBoundary = null; 4379 } 4380 this.destroyHarAutomation(); 4381 4382 for (const [id, panel] of this._toolPanels) { 4383 try { 4384 gDevTools.emit(id + "-destroy", this, panel); 4385 this.emit(id + "-destroy", panel); 4386 4387 const rv = panel.destroy(); 4388 if (rv) { 4389 console.error( 4390 `Panel ${id}'s destroy method returned something whereas it shouldn't (and should be synchronous).` 4391 ); 4392 } 4393 } catch (e) { 4394 // We don't want to stop here if any panel fail to close. 4395 console.error("Panel " + id + ":", e); 4396 } 4397 } 4398 4399 this.browserRequire = null; 4400 this._toolNames = null; 4401 4402 // Reset preferences set by the toolbox, then remove the preference front. 4403 const onResetPreference = this.resetPreference().then(() => { 4404 this._preferenceFrontRequest = null; 4405 }); 4406 4407 this.commands.targetCommand.unwatchTargets({ 4408 types: this.commands.targetCommand.ALL_TYPES, 4409 onAvailable: this._onTargetAvailable, 4410 onSelected: this._onTargetSelected, 4411 onDestroyed: this._onTargetDestroyed, 4412 }); 4413 4414 const watchedResources = [ 4415 this.commands.resourceCommand.TYPES.CONSOLE_MESSAGE, 4416 this.commands.resourceCommand.TYPES.ERROR_MESSAGE, 4417 this.commands.resourceCommand.TYPES.DOCUMENT_EVENT, 4418 this.commands.resourceCommand.TYPES.THREAD_STATE, 4419 ]; 4420 4421 if (!this.isBrowserToolbox) { 4422 watchedResources.push(this.commands.resourceCommand.TYPES.NETWORK_EVENT); 4423 } 4424 4425 if ( 4426 Services.prefs.getBoolPref( 4427 "devtools.debugger.features.javascript-tracing", 4428 false 4429 ) 4430 ) { 4431 watchedResources.push(this.commands.resourceCommand.TYPES.JSTRACER_STATE); 4432 this.commands.tracerCommand.off("toggle", this.onTracerToggled); 4433 } 4434 4435 this.commands.resourceCommand.unwatchResources(watchedResources, { 4436 onAvailable: this._onResourceAvailable, 4437 }); 4438 4439 // Unregister buttons listeners 4440 if (this.toolbarButtons) { 4441 this.toolbarButtons.forEach(button => { 4442 if (typeof button.teardown == "function") { 4443 // teardown arguments have already been bound in _createButtonState 4444 button.teardown(); 4445 } 4446 }); 4447 } 4448 4449 // We need to grab a reference to win before this._host is destroyed. 4450 const win = this.win; 4451 const host = this._getTelemetryHostString(); 4452 const width = Math.ceil(win.outerWidth / 50) * 50; 4453 const prevPanelName = this.getTelemetryPanelNameOrOther(this.currentToolId); 4454 4455 this.telemetry.toolClosed("toolbox", this); 4456 this.telemetry.recordEvent("exit", prevPanelName, null, { 4457 host, 4458 width, 4459 panel_name: this.getTelemetryPanelNameOrOther(this.currentToolId), 4460 next_panel: "none", 4461 reason: "toolbox_close", 4462 }); 4463 this.telemetry.recordEvent("close", "tools", null, { 4464 host, 4465 width, 4466 }); 4467 4468 // Wait for the preferences to be reset before destroying the target descriptor (which will destroy the preference front) 4469 const onceDestroyed = new Promise(resolve => { 4470 resolve( 4471 onResetPreference 4472 .catch(console.error) 4473 .then(async () => { 4474 // Destroy the node picker *after* destroying the panel, 4475 // which may still try to access it. (And might spawn a new one) 4476 if (this._nodePicker) { 4477 this._nodePicker.destroy(); 4478 this._nodePicker = null; 4479 } 4480 this.selection.destroy(); 4481 this.selection = null; 4482 4483 if (this._netMonitorAPI) { 4484 this._netMonitorAPI.destroy(); 4485 this._netMonitorAPI = null; 4486 } 4487 4488 if (this._sourceMapURLService) { 4489 await this._sourceMapURLService.waitForSourcesLoading(); 4490 this._sourceMapURLService.destroy(); 4491 this._sourceMapURLService = null; 4492 } 4493 4494 this._removeWindowListeners(); 4495 this._removeChromeEventHandlerEvents(); 4496 4497 if (this._store) { 4498 // Prevents any further action from being dispatched. 4499 // Do that late as NetMonitorAPI may still trigger some actions. 4500 this._store.dispatch(START_IGNORE_ACTION); 4501 this._store = null; 4502 } 4503 4504 // All Commands need to be destroyed. 4505 // This is done after other destruction tasks since it may tear down 4506 // fronts and the debugger transport which earlier destroy methods may 4507 // require to complete. 4508 // (i.e. avoid exceptions about closing connection with pending requests) 4509 // 4510 // For similar reasons, only destroy the TargetCommand after every 4511 // other outstanding cleanup is done. Destroying the target list 4512 // will lead to destroy frame targets which can temporarily make 4513 // some fronts unresponsive and block the cleanup. 4514 return this.commands.destroy(); 4515 }, console.error) 4516 .then(() => { 4517 this.emit("destroyed"); 4518 4519 // Free _host after the call to destroyed in order to let a chance 4520 // to destroyed listeners to still query toolbox attributes 4521 this._host = null; 4522 this._win = null; 4523 this._toolPanels.clear(); 4524 this._descriptorFront = null; 4525 this.commands = null; 4526 this._visibleIframes.clear(); 4527 4528 // Force GC to prevent long GC pauses when running tests and to free up 4529 // memory in general when the toolbox is closed. 4530 if (flags.testing) { 4531 win.windowUtils.garbageCollect(); 4532 } 4533 }) 4534 .catch(console.error) 4535 ); 4536 }); 4537 4538 const leakCheckObserver = ({ wrappedJSObject: barrier }) => { 4539 // Make the leak detector wait until this toolbox is properly destroyed. 4540 barrier.client.addBlocker( 4541 "DevTools: Wait until toolbox is destroyed", 4542 onceDestroyed 4543 ); 4544 }; 4545 4546 const topic = "shutdown-leaks-before-check"; 4547 Services.obs.addObserver(leakCheckObserver, topic); 4548 4549 await onceDestroyed; 4550 4551 Services.obs.removeObserver(leakCheckObserver, topic); 4552 } 4553 4554 /** 4555 * Open the textbox context menu at given coordinates. 4556 * Panels in the toolbox can call this on contextmenu events with event.screenX/Y 4557 * instead of having to implement their own copy/paste/selectAll menu. 4558 * 4559 * @param {number} x 4560 * @param {number} y 4561 */ 4562 openTextBoxContextMenu(x, y) { 4563 const menu = createEditContextMenu(this.topWindow, "toolbox-menu"); 4564 4565 // Fire event for tests 4566 menu.once("open", () => this.emit("menu-open")); 4567 menu.once("close", () => this.emit("menu-close")); 4568 4569 menu.popup(x, y, this.doc); 4570 } 4571 4572 /** 4573 * Retrieve the current textbox context menu, if available. 4574 */ 4575 getTextBoxContextMenu() { 4576 return this.topDoc.getElementById("toolbox-menu"); 4577 } 4578 4579 /** 4580 * Reset preferences set by the toolbox. 4581 */ 4582 async resetPreference() { 4583 if ( 4584 // No preferences have been changed, so there is nothing to reset. 4585 !this._preferenceFrontRequest || 4586 // Did any pertinent prefs actually change? For autohide and the pseudo-locale, 4587 // only reset prefs in the Browser Toolbox if it's been toggled in the UI 4588 // (don't reset the pref if it was already set before opening) 4589 (!this._autohideHasBeenToggled && !this._pseudoLocaleChanged) 4590 ) { 4591 return; 4592 } 4593 4594 const preferenceFront = await this.preferenceFront; 4595 4596 if (this._autohideHasBeenToggled) { 4597 await preferenceFront.clearUserPref(DISABLE_AUTOHIDE_PREF); 4598 } 4599 if (this._pseudoLocaleChanged) { 4600 await preferenceFront.clearUserPref(PSEUDO_LOCALE_PREF); 4601 } 4602 } 4603 4604 // HAR Automation 4605 4606 async initHarAutomation() { 4607 const autoExport = Services.prefs.getBoolPref( 4608 "devtools.netmonitor.har.enableAutoExportToFile" 4609 ); 4610 if (autoExport) { 4611 this.harAutomation = new HarAutomation(); 4612 await this.harAutomation.initialize(this); 4613 } 4614 } 4615 destroyHarAutomation() { 4616 if (this.harAutomation) { 4617 this.harAutomation.destroy(); 4618 } 4619 } 4620 4621 /** 4622 * Returns gViewSourceUtils for viewing source. 4623 */ 4624 get gViewSourceUtils() { 4625 return this.win.gViewSourceUtils; 4626 } 4627 4628 /** 4629 * Open a CSS file when there is no line or column information available. 4630 * 4631 * @param {string} url The URL of the CSS file to open. 4632 */ 4633 async viewGeneratedSourceInStyleEditor(url) { 4634 if (typeof url !== "string") { 4635 console.warn("Failed to open generated source, no url given"); 4636 return false; 4637 } 4638 4639 // The style editor hides the generated file if the file has original 4640 // sources, so we have no choice but to open whichever original file 4641 // corresponds to the first line of the generated file. 4642 return viewSource.viewSourceInStyleEditor(this, url, 1); 4643 } 4644 4645 /** 4646 * Given a URL for a stylesheet (generated or original), open in the style 4647 * editor if possible. Falls back to plain "view-source:". 4648 * If the stylesheet has a sourcemap, we will attempt to open the original 4649 * version of the file instead of the generated version. 4650 */ 4651 async viewSourceInStyleEditorByURL(url, line, column) { 4652 if (typeof url !== "string") { 4653 console.warn("Failed to open source, no url given"); 4654 return false; 4655 } 4656 if (typeof line !== "number") { 4657 console.warn( 4658 "No line given when navigating to source. If you're seeing this, there is a bug." 4659 ); 4660 4661 // This is a fallback in case of programming errors, but in a perfect 4662 // world, viewSourceInStyleEditorByURL would always get a line/colum. 4663 line = 1; 4664 column = null; 4665 } 4666 4667 return viewSource.viewSourceInStyleEditor(this, url, line, column); 4668 } 4669 4670 /** 4671 * Opens source in style editor. Falls back to plain "view-source:". 4672 * If the stylesheet has a sourcemap, we will attempt to open the original 4673 * version of the file instead of the generated version. 4674 */ 4675 async viewSourceInStyleEditorByResource(stylesheetResource, line, column) { 4676 if (!stylesheetResource || typeof stylesheetResource !== "object") { 4677 console.warn("Failed to open source, no stylesheet given"); 4678 return false; 4679 } 4680 if (typeof line !== "number") { 4681 console.warn( 4682 "No line given when navigating to source. If you're seeing this, there is a bug." 4683 ); 4684 4685 // This is a fallback in case of programming errors, but in a perfect 4686 // world, viewSourceInStyleEditorByResource would always get a line/colum. 4687 line = 1; 4688 column = null; 4689 } 4690 4691 return viewSource.viewSourceInStyleEditor( 4692 this, 4693 stylesheetResource, 4694 line, 4695 column 4696 ); 4697 } 4698 4699 async viewElementInInspector(objectGrip, reason) { 4700 // Open the inspector and select the DOM Element. 4701 await this.loadTool("inspector"); 4702 const inspector = this.getPanel("inspector"); 4703 const nodeFound = await inspector.inspectNodeActor(objectGrip, reason); 4704 if (nodeFound) { 4705 await this.selectTool("inspector", reason); 4706 } 4707 } 4708 4709 /** 4710 * Open a JS file when there is no line or column information available. 4711 * 4712 * @param {string} url The URL of the JS file to open. 4713 */ 4714 async viewGeneratedSourceInDebugger(url) { 4715 if (typeof url !== "string") { 4716 console.warn("Failed to open generated source, no url given"); 4717 return false; 4718 } 4719 4720 return viewSource.viewSourceInDebugger(this, url, null, null, null, null); 4721 } 4722 4723 /** 4724 * Opens source in debugger, the sourcemapped location will be selected in 4725 * the debugger panel, if the given location resolves to a know sourcemapped one. 4726 * 4727 * Falls back to plain "view-source:". 4728 * 4729 * @see devtools/client/shared/source-utils.js 4730 */ 4731 async viewSourceInDebugger( 4732 sourceURL, 4733 sourceLine, 4734 sourceColumn, 4735 sourceId, 4736 reason 4737 ) { 4738 if (typeof sourceURL !== "string" && typeof sourceId !== "string") { 4739 console.warn("Failed to open generated source, no url/id given"); 4740 return false; 4741 } 4742 if (typeof sourceLine !== "number") { 4743 console.warn( 4744 "No line given when navigating to source. If you're seeing this, there is a bug." 4745 ); 4746 4747 // This is a fallback in case of programming errors, but in a perfect 4748 // world, viewSourceInDebugger would always get a line/colum. 4749 sourceLine = 1; 4750 sourceColumn = null; 4751 } 4752 4753 return viewSource.viewSourceInDebugger( 4754 this, 4755 sourceURL, 4756 sourceLine, 4757 sourceColumn, 4758 sourceId, 4759 reason 4760 ); 4761 } 4762 4763 /** 4764 * Opens source in plain "view-source:". 4765 * 4766 * @see devtools/client/shared/source-utils.js 4767 */ 4768 viewSource(sourceURL, sourceLine, sourceColumn) { 4769 return viewSource.viewSource(this, sourceURL, sourceLine, sourceColumn); 4770 } 4771 4772 // Support for WebExtensions API (`devtools.network.*`) 4773 4774 /** 4775 * Return Netmonitor API object. This object offers Network monitor 4776 * public API that can be consumed by other panels or WE API. 4777 */ 4778 async getNetMonitorAPI() { 4779 const netPanel = this.getPanel("netmonitor"); 4780 4781 // Return Net panel if it exists. 4782 if (netPanel) { 4783 return netPanel.panelWin.Netmonitor.api; 4784 } 4785 4786 if (this._netMonitorAPI) { 4787 return this._netMonitorAPI; 4788 } 4789 4790 // Create and initialize Network monitor API object. 4791 // This object is only connected to the backend - not to the UI. 4792 this._netMonitorAPI = new NetMonitorAPI(); 4793 await this._netMonitorAPI.connect(this); 4794 4795 return this._netMonitorAPI; 4796 } 4797 4798 /** 4799 * Returns data (HAR) collected by the Network panel. 4800 */ 4801 async getHARFromNetMonitor() { 4802 const netMonitor = await this.getNetMonitorAPI(); 4803 let har = await netMonitor.getHar(); 4804 4805 // Return default empty HAR file if needed. 4806 har = har || buildHarLog(Services.appinfo); 4807 4808 // Return the log directly to be compatible with 4809 // Chrome WebExtension API. 4810 return har.log; 4811 } 4812 4813 /** 4814 * Add listener for `onRequestFinished` events. 4815 * 4816 * @param {object} listener 4817 * The listener to be called it's expected to be 4818 * a function that takes ({harEntry, requestId}) 4819 * as first argument. 4820 */ 4821 async addRequestFinishedListener(listener) { 4822 const netMonitor = await this.getNetMonitorAPI(); 4823 netMonitor.addRequestFinishedListener(listener); 4824 } 4825 4826 async removeRequestFinishedListener(listener) { 4827 const netMonitor = await this.getNetMonitorAPI(); 4828 netMonitor.removeRequestFinishedListener(listener); 4829 4830 // Destroy Network monitor API object if the following is true: 4831 // 1) there is no listener 4832 // 2) the Net panel doesn't exist/use the API object (if the panel 4833 // exists it's also responsible for destroying it, 4834 // see `NetMonitorPanel.open` for more details) 4835 const netPanel = this.getPanel("netmonitor"); 4836 const hasListeners = netMonitor.hasRequestFinishedListeners(); 4837 if (this._netMonitorAPI && !hasListeners && !netPanel) { 4838 this._netMonitorAPI.destroy(); 4839 this._netMonitorAPI = null; 4840 } 4841 } 4842 4843 /** 4844 * Used to lazily fetch HTTP response content within 4845 * `onRequestFinished` event listener. 4846 * 4847 * @param {string} requestId 4848 * Id of the request for which the response content 4849 * should be fetched. 4850 */ 4851 async fetchResponseContent(requestId) { 4852 const netMonitor = await this.getNetMonitorAPI(); 4853 return netMonitor.fetchResponseContent(requestId); 4854 } 4855 4856 // Support management of installed WebExtensions that provide a devtools_page. 4857 4858 /** 4859 * List the subset of the active WebExtensions which have a devtools_page (used by 4860 * toolbox-options.js to create the list of the tools provided by the enabled 4861 * WebExtensions). 4862 * 4863 * @see devtools/client/framework/toolbox-options.js 4864 */ 4865 listWebExtensions() { 4866 // Return the array of the enabled webextensions (we can't use the prefs list here, 4867 // because some of them may be disabled by the Addon Manager and still have a devtools 4868 // preference). 4869 return Array.from(this._webExtensions).map(([uuid, { name, pref }]) => { 4870 return { uuid, name, pref }; 4871 }); 4872 } 4873 4874 /** 4875 * Add a WebExtension to the list of the active extensions (given the extension UUID, 4876 * a unique id assigned to an extension when it is installed, and its name), 4877 * and emit a "webextension-registered" event to allow toolbox-options.js 4878 * to refresh the listed tools accordingly. 4879 * 4880 * @see browser/components/extensions/ext-devtools.js 4881 */ 4882 registerWebExtension(extensionUUID, { name, pref }) { 4883 // Ensure that an installed extension (active in the AddonManager) which 4884 // provides a devtools page is going to be listed in the toolbox options 4885 // (and refresh its name if it was already listed). 4886 this._webExtensions.set(extensionUUID, { name, pref }); 4887 this.emit("webextension-registered", extensionUUID); 4888 } 4889 4890 /** 4891 * Remove an active WebExtension from the list of the active extensions (given the 4892 * extension UUID, a unique id assigned to an extension when it is installed, and its 4893 * name), and emit a "webextension-unregistered" event to allow toolbox-options.js 4894 * to refresh the listed tools accordingly. 4895 * 4896 * @see browser/components/extensions/ext-devtools.js 4897 */ 4898 unregisterWebExtension(extensionUUID) { 4899 // Ensure that an extension that has been disabled/uninstalled from the AddonManager 4900 // is going to be removed from the toolbox options. 4901 this._webExtensions.delete(extensionUUID); 4902 this.emit("webextension-unregistered", extensionUUID); 4903 } 4904 4905 /** 4906 * A helper function which returns true if the extension with the given UUID is listed 4907 * as active for the toolbox and has its related devtools about:config preference set 4908 * to true. 4909 * 4910 * @see browser/components/extensions/ext-devtools.js 4911 */ 4912 isWebExtensionEnabled(extensionUUID) { 4913 const extInfo = this._webExtensions.get(extensionUUID); 4914 return extInfo && Services.prefs.getBoolPref(extInfo.pref, false); 4915 } 4916 4917 /** 4918 * Returns a panel id in the case of built in panels or "other" in the case of 4919 * third party panels. This is necessary due to limitations in addon id strings, 4920 * the permitted length of event telemetry property values and what we actually 4921 * want to see in our telemetry. 4922 * 4923 * @param {string} id 4924 * The panel id we would like to process. 4925 */ 4926 getTelemetryPanelNameOrOther(id) { 4927 if (!this._toolNames) { 4928 const definitions = gDevTools.getToolDefinitionArray(); 4929 const definitionIds = definitions.map(definition => definition.id); 4930 4931 this._toolNames = new Set(definitionIds); 4932 } 4933 4934 if (!this._toolNames.has(id)) { 4935 return "other"; 4936 } 4937 4938 return id; 4939 } 4940 4941 /** 4942 * Sets basic information on the DebugTargetInfo component 4943 */ 4944 _setDebugTargetData() { 4945 // Note that local WebExtension are debugged via WINDOW host, 4946 // but we still want to display target data. 4947 if ( 4948 this.hostType === Toolbox.HostType.PAGE || 4949 this._descriptorFront.isWebExtensionDescriptor 4950 ) { 4951 // Displays DebugTargetInfo which shows the basic information of debug target, 4952 // if `about:devtools-toolbox` URL opens directly. 4953 // DebugTargetInfo requires this._debugTargetData to be populated 4954 this.component.setDebugTargetData(this._getDebugTargetData()); 4955 } 4956 } 4957 4958 _onResourceAvailable(resources) { 4959 let errors = this._errorCount || 0; 4960 4961 const { TYPES } = this.commands.resourceCommand; 4962 for (const resource of resources) { 4963 const { resourceType } = resource; 4964 if ( 4965 resourceType === TYPES.ERROR_MESSAGE && 4966 // ERROR_MESSAGE resources can be warnings/info, but here we only want to count errors 4967 resource.pageError.error 4968 ) { 4969 errors++; 4970 continue; 4971 } 4972 4973 if (resourceType === TYPES.CONSOLE_MESSAGE) { 4974 const { level } = resource; 4975 if (level === "error" || level === "exception" || level === "assert") { 4976 errors++; 4977 } 4978 4979 // Reset the count on console.clear 4980 if (level === "clear") { 4981 errors = 0; 4982 } 4983 } 4984 4985 // Only consider top level document, and ignore remote iframes top document 4986 if ( 4987 resourceType === TYPES.DOCUMENT_EVENT && 4988 resource.name === "will-navigate" && 4989 resource.targetFront.isTopLevel 4990 ) { 4991 this._onWillNavigate({ 4992 isFrameSwitching: resource.isFrameSwitching, 4993 }); 4994 // While we will call `setErrorCount(0)` from onWillNavigate, we also need to reset 4995 // `errors` local variable in order to clear previous errors processed in the same 4996 // throttling bucket as this will-navigate resource. 4997 errors = 0; 4998 } 4999 5000 if ( 5001 resourceType === TYPES.DOCUMENT_EVENT && 5002 !resource.isFrameSwitching && 5003 // `url` is set on the targetFront when we receive dom-loading, and `title` when 5004 // `dom-interactive` is received. Here we're only updating the window title in 5005 // the "newer" event. 5006 resource.name === "dom-interactive" 5007 ) { 5008 // the targetFront title and url are updated on dom-interactive, so delay refreshing 5009 // the host title a bit in order for the event listener in targetCommand to be 5010 // executed. 5011 setTimeout(() => { 5012 if (resource.targetFront.isDestroyed()) { 5013 // The resource's target might have been destroyed in between and 5014 // would no longer have a valid actorID available. 5015 return; 5016 } 5017 5018 this._updateFrames({ 5019 frameData: { 5020 id: resource.targetFront.actorID, 5021 url: resource.targetFront.url, 5022 title: resource.targetFront.title, 5023 }, 5024 }); 5025 5026 if (resource.targetFront.isTopLevel) { 5027 this._refreshHostTitle(); 5028 this._setDebugTargetData(); 5029 } 5030 }, 0); 5031 } 5032 5033 if (resourceType == TYPES.THREAD_STATE) { 5034 this._onThreadStateChanged(resource); 5035 } 5036 if (resourceType == TYPES.JSTRACER_STATE) { 5037 this._onTracingStateChanged(resource); 5038 } 5039 } 5040 5041 this.setErrorCount(errors); 5042 } 5043 5044 _onResourceUpdated(resources) { 5045 let errors = this._errorCount || 0; 5046 5047 for (const { update } of resources) { 5048 // In order to match webconsole behaviour, we treat 4xx and 5xx network calls as errors. 5049 if ( 5050 update.resourceType === 5051 this.commands.resourceCommand.TYPES.NETWORK_EVENT && 5052 update.resourceUpdates.status && 5053 update.resourceUpdates.status.toString().match(REGEX_4XX_5XX) 5054 ) { 5055 errors++; 5056 } 5057 } 5058 5059 this.setErrorCount(errors); 5060 } 5061 5062 /** 5063 * Set the number of errors in the toolbar icon. 5064 * 5065 * @param {number} count 5066 */ 5067 setErrorCount(count) { 5068 // Don't re-render if the number of errors changed 5069 if (!this.component || this._errorCount === count) { 5070 return; 5071 } 5072 5073 this._errorCount = count; 5074 5075 // Update button properties and trigger a render of the toolbox 5076 this.updateErrorCountButton(); 5077 this._throttledSetToolboxButtons(); 5078 } 5079 } 5080 5081 exports.Toolbox = Toolbox;