forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			4735 lines
		
	
	
	
		
			147 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			4735 lines
		
	
	
	
		
			147 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| const SOURCE_MAP_WORKER =
 | |
|   "resource://devtools/client/shared/source-map/worker.js";
 | |
| const SOURCE_MAP_WORKER_ASSETS =
 | |
|   "resource://devtools/client/shared/source-map/assets/";
 | |
| 
 | |
| const MAX_ORDINAL = 99;
 | |
| const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
 | |
| const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
 | |
| const DISABLE_AUTOHIDE_PREF = "ui.popup.disable_autohide";
 | |
| const PSEUDO_LOCALE_PREF = "intl.l10n.pseudo";
 | |
| const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST";
 | |
| const CURRENT_THEME_SCALAR = "devtools.current_theme";
 | |
| const HTML_NS = "http://www.w3.org/1999/xhtml";
 | |
| const REGEX_4XX_5XX = /^[4,5]\d\d$/;
 | |
| 
 | |
| const BROWSERTOOLBOX_SCOPE_PREF = "devtools.browsertoolbox.scope";
 | |
| const BROWSERTOOLBOX_SCOPE_EVERYTHING = "everything";
 | |
| const BROWSERTOOLBOX_SCOPE_PARENTPROCESS = "parent-process";
 | |
| 
 | |
| var { Ci, Cc } = require("chrome");
 | |
| const { debounce } = require("devtools/shared/debounce");
 | |
| const { throttle } = require("devtools/shared/throttle");
 | |
| const { safeAsyncMethod } = require("devtools/shared/async-utils");
 | |
| var { gDevTools } = require("devtools/client/framework/devtools");
 | |
| var EventEmitter = require("devtools/shared/event-emitter");
 | |
| const Selection = require("devtools/client/framework/selection");
 | |
| var Telemetry = require("devtools/client/shared/telemetry");
 | |
| const { getUnicodeUrl } = require("devtools/client/shared/unicode-url");
 | |
| var { DOMHelpers } = require("devtools/shared/dom-helpers");
 | |
| const { KeyCodes } = require("devtools/client/shared/keycodes");
 | |
| const {
 | |
|   FluentL10n,
 | |
| } = require("devtools/client/shared/fluent-l10n/fluent-l10n");
 | |
| 
 | |
| var Startup = Cc["@mozilla.org/devtools/startup-clh;1"].getService(
 | |
|   Ci.nsISupports
 | |
| ).wrappedJSObject;
 | |
| 
 | |
| const { createCommandsDictionary } = require("devtools/shared/commands/index");
 | |
| 
 | |
| const { BrowserLoader } = ChromeUtils.import(
 | |
|   "resource://devtools/shared/loader/browser-loader.js"
 | |
| );
 | |
| 
 | |
| const { MultiLocalizationHelper } = require("devtools/shared/l10n");
 | |
| const L10N = new MultiLocalizationHelper(
 | |
|   "devtools/client/locales/toolbox.properties",
 | |
|   "chrome://branding/locale/brand.properties"
 | |
| );
 | |
| 
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "registerStoreObserver",
 | |
|   "devtools/client/shared/redux/subscriber",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "createToolboxStore",
 | |
|   "devtools/client/framework/store",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   ["registerWalkerListeners"],
 | |
|   "devtools/client/framework/actions/index",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   ["selectTarget"],
 | |
|   "devtools/shared/commands/target/actions/targets",
 | |
|   true
 | |
| );
 | |
| 
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "AppConstants",
 | |
|   "resource://gre/modules/AppConstants.jsm",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "KeyShortcuts",
 | |
|   "devtools/client/shared/key-shortcuts"
 | |
| );
 | |
| loader.lazyRequireGetter(this, "ZoomKeys", "devtools/client/shared/zoom-keys");
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "ToolboxButtons",
 | |
|   "devtools/client/definitions",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "SourceMapURLService",
 | |
|   "devtools/client/framework/source-map-url-service",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "BrowserConsoleManager",
 | |
|   "devtools/client/webconsole/browser-console-manager",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "viewSource",
 | |
|   "devtools/client/shared/view-source"
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "buildHarLog",
 | |
|   "devtools/client/netmonitor/src/har/har-builder-utils",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "NetMonitorAPI",
 | |
|   "devtools/client/netmonitor/src/api",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "sortPanelDefinitions",
 | |
|   "devtools/client/framework/toolbox-tabs-order-manager",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "createEditContextMenu",
 | |
|   "devtools/client/framework/toolbox-context-menu",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "getSelectedTarget",
 | |
|   "devtools/shared/commands/target/selectors/targets",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "remoteClientManager",
 | |
|   "devtools/client/shared/remote-debugging/remote-client-manager.js",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "ResponsiveUIManager",
 | |
|   "devtools/client/responsive/manager"
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "DevToolsUtils",
 | |
|   "devtools/shared/DevToolsUtils"
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "NodePicker",
 | |
|   "devtools/client/inspector/node-picker"
 | |
| );
 | |
| 
 | |
| loader.lazyGetter(this, "domNodeConstants", () => {
 | |
|   return require("devtools/shared/dom-node-constants");
 | |
| });
 | |
| 
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "NodeFront",
 | |
|   "devtools/client/fronts/node",
 | |
|   true
 | |
| );
 | |
| 
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "PICKER_TYPES",
 | |
|   "devtools/shared/picker-constants"
 | |
| );
 | |
| 
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "HarAutomation",
 | |
|   "devtools/client/netmonitor/src/har/har-automation",
 | |
|   true
 | |
| );
 | |
| 
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "getThreadOptions",
 | |
|   "devtools/client/shared/thread-utils",
 | |
|   true
 | |
| );
 | |
| 
 | |
| const DEVTOOLS_F12_DISABLED_PREF = "devtools.experiment.f12.shortcut_disabled";
 | |
| 
 | |
| /**
 | |
|  * A "Toolbox" is the component that holds all the tools for one specific
 | |
|  * target. Visually, it's a document that includes the tools tabs and all
 | |
|  * the iframes where the tool panels will be living in.
 | |
|  *
 | |
|  * @param {object} descriptorFront
 | |
|  *        The context to inspect identified by this descriptor.
 | |
|  * @param {string} selectedTool
 | |
|  *        Tool to select initially
 | |
|  * @param {Toolbox.HostType} hostType
 | |
|  *        Type of host that will host the toolbox (e.g. sidebar, window)
 | |
|  * @param {DOMWindow} contentWindow
 | |
|  *        The window object of the toolbox document
 | |
|  * @param {string} frameId
 | |
|  *        A unique identifier to differentiate toolbox documents from the
 | |
|  *        chrome codebase when passing DOM messages
 | |
|  * @param {Number} msSinceProcessStart
 | |
|  *        the number of milliseconds since process start using monotonic
 | |
|  *        timestamps (unaffected by system clock changes).
 | |
|  */
 | |
| function Toolbox(
 | |
|   descriptorFront,
 | |
|   selectedTool,
 | |
|   hostType,
 | |
|   contentWindow,
 | |
|   frameId,
 | |
|   msSinceProcessStart
 | |
| ) {
 | |
|   this._win = contentWindow;
 | |
|   this.frameId = frameId;
 | |
|   this.selection = new Selection();
 | |
|   this.telemetry = new Telemetry();
 | |
| 
 | |
|   this.descriptorFront = descriptorFront;
 | |
| 
 | |
|   // The session ID is used to determine which telemetry events belong to which
 | |
|   // toolbox session. Because we use Amplitude to analyse the telemetry data we
 | |
|   // must use the time since the system wide epoch as the session ID.
 | |
|   this.sessionId = msSinceProcessStart;
 | |
| 
 | |
|   // If the user opened the toolbox, we can now enable the F12 shortcut.
 | |
|   if (Services.prefs.getBoolPref(DEVTOOLS_F12_DISABLED_PREF, false)) {
 | |
|     // If the toolbox is opening while F12 was disabled, the user might have
 | |
|     // pressed F12 and seen the "enable devtools" notification.
 | |
|     // Flip the preference.
 | |
|     Services.prefs.setBoolPref(DEVTOOLS_F12_DISABLED_PREF, false);
 | |
|   }
 | |
| 
 | |
|   // Map of the available DevTools WebExtensions:
 | |
|   //   Map<extensionUUID, extensionName>
 | |
|   this._webExtensions = new Map();
 | |
| 
 | |
|   this._toolPanels = new Map();
 | |
|   this._inspectorExtensionSidebars = new Map();
 | |
| 
 | |
|   this._netMonitorAPI = null;
 | |
| 
 | |
|   // Map of frames (id => frame-info) and currently selected frame id.
 | |
|   this.frameMap = new Map();
 | |
|   this.selectedFrameId = null;
 | |
| 
 | |
|   // Number of targets currently paused
 | |
|   this._pausedTargets = 0;
 | |
| 
 | |
|   /**
 | |
|    * KeyShortcuts instance specific to WINDOW host type.
 | |
|    * This is the key shortcuts that are only register when the toolbox
 | |
|    * is loaded in its own window. Otherwise, these shortcuts are typically
 | |
|    * registered by devtools-startup.js module.
 | |
|    */
 | |
|   this._windowHostShortcuts = null;
 | |
| 
 | |
|   this._toolRegistered = this._toolRegistered.bind(this);
 | |
|   this._toolUnregistered = this._toolUnregistered.bind(this);
 | |
|   this._refreshHostTitle = this._refreshHostTitle.bind(this);
 | |
|   this.toggleNoAutohide = this.toggleNoAutohide.bind(this);
 | |
|   this.disablePseudoLocale = () => this.changePseudoLocale("none");
 | |
|   this.enableAccentedPseudoLocale = () => this.changePseudoLocale("accented");
 | |
|   this.enableBidiPseudoLocale = () => this.changePseudoLocale("bidi");
 | |
|   this._updateFrames = this._updateFrames.bind(this);
 | |
|   this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
 | |
|   this.closeToolbox = this.closeToolbox.bind(this);
 | |
|   this.destroy = this.destroy.bind(this);
 | |
|   this._applyCacheSettings = this._applyCacheSettings.bind(this);
 | |
|   this._applyCustomFormatterSetting = this._applyCustomFormatterSetting.bind(
 | |
|     this
 | |
|   );
 | |
|   this._applyServiceWorkersTestingSettings = this._applyServiceWorkersTestingSettings.bind(
 | |
|     this
 | |
|   );
 | |
|   this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this);
 | |
|   this._onFocus = this._onFocus.bind(this);
 | |
|   this._onBrowserMessage = this._onBrowserMessage.bind(this);
 | |
|   this._onTabsOrderUpdated = this._onTabsOrderUpdated.bind(this);
 | |
|   this._onToolbarFocus = this._onToolbarFocus.bind(this);
 | |
|   this._onToolbarArrowKeypress = this._onToolbarArrowKeypress.bind(this);
 | |
|   this._onPickerClick = this._onPickerClick.bind(this);
 | |
|   this._onPickerKeypress = this._onPickerKeypress.bind(this);
 | |
|   this._onPickerStarting = this._onPickerStarting.bind(this);
 | |
|   this._onPickerStarted = this._onPickerStarted.bind(this);
 | |
|   this._onPickerStopped = this._onPickerStopped.bind(this);
 | |
|   this._onPickerCanceled = this._onPickerCanceled.bind(this);
 | |
|   this._onPickerPicked = this._onPickerPicked.bind(this);
 | |
|   this._onPickerPreviewed = this._onPickerPreviewed.bind(this);
 | |
|   this._onInspectObject = this._onInspectObject.bind(this);
 | |
|   this._onNewSelectedNodeFront = this._onNewSelectedNodeFront.bind(this);
 | |
|   this._onToolSelected = this._onToolSelected.bind(this);
 | |
|   this._onContextMenu = this._onContextMenu.bind(this);
 | |
|   this._onMouseDown = this._onMouseDown.bind(this);
 | |
|   this.updateToolboxButtonsVisibility = this.updateToolboxButtonsVisibility.bind(
 | |
|     this
 | |
|   );
 | |
|   this.updateToolboxButtons = this.updateToolboxButtons.bind(this);
 | |
|   this.selectTool = this.selectTool.bind(this);
 | |
|   this._pingTelemetrySelectTool = this._pingTelemetrySelectTool.bind(this);
 | |
|   this.toggleSplitConsole = this.toggleSplitConsole.bind(this);
 | |
|   this.toggleOptions = this.toggleOptions.bind(this);
 | |
|   this._onTargetAvailable = this._onTargetAvailable.bind(this);
 | |
|   this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
 | |
|   this._onTargetSelected = this._onTargetSelected.bind(this);
 | |
|   this._onResourceAvailable = this._onResourceAvailable.bind(this);
 | |
|   this._onResourceUpdated = this._onResourceUpdated.bind(this);
 | |
|   this._onToolSelectedStopPicker = this._onToolSelectedStopPicker.bind(this);
 | |
| 
 | |
|   // `component` might be null if the toolbox was destroying during the throttling
 | |
|   this._throttledSetToolboxButtons = throttle(
 | |
|     () => this.component?.setToolboxButtons(this.toolbarButtons),
 | |
|     500,
 | |
|     this
 | |
|   );
 | |
| 
 | |
|   if (!selectedTool) {
 | |
|     selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
 | |
|   }
 | |
|   this._defaultToolId = selectedTool;
 | |
| 
 | |
|   this._hostType = hostType;
 | |
| 
 | |
|   this.isOpen = new Promise(
 | |
|     function(resolve) {
 | |
|       this._resolveIsOpen = resolve;
 | |
|     }.bind(this)
 | |
|   );
 | |
| 
 | |
|   EventEmitter.decorate(this);
 | |
| 
 | |
|   this.on("host-changed", this._refreshHostTitle);
 | |
|   this.on("select", this._onToolSelected);
 | |
| 
 | |
|   this.selection.on("new-node-front", this._onNewSelectedNodeFront);
 | |
| 
 | |
|   gDevTools.on("tool-registered", this._toolRegistered);
 | |
|   gDevTools.on("tool-unregistered", this._toolUnregistered);
 | |
| 
 | |
|   /**
 | |
|    * Get text direction for the current locale direction.
 | |
|    *
 | |
|    * `getComputedStyle` forces a synchronous reflow, so use a lazy getter in order to
 | |
|    * call it only once.
 | |
|    */
 | |
|   loader.lazyGetter(this, "direction", () => {
 | |
|     const { documentElement } = this.doc;
 | |
|     const isRtl =
 | |
|       this.win.getComputedStyle(documentElement).direction === "rtl";
 | |
|     return isRtl ? "rtl" : "ltr";
 | |
|   });
 | |
| }
 | |
| exports.Toolbox = Toolbox;
 | |
| 
 | |
| /**
 | |
|  * The toolbox can be 'hosted' either embedded in a browser window
 | |
|  * or in a separate window.
 | |
|  */
 | |
| Toolbox.HostType = {
 | |
|   BOTTOM: "bottom",
 | |
|   RIGHT: "right",
 | |
|   LEFT: "left",
 | |
|   WINDOW: "window",
 | |
|   BROWSERTOOLBOX: "browsertoolbox",
 | |
|   // This is typically used by `about:debugging`, when opening toolbox in a new tab,
 | |
|   // via `about:devtools-toolbox` URLs.
 | |
|   PAGE: "page",
 | |
| };
 | |
| 
 | |
| Toolbox.prototype = {
 | |
|   _URL: "about:devtools-toolbox",
 | |
| 
 | |
|   _prefs: {
 | |
|     LAST_TOOL: "devtools.toolbox.selectedTool",
 | |
|   },
 | |
| 
 | |
|   get nodePicker() {
 | |
|     if (!this._nodePicker) {
 | |
|       this._nodePicker = new NodePicker(this.commands, this.selection);
 | |
|       this._nodePicker.on("picker-starting", this._onPickerStarting);
 | |
|       this._nodePicker.on("picker-started", this._onPickerStarted);
 | |
|       this._nodePicker.on("picker-stopped", this._onPickerStopped);
 | |
|       this._nodePicker.on("picker-node-canceled", this._onPickerCanceled);
 | |
|       this._nodePicker.on("picker-node-picked", this._onPickerPicked);
 | |
|       this._nodePicker.on("picker-node-previewed", this._onPickerPreviewed);
 | |
|     }
 | |
| 
 | |
|     return this._nodePicker;
 | |
|   },
 | |
| 
 | |
|   get store() {
 | |
|     if (!this._store) {
 | |
|       this._store = createToolboxStore();
 | |
|     }
 | |
|     return this._store;
 | |
|   },
 | |
| 
 | |
|   get currentToolId() {
 | |
|     return this._currentToolId;
 | |
|   },
 | |
| 
 | |
|   set currentToolId(id) {
 | |
|     this._currentToolId = id;
 | |
|     this.component.setCurrentToolId(id);
 | |
|   },
 | |
| 
 | |
|   get defaultToolId() {
 | |
|     return this._defaultToolId;
 | |
|   },
 | |
| 
 | |
|   get panelDefinitions() {
 | |
|     return this._panelDefinitions;
 | |
|   },
 | |
| 
 | |
|   set panelDefinitions(definitions) {
 | |
|     this._panelDefinitions = definitions;
 | |
|     this._combineAndSortPanelDefinitions();
 | |
|   },
 | |
| 
 | |
|   get visibleAdditionalTools() {
 | |
|     if (!this._visibleAdditionalTools) {
 | |
|       this._visibleAdditionalTools = [];
 | |
|     }
 | |
| 
 | |
|     return this._visibleAdditionalTools;
 | |
|   },
 | |
| 
 | |
|   set visibleAdditionalTools(tools) {
 | |
|     this._visibleAdditionalTools = tools;
 | |
|     if (this.isReady) {
 | |
|       this._combineAndSortPanelDefinitions();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Combines the built-in panel definitions and the additional tool definitions that
 | |
|    * can be set by add-ons.
 | |
|    */
 | |
|   _combineAndSortPanelDefinitions() {
 | |
|     let definitions = [
 | |
|       ...this._panelDefinitions,
 | |
|       ...this.getVisibleAdditionalTools(),
 | |
|     ];
 | |
|     definitions = sortPanelDefinitions(definitions);
 | |
|     this.component.setPanelDefinitions(definitions);
 | |
|   },
 | |
| 
 | |
|   lastUsedToolId: null,
 | |
| 
 | |
|   /**
 | |
|    * Returns a *copy* of the _toolPanels collection.
 | |
|    *
 | |
|    * @return {Map} panels
 | |
|    *         All the running panels in the toolbox
 | |
|    */
 | |
|   getToolPanels() {
 | |
|     return new Map(this._toolPanels);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Access the panel for a given tool
 | |
|    */
 | |
|   getPanel(id) {
 | |
|     return this._toolPanels.get(id);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the panel instance for a given tool once it is ready.
 | |
|    * If the tool is already opened, the promise will resolve immediately,
 | |
|    * otherwise it will wait until the tool has been opened before resolving.
 | |
|    *
 | |
|    * Note that this does not open the tool, use selectTool if you'd
 | |
|    * like to select the tool right away.
 | |
|    *
 | |
|    * @param  {String} id
 | |
|    *         The id of the panel, for example "jsdebugger".
 | |
|    * @returns Promise
 | |
|    *          A promise that resolves once the panel is ready.
 | |
|    */
 | |
|   getPanelWhenReady(id) {
 | |
|     const panel = this.getPanel(id);
 | |
|     return new Promise(resolve => {
 | |
|       if (panel) {
 | |
|         resolve(panel);
 | |
|       } else {
 | |
|         this.on(id + "-ready", initializedPanel => {
 | |
|           resolve(initializedPanel);
 | |
|         });
 | |
|       }
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * This is a shortcut for getPanel(currentToolId) because it is much more
 | |
|    * likely that we're going to want to get the panel that we've just made
 | |
|    * visible
 | |
|    */
 | |
|   getCurrentPanel() {
 | |
|     return this._toolPanels.get(this.currentToolId);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the current top level target the toolbox is debugging.
 | |
|    *
 | |
|    * This will only be defined *after* calling Toolbox.open(),
 | |
|    * after it has called `targetCommands.startListening`.
 | |
|    */
 | |
|   get target() {
 | |
|     return this.commands.targetCommand.targetFront;
 | |
|   },
 | |
| 
 | |
|   get threadFront() {
 | |
|     return this.commands.targetCommand.targetFront.threadFront;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate
 | |
|    * tab. See HostType for more details.
 | |
|    */
 | |
|   get hostType() {
 | |
|     return this._hostType;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Shortcut to the window containing the toolbox UI
 | |
|    */
 | |
|   get win() {
 | |
|     return this._win;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * When the toolbox is loaded in a frame with type="content", win.parent will not return
 | |
|    * the parent Chrome window. This getter should return the parent Chrome window
 | |
|    * regardless of the frame type. See Bug 1539979.
 | |
|    */
 | |
|   get topWindow() {
 | |
|     return DevToolsUtils.getTopWindow(this.win);
 | |
|   },
 | |
| 
 | |
|   get topDoc() {
 | |
|     return this.topWindow.document;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Shortcut to the document containing the toolbox UI
 | |
|    */
 | |
|   get doc() {
 | |
|     return this.win.document;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the toggled state of the split console
 | |
|    */
 | |
|   get splitConsole() {
 | |
|     return this._splitConsole;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the focused state of the split console
 | |
|    */
 | |
|   isSplitConsoleFocused() {
 | |
|     if (!this._splitConsole) {
 | |
|       return false;
 | |
|     }
 | |
|     const focusedWin = Services.focus.focusedWindow;
 | |
|     return (
 | |
|       focusedWin &&
 | |
|       focusedWin ===
 | |
|         this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   get isBrowserToolbox() {
 | |
|     return this.hostType === Toolbox.HostType.BROWSERTOOLBOX;
 | |
|   },
 | |
| 
 | |
|   get isMultiProcessBrowserToolbox() {
 | |
|     return (
 | |
|       this.isBrowserToolbox &&
 | |
|       Services.prefs.getBoolPref("devtools.browsertoolbox.fission", false)
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Set a given target as selected (which may impact the console evaluation context selector).
 | |
|    *
 | |
|    * @param {String} targetActorID: The actorID of the target we want to select.
 | |
|    */
 | |
|   selectTarget(targetActorID) {
 | |
|     if (this.getSelectedTargetFront()?.actorID !== targetActorID) {
 | |
|       // The selected target is managed by the TargetCommand's store.
 | |
|       // So dispatch this action against that other store.
 | |
|       this.commands.targetCommand.store.dispatch(selectTarget(targetActorID));
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * @returns {ThreadFront|null} The selected thread front, or null if there is none.
 | |
|    */
 | |
|   getSelectedTargetFront() {
 | |
|     // The selected target is managed by the TargetCommand's store.
 | |
|     // So pull the state from that other store.
 | |
|     const selectedTarget = getSelectedTarget(
 | |
|       this.commands.targetCommand.store.getState()
 | |
|     );
 | |
|     if (!selectedTarget) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     return this.commands.client.getFrontByID(selectedTarget.actorID);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * For now, the debugger isn't hooked to TargetCommand's store
 | |
|    * to display its thread list. So manually forward target selection change
 | |
|    * to the debugger via a dedicated action
 | |
|    */
 | |
|   _onTargetCommandStateChange(state, oldState) {
 | |
|     if (getSelectedTarget(state) !== getSelectedTarget(oldState)) {
 | |
|       const dbg = this.getPanel("jsdebugger");
 | |
|       if (!dbg) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       const threadActorID = getSelectedTarget(state)?.threadFront?.actorID;
 | |
|       if (!threadActorID) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       dbg.selectThread(threadActorID);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called on each new THREAD_STATE resource
 | |
|    *
 | |
|    * @param {Object} resource The THREAD_STATE resource
 | |
|    */
 | |
|   _onThreadStateChanged(resource) {
 | |
|     if (resource.state == "paused") {
 | |
|       this._pauseToolbox(resource.why.type);
 | |
|     } else if (resource.state == "resumed") {
 | |
|       this._resumeToolbox();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _pauseToolbox(reason) {
 | |
|     // Suppress interrupted events by default because the thread is
 | |
|     // paused/resumed a lot for various actions.
 | |
|     if (reason === "interrupted") {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.highlightTool("jsdebugger");
 | |
| 
 | |
|     if (
 | |
|       reason === "debuggerStatement" ||
 | |
|       reason === "mutationBreakpoint" ||
 | |
|       reason === "eventBreakpoint" ||
 | |
|       reason === "breakpoint" ||
 | |
|       reason === "exception" ||
 | |
|       reason === "resumeLimit" ||
 | |
|       reason === "XHR"
 | |
|     ) {
 | |
|       this.raise();
 | |
|       this.selectTool("jsdebugger", reason);
 | |
|       // Each Target/Thread can be paused only once at a time,
 | |
|       // so, for each pause, we should have a related resumed event.
 | |
|       // But we may have multiple targets paused at the same time
 | |
|       this._pausedTargets++;
 | |
|       this.emit("toolbox-paused");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _resumeToolbox() {
 | |
|     if (this.isHighlighted("jsdebugger")) {
 | |
|       this._pausedTargets--;
 | |
|       if (this._pausedTargets == 0) {
 | |
|         this.emit("toolbox-resumed");
 | |
|         this.unhighlightTool("jsdebugger");
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * This method will be called for the top-level target, as well as any potential
 | |
|    * additional targets we may care about.
 | |
|    */
 | |
|   async _onTargetAvailable({ targetFront, isTargetSwitching }) {
 | |
|     if (targetFront.isTopLevel) {
 | |
|       // Attach to a new top-level target.
 | |
|       // For now, register these event listeners only on the top level target
 | |
|       if (!targetFront.targetForm.ignoreSubFrames) {
 | |
|         targetFront.on("frame-update", this._updateFrames);
 | |
|       }
 | |
|       const consoleFront = await targetFront.getFront("console");
 | |
|       consoleFront.on("inspectObject", this._onInspectObject);
 | |
|     }
 | |
| 
 | |
|     // Walker listeners allow to monitor DOM Mutation breakpoint updates.
 | |
|     // All targets should be monitored.
 | |
|     targetFront.watchFronts("inspector", async inspectorFront => {
 | |
|       registerWalkerListeners(this.store, inspectorFront.walker);
 | |
|     });
 | |
| 
 | |
|     if (targetFront.isTopLevel && isTargetSwitching) {
 | |
|       // These methods expect the target to be attached, which is guaranteed by the time
 | |
|       // _onTargetAvailable is called by the targetCommand.
 | |
|       await this._listFrames();
 | |
|       // The target may have been destroyed while calling _listFrames if we navigate quickly
 | |
|       if (targetFront.isDestroyed()) {
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (targetFront.targetForm.ignoreSubFrames) {
 | |
|       this._updateFrames({
 | |
|         frames: [
 | |
|           {
 | |
|             id: targetFront.actorID,
 | |
|             targetFront,
 | |
|             url: targetFront.url,
 | |
|             title: targetFront.title,
 | |
|             isTopLevel: targetFront.isTopLevel,
 | |
|           },
 | |
|         ],
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // If a new popup is debugged, automagically switch the toolbox to become
 | |
|     // an independant window so that we can easily keep debugging the new tab.
 | |
|     // Only do that if that's not the current top level, otherwise it means
 | |
|     // we opened a toolbox dedicated to the popup.
 | |
|     if (
 | |
|       targetFront.targetForm.isPopup &&
 | |
|       !targetFront.isTopLevel &&
 | |
|       this.descriptorFront.isLocalTab
 | |
|     ) {
 | |
|       await this.switchHostToTab(targetFront.targetForm.browsingContextID);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   async _onTargetSelected({ targetFront }) {
 | |
|     this._updateFrames({ selected: targetFront.actorID });
 | |
|     this.selectTarget(targetFront.actorID);
 | |
|   },
 | |
| 
 | |
|   _onTargetDestroyed({ targetFront }) {
 | |
|     if (targetFront.isTopLevel) {
 | |
|       const consoleFront = targetFront.getCachedFront("console");
 | |
|       // If the target has already been destroyed, its console front will
 | |
|       // also already be destroyed and so we won't be able to retrieve it.
 | |
|       // Nor is it important to clear its listener as fronts automatically clears
 | |
|       // all their listeners on destroy.
 | |
|       if (consoleFront) {
 | |
|         consoleFront.off("inspectObject", this._onInspectObject);
 | |
|       }
 | |
|       targetFront.off("frame-update", this._updateFrames);
 | |
|     } else if (this.selection) {
 | |
|       this.selection.onTargetDestroyed(targetFront);
 | |
|     }
 | |
| 
 | |
|     // When navigating the old (top level) target can get destroyed before the thread state changed
 | |
|     // event for the target is received, so it gets lost. This currently happens with bf-cache
 | |
|     // navigations when paused, so lets make sure we resumed if not.
 | |
|     //
 | |
|     // We should also resume if a paused non-top-level target is destroyed
 | |
|     if (targetFront.isTopLevel || targetFront.threadFront?.paused) {
 | |
|       this._resumeToolbox();
 | |
|     }
 | |
| 
 | |
|     if (targetFront.targetForm.ignoreSubFrames) {
 | |
|       this._updateFrames({
 | |
|         frames: [
 | |
|           {
 | |
|             id: targetFront.actorID,
 | |
|             destroy: true,
 | |
|           },
 | |
|         ],
 | |
|       });
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onTargetThreadFrontResumeWrongOrder() {
 | |
|     const box = this.getNotificationBox();
 | |
|     box.appendNotification(
 | |
|       L10N.getStr("toolbox.resumeOrderWarning"),
 | |
|       "wrong-resume-order",
 | |
|       "",
 | |
|       box.PRIORITY_WARNING_HIGH
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Open the toolbox
 | |
|    */
 | |
|   open() {
 | |
|     return async function() {
 | |
|       // Kick off async loading the Fluent bundles.
 | |
|       const fluentL10n = new FluentL10n();
 | |
|       const fluentInitPromise = fluentL10n.init([
 | |
|         "devtools/client/toolbox.ftl",
 | |
|       ]);
 | |
| 
 | |
|       const isToolboxURL = this.win.location.href.startsWith(this._URL);
 | |
|       if (isToolboxURL) {
 | |
|         // Update the URL so that onceDOMReady watch for the right url.
 | |
|         this._URL = this.win.location.href;
 | |
|       }
 | |
| 
 | |
|       const domReady = new Promise(resolve => {
 | |
|         DOMHelpers.onceDOMReady(
 | |
|           this.win,
 | |
|           () => {
 | |
|             resolve();
 | |
|           },
 | |
|           this._URL
 | |
|         );
 | |
|       });
 | |
| 
 | |
|       // This attribute is meant to be a public attribute on the Toolbox object
 | |
|       // It exposes commands modules listed in devtools/shared/commands/index.js
 | |
|       // which are an abstraction on top of RDP methods.
 | |
|       // See devtools/shared/commands/README.md
 | |
|       // Bug 1700909 will make the commands be instantiated by gDevTools instead of the Toolbox.
 | |
|       this.commands = await createCommandsDictionary(this.descriptorFront);
 | |
| 
 | |
|       this.commands.targetCommand.on(
 | |
|         "target-thread-wrong-order-on-resume",
 | |
|         this._onTargetThreadFrontResumeWrongOrder.bind(this)
 | |
|       );
 | |
|       registerStoreObserver(
 | |
|         this.commands.targetCommand.store,
 | |
|         this._onTargetCommandStateChange.bind(this)
 | |
|       );
 | |
| 
 | |
|       // Bug 1709063: Use commands.resourceCommand instead of toolbox.resourceCommand
 | |
|       this.resourceCommand = this.commands.resourceCommand;
 | |
| 
 | |
|       // Optimization: fire up a few other things before waiting on
 | |
|       // the iframe being ready (makes startup faster)
 | |
|       await this.commands.targetCommand.startListening();
 | |
| 
 | |
|       // Lets get the current thread settings from the prefs and
 | |
|       // update the threadConfigurationActor which should manage
 | |
|       // updating the current threads.
 | |
|       const options = await getThreadOptions();
 | |
|       await this.commands.threadConfigurationCommand.updateConfiguration(
 | |
|         options
 | |
|       );
 | |
| 
 | |
|       // This needs to be done before watching for resources so console messages can be
 | |
|       // custom formatted right away.
 | |
|       await this._applyCustomFormatterSetting();
 | |
| 
 | |
|       // The targetCommand is created right before this code.
 | |
|       // It means that this call to watchTargets is the first,
 | |
|       // and we are registering the first target listener, which means
 | |
|       // Toolbox._onTargetAvailable will be called first, before any other
 | |
|       // onTargetAvailable listener that might be registered on targetCommand.
 | |
|       await this.commands.targetCommand.watchTargets({
 | |
|         types: this.commands.targetCommand.ALL_TYPES,
 | |
|         onAvailable: this._onTargetAvailable,
 | |
|         onSelected: this._onTargetSelected,
 | |
|         onDestroyed: this._onTargetDestroyed,
 | |
|       });
 | |
| 
 | |
|       const watchedResources = [
 | |
|         // Watch for console API messages, errors and network events in order to populate
 | |
|         // the error count icon in the toolbox.
 | |
|         this.resourceCommand.TYPES.CONSOLE_MESSAGE,
 | |
|         this.resourceCommand.TYPES.ERROR_MESSAGE,
 | |
|         this.resourceCommand.TYPES.DOCUMENT_EVENT,
 | |
|         this.resourceCommand.TYPES.THREAD_STATE,
 | |
|       ];
 | |
| 
 | |
|       if (!this.isBrowserToolbox) {
 | |
|         // Independently of watching network event resources for the error count icon,
 | |
|         // we need to start tracking network activity on toolbox open for targets such
 | |
|         // as tabs, in order to ensure there is always at least one listener existing
 | |
|         // for network events across the lifetime of the various panels, so stopping
 | |
|         // the resource command from clearing out its cache of network event resources.
 | |
|         watchedResources.push(this.resourceCommand.TYPES.NETWORK_EVENT);
 | |
|       }
 | |
| 
 | |
|       const onResourcesWatched = this.resourceCommand.watchResources(
 | |
|         watchedResources,
 | |
|         {
 | |
|           onAvailable: this._onResourceAvailable,
 | |
|           onUpdated: this._onResourceUpdated,
 | |
|         }
 | |
|       );
 | |
| 
 | |
|       await domReady;
 | |
| 
 | |
|       this.browserRequire = BrowserLoader({
 | |
|         window: this.win,
 | |
|         useOnlyShared: true,
 | |
|       }).require;
 | |
| 
 | |
|       this.isReady = true;
 | |
| 
 | |
|       const framesPromise = this._listFrames();
 | |
| 
 | |
|       Services.prefs.addObserver(
 | |
|         "devtools.cache.disabled",
 | |
|         this._applyCacheSettings
 | |
|       );
 | |
|       Services.prefs.addObserver(
 | |
|         "devtools.custom-formatters.enabled",
 | |
|         this._applyCustomFormatterSetting
 | |
|       );
 | |
|       Services.prefs.addObserver(
 | |
|         "devtools.serviceWorkers.testing.enabled",
 | |
|         this._applyServiceWorkersTestingSettings
 | |
|       );
 | |
|       Services.prefs.addObserver(
 | |
|         BROWSERTOOLBOX_SCOPE_PREF,
 | |
|         this._refreshHostTitle
 | |
|       );
 | |
| 
 | |
|       // Get the DOM element to mount the ToolboxController to.
 | |
|       this._componentMount = this.doc.getElementById("toolbox-toolbar-mount");
 | |
| 
 | |
|       await fluentInitPromise;
 | |
| 
 | |
|       // Mount the ToolboxController component and update all its state
 | |
|       // that can be updated synchronousl
 | |
|       this._mountReactComponent(fluentL10n.getBundles());
 | |
|       this._buildDockOptions();
 | |
|       this._buildInitialPanelDefinitions();
 | |
|       this._setDebugTargetData();
 | |
| 
 | |
|       // Forward configuration flags to the DevTools server.
 | |
|       this._applyCacheSettings();
 | |
|       this._applyServiceWorkersTestingSettings();
 | |
| 
 | |
|       this._addWindowListeners();
 | |
|       this._addChromeEventHandlerEvents();
 | |
| 
 | |
|       // Get the tab bar of the ToolboxController to attach the "keypress" event listener to.
 | |
|       this._tabBar = this.doc.querySelector(".devtools-tabbar");
 | |
|       this._tabBar.addEventListener("keypress", this._onToolbarArrowKeypress);
 | |
| 
 | |
|       this._componentMount.setAttribute(
 | |
|         "aria-label",
 | |
|         L10N.getStr("toolbox.label")
 | |
|       );
 | |
| 
 | |
|       this.webconsolePanel = this.doc.querySelector(
 | |
|         "#toolbox-panel-webconsole"
 | |
|       );
 | |
|       this.webconsolePanel.height = Services.prefs.getIntPref(
 | |
|         SPLITCONSOLE_HEIGHT_PREF
 | |
|       );
 | |
|       this.webconsolePanel.addEventListener(
 | |
|         "resize",
 | |
|         this._saveSplitConsoleHeight
 | |
|       );
 | |
| 
 | |
|       this._buildButtons();
 | |
| 
 | |
|       this._pingTelemetry();
 | |
| 
 | |
|       // The isToolSupported check needs to happen after the target is
 | |
|       // remoted, otherwise we could have done it in the toolbox constructor
 | |
|       // (bug 1072764).
 | |
|       const toolDef = gDevTools.getToolDefinition(this._defaultToolId);
 | |
|       if (!toolDef || !toolDef.isToolSupported(this)) {
 | |
|         this._defaultToolId = "webconsole";
 | |
|       }
 | |
| 
 | |
|       // Update all ToolboxController state that can only be done asynchronously
 | |
|       await this._setInitialMeatballState();
 | |
| 
 | |
|       // Start rendering the toolbox toolbar before selecting the tool, as the tools
 | |
|       // can take a few hundred milliseconds seconds to start up.
 | |
|       //
 | |
|       // Delay React rendering as Toolbox.open is synchronous.
 | |
|       // Even if this involve promises, it is synchronous. Toolbox.open already loads
 | |
|       // react modules and freeze the event loop for a significant time.
 | |
|       // requestIdleCallback allows releasing it to allow user events to be processed.
 | |
|       // Use 16ms maximum delay to allow one frame to be rendered at 60FPS
 | |
|       // (1000ms/60FPS=16ms)
 | |
|       this.win.requestIdleCallback(
 | |
|         () => {
 | |
|           this.component.setCanRender();
 | |
|         },
 | |
|         { timeout: 16 }
 | |
|       );
 | |
| 
 | |
|       await this.selectTool(this._defaultToolId, "initial_panel");
 | |
| 
 | |
|       // Wait until the original tool is selected so that the split
 | |
|       // console input will receive focus.
 | |
|       let splitConsolePromise = Promise.resolve();
 | |
|       if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
 | |
|         splitConsolePromise = this.openSplitConsole();
 | |
|         this.telemetry.addEventProperty(
 | |
|           this.topWindow,
 | |
|           "open",
 | |
|           "tools",
 | |
|           null,
 | |
|           "splitconsole",
 | |
|           true
 | |
|         );
 | |
|       } else {
 | |
|         this.telemetry.addEventProperty(
 | |
|           this.topWindow,
 | |
|           "open",
 | |
|           "tools",
 | |
|           null,
 | |
|           "splitconsole",
 | |
|           false
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       await Promise.all([
 | |
|         splitConsolePromise,
 | |
|         framesPromise,
 | |
|         onResourcesWatched,
 | |
|       ]);
 | |
| 
 | |
|       // We do not expect the focus to be restored when using about:debugging toolboxes
 | |
|       // Otherwise, when reloading the toolbox, the debugged tab will be focused.
 | |
|       if (this.hostType !== Toolbox.HostType.PAGE) {
 | |
|         // Request the actor to restore the focus to the content page once the
 | |
|         // target is detached. This typically happens when the console closes.
 | |
|         // We restore the focus as it may have been stolen by the console input.
 | |
|         await this.commands.targetConfigurationCommand.updateConfiguration({
 | |
|           restoreFocus: true,
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       await this.initHarAutomation();
 | |
| 
 | |
|       this.emit("ready");
 | |
|       this._resolveIsOpen();
 | |
|     }
 | |
|       .bind(this)()
 | |
|       .catch(e => {
 | |
|         console.error("Exception while opening the toolbox", String(e), e);
 | |
|         // While the exception stack is correctly printed in the Browser console when
 | |
|         // passing `e` to console.error, it is not on the stdout, so print it via dump.
 | |
|         dump(e.stack + "\n");
 | |
|       });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Retrieve the ChromeEventHandler associated to the toolbox frame.
 | |
|    * When DevTools are loaded in a content frame, this will return the containing chrome
 | |
|    * frame. Events from nested frames will bubble up to this chrome frame, which allows to
 | |
|    * listen to events from nested frames.
 | |
|    */
 | |
|   getChromeEventHandler() {
 | |
|     if (!this.win || !this.win.docShell) {
 | |
|       return null;
 | |
|     }
 | |
|     return this.win.docShell.chromeEventHandler;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Attach events on the chromeEventHandler for the current window. When loaded in a
 | |
|    * frame with type set to "content", events will not bubble across frames. The
 | |
|    * chromeEventHandler does not have this limitation and will catch all events triggered
 | |
|    * on any of the frames under the devtools document.
 | |
|    *
 | |
|    * Events relying on the chromeEventHandler need to be added and removed at specific
 | |
|    * moments in the lifecycle of the toolbox, so all the events relying on it should be
 | |
|    * grouped here.
 | |
|    */
 | |
|   _addChromeEventHandlerEvents() {
 | |
|     // win.docShell.chromeEventHandler might not be accessible anymore when removing the
 | |
|     // events, so we can't rely on a dynamic getter here.
 | |
|     // Keep a reference on the chromeEventHandler used to addEventListener to be sure we
 | |
|     // can remove the listeners afterwards.
 | |
|     this._chromeEventHandler = this.getChromeEventHandler();
 | |
|     if (!this._chromeEventHandler) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Add shortcuts and window-host-shortcuts that use the ChromeEventHandler as target.
 | |
|     this._addShortcuts();
 | |
|     this._addWindowHostShortcuts();
 | |
| 
 | |
|     this._chromeEventHandler.addEventListener(
 | |
|       "keypress",
 | |
|       this._splitConsoleOnKeypress
 | |
|     );
 | |
|     this._chromeEventHandler.addEventListener("focus", this._onFocus, true);
 | |
|     this._chromeEventHandler.addEventListener(
 | |
|       "contextmenu",
 | |
|       this._onContextMenu
 | |
|     );
 | |
|     this._chromeEventHandler.addEventListener("mousedown", this._onMouseDown);
 | |
|   },
 | |
| 
 | |
|   _removeChromeEventHandlerEvents() {
 | |
|     if (!this._chromeEventHandler) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Remove shortcuts and window-host-shortcuts that use the ChromeEventHandler as
 | |
|     // target.
 | |
|     this._removeShortcuts();
 | |
|     this._removeWindowHostShortcuts();
 | |
| 
 | |
|     this._chromeEventHandler.removeEventListener(
 | |
|       "keypress",
 | |
|       this._splitConsoleOnKeypress
 | |
|     );
 | |
|     this._chromeEventHandler.removeEventListener("focus", this._onFocus, true);
 | |
|     this._chromeEventHandler.removeEventListener(
 | |
|       "contextmenu",
 | |
|       this._onContextMenu
 | |
|     );
 | |
|     this._chromeEventHandler.removeEventListener(
 | |
|       "mousedown",
 | |
|       this._onMouseDown
 | |
|     );
 | |
| 
 | |
|     this._chromeEventHandler = null;
 | |
|   },
 | |
| 
 | |
|   _addShortcuts() {
 | |
|     // Create shortcuts instance for the toolbox
 | |
|     if (!this.shortcuts) {
 | |
|       this.shortcuts = new KeyShortcuts({
 | |
|         window: this.doc.defaultView,
 | |
|         // The toolbox key shortcuts should be triggered from any frame in DevTools.
 | |
|         // Use the chromeEventHandler as the target to catch events from all frames.
 | |
|         target: this.getChromeEventHandler(),
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // Listen for the shortcut key to show the frame list
 | |
|     this.shortcuts.on(L10N.getStr("toolbox.showFrames.key"), event => {
 | |
|       if (event.target.id === "command-button-frames") {
 | |
|         event.target.click();
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     // Listen for tool navigation shortcuts.
 | |
|     this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"), event => {
 | |
|       this.selectNextTool();
 | |
|       event.preventDefault();
 | |
|     });
 | |
|     this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"), event => {
 | |
|       this.selectPreviousTool();
 | |
|       event.preventDefault();
 | |
|     });
 | |
|     this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"), event => {
 | |
|       this.switchToPreviousHost();
 | |
|       event.preventDefault();
 | |
|     });
 | |
| 
 | |
|     // List for Help/Settings key.
 | |
|     this.shortcuts.on(L10N.getStr("toolbox.help.key"), this.toggleOptions);
 | |
| 
 | |
|     if (!this.isBrowserToolbox) {
 | |
|       // Listen for Reload shortcuts
 | |
|       [
 | |
|         ["reload", false],
 | |
|         ["reload2", false],
 | |
|         ["forceReload", true],
 | |
|         ["forceReload2", true],
 | |
|       ].forEach(([id, force]) => {
 | |
|         const key = L10N.getStr("toolbox." + id + ".key");
 | |
|         this.shortcuts.on(key, event => {
 | |
|           this.commands.targetCommand.reloadTopLevelTarget(force);
 | |
| 
 | |
|           // Prevent Firefox shortcuts from reloading the page
 | |
|           event.preventDefault();
 | |
|         });
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // Add zoom-related shortcuts.
 | |
|     if (!this._hostOptions || this._hostOptions.zoom === true) {
 | |
|       ZoomKeys.register(this.win, this.shortcuts);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _removeShortcuts() {
 | |
|     if (this.shortcuts) {
 | |
|       this.shortcuts.destroy();
 | |
|       this.shortcuts = null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Adds the keys and commands to the Toolbox Window in window mode.
 | |
|    */
 | |
|   _addWindowHostShortcuts() {
 | |
|     if (this.hostType != Toolbox.HostType.WINDOW) {
 | |
|       // Those shortcuts are only valid for host type WINDOW.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (!this._windowHostShortcuts) {
 | |
|       this._windowHostShortcuts = new KeyShortcuts({
 | |
|         window: this.win,
 | |
|         // The window host key shortcuts should be triggered from any frame in DevTools.
 | |
|         // Use the chromeEventHandler as the target to catch events from all frames.
 | |
|         target: this.getChromeEventHandler(),
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     const shortcuts = this._windowHostShortcuts;
 | |
| 
 | |
|     for (const item of Startup.KeyShortcuts) {
 | |
|       const { id, toolId, shortcut, modifiers } = item;
 | |
|       const electronKey = KeyShortcuts.parseXulKey(modifiers, shortcut);
 | |
| 
 | |
|       if (id == "browserConsole") {
 | |
|         // Add key for toggling the browser console from the detached window
 | |
|         shortcuts.on(electronKey, () => {
 | |
|           BrowserConsoleManager.toggleBrowserConsole();
 | |
|         });
 | |
|       } else if (toolId) {
 | |
|         // KeyShortcuts contain tool-specific and global key shortcuts,
 | |
|         // here we only need to copy shortcut specific to each tool.
 | |
|         shortcuts.on(electronKey, () => {
 | |
|           this.selectTool(toolId, "key_shortcut").then(() =>
 | |
|             this.fireCustomKey(toolId)
 | |
|           );
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // CmdOrCtrl+W is registered only when the toolbox is running in
 | |
|     // detached window. In the other case the entire browser tab
 | |
|     // is closed when the user uses this shortcut.
 | |
|     shortcuts.on(L10N.getStr("toolbox.closeToolbox.key"), this.closeToolbox);
 | |
| 
 | |
|     // The others are only registered in window host type as for other hosts,
 | |
|     // these keys are already registered by devtools-startup.js
 | |
|     shortcuts.on(
 | |
|       L10N.getStr("toolbox.toggleToolboxF12.key"),
 | |
|       this.closeToolbox
 | |
|     );
 | |
|     if (AppConstants.platform == "macosx") {
 | |
|       shortcuts.on(
 | |
|         L10N.getStr("toolbox.toggleToolboxOSX.key"),
 | |
|         this.closeToolbox
 | |
|       );
 | |
|     } else {
 | |
|       shortcuts.on(L10N.getStr("toolbox.toggleToolbox.key"), this.closeToolbox);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _removeWindowHostShortcuts() {
 | |
|     if (this._windowHostShortcuts) {
 | |
|       this._windowHostShortcuts.destroy();
 | |
|       this._windowHostShortcuts = null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onContextMenu(e) {
 | |
|     // Handle context menu events in standard input elements: <input> and <textarea>.
 | |
|     // Also support for custom input elements using .devtools-input class
 | |
|     // (e.g. CodeMirror instances).
 | |
|     const isInInput =
 | |
|       e.originalTarget.closest("input[type=text]") ||
 | |
|       e.originalTarget.closest("input[type=search]") ||
 | |
|       e.originalTarget.closest("input:not([type])") ||
 | |
|       e.originalTarget.closest(".devtools-input") ||
 | |
|       e.originalTarget.closest("textarea");
 | |
| 
 | |
|     const doc = e.originalTarget.ownerDocument;
 | |
|     const isHTMLPanel = doc.documentElement.namespaceURI === HTML_NS;
 | |
| 
 | |
|     if (
 | |
|       // Context-menu events on input elements will use a custom context menu.
 | |
|       isInInput ||
 | |
|       // Context-menu events from HTML panels should not trigger the default
 | |
|       // browser context menu for HTML documents.
 | |
|       isHTMLPanel
 | |
|     ) {
 | |
|       e.stopPropagation();
 | |
|       e.preventDefault();
 | |
|     }
 | |
| 
 | |
|     if (isInInput) {
 | |
|       this.openTextBoxContextMenu(e.screenX, e.screenY);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onMouseDown(e) {
 | |
|     const isMiddleClick = e.button === 1;
 | |
|     if (isMiddleClick) {
 | |
|       // Middle clicks will trigger the scroll lock feature to turn on.
 | |
|       // When the DevTools toolbox was running in an <iframe>, this behavior was
 | |
|       // disabled by default. When running in a <browser> element, we now need
 | |
|       // to catch and preventDefault() on those events.
 | |
|       e.preventDefault();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _getDebugTargetData() {
 | |
|     const url = new URL(this.win.location);
 | |
|     const remoteId = url.searchParams.get("remoteId");
 | |
|     const runtimeInfo = remoteClientManager.getRuntimeInfoByRemoteId(remoteId);
 | |
|     const connectionType = remoteClientManager.getConnectionTypeByRemoteId(
 | |
|       remoteId
 | |
|     );
 | |
| 
 | |
|     return {
 | |
|       connectionType,
 | |
|       runtimeInfo,
 | |
|       descriptorType: this.descriptorFront.descriptorType,
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   isDebugTargetFenix() {
 | |
|     return this._getDebugTargetData()?.runtimeInfo?.isFenix;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * loading React modules when needed (to avoid performance penalties
 | |
|    * during Firefox start up time).
 | |
|    */
 | |
|   get React() {
 | |
|     return this.browserRequire("devtools/client/shared/vendor/react");
 | |
|   },
 | |
| 
 | |
|   get ReactDOM() {
 | |
|     return this.browserRequire("devtools/client/shared/vendor/react-dom");
 | |
|   },
 | |
| 
 | |
|   get ReactRedux() {
 | |
|     return this.browserRequire("devtools/client/shared/vendor/react-redux");
 | |
|   },
 | |
| 
 | |
|   get ToolboxController() {
 | |
|     return this.browserRequire(
 | |
|       "devtools/client/framework/components/ToolboxController"
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Unconditionally create and get the source map service.
 | |
|    */
 | |
|   _createSourceMapService() {
 | |
|     if (this._sourceMapService) {
 | |
|       return this._sourceMapService;
 | |
|     }
 | |
|     // Uses browser loader to access the `Worker` global.
 | |
|     const service = this.browserRequire(
 | |
|       "devtools/client/shared/source-map/index"
 | |
|     );
 | |
| 
 | |
|     // Provide a wrapper for the service that reports errors more nicely.
 | |
|     this._sourceMapService = new Proxy(service, {
 | |
|       get: (target, name) => {
 | |
|         switch (name) {
 | |
|           case "getOriginalURLs":
 | |
|             return urlInfo => {
 | |
|               return target.getOriginalURLs(urlInfo).catch(text => {
 | |
|                 const message = L10N.getFormatStr(
 | |
|                   "toolbox.sourceMapFailure",
 | |
|                   text,
 | |
|                   urlInfo.url,
 | |
|                   urlInfo.sourceMapURL
 | |
|                 );
 | |
|                 this.target.logWarningInPage(message, "source map");
 | |
|                 // It's ok to swallow errors here, because a null
 | |
|                 // result just means that no source map was found.
 | |
|                 return null;
 | |
|               });
 | |
|             };
 | |
| 
 | |
|           case "getOriginalSourceText":
 | |
|             return originalSourceId => {
 | |
|               return target
 | |
|                 .getOriginalSourceText(originalSourceId)
 | |
|                 .catch(error => {
 | |
|                   const message = L10N.getFormatStr(
 | |
|                     "toolbox.sourceMapSourceFailure",
 | |
|                     error.message,
 | |
|                     error.metadata ? error.metadata.url : "<unknown>"
 | |
|                   );
 | |
|                   this.target.logWarningInPage(message, "source map");
 | |
|                   // Also replace the result with the error text.
 | |
|                   // Note that this result has to have the same form
 | |
|                   // as whatever the upstream getOriginalSourceText
 | |
|                   // returns.
 | |
|                   return {
 | |
|                     text: message,
 | |
|                     contentType: "text/plain",
 | |
|                   };
 | |
|                 });
 | |
|             };
 | |
| 
 | |
|           case "applySourceMap":
 | |
|             return (generatedId, url, code, mappings) => {
 | |
|               return target
 | |
|                 .applySourceMap(generatedId, url, code, mappings)
 | |
|                 .then(async result => {
 | |
|                   // If a tool has changed or introduced a source map
 | |
|                   // (e.g, by pretty-printing a source), tell the
 | |
|                   // source map URL service about the change, so that
 | |
|                   // subscribers to that service can be updated as
 | |
|                   // well.
 | |
|                   if (this._sourceMapURLService) {
 | |
|                     await this._sourceMapURLService.newSourceMapCreated(
 | |
|                       generatedId
 | |
|                     );
 | |
|                   }
 | |
|                   return result;
 | |
|                 });
 | |
|             };
 | |
| 
 | |
|           default:
 | |
|             return target[name];
 | |
|         }
 | |
|       },
 | |
|     });
 | |
| 
 | |
|     this._sourceMapService.startSourceMapWorker(
 | |
|       SOURCE_MAP_WORKER,
 | |
|       SOURCE_MAP_WORKER_ASSETS
 | |
|     );
 | |
|     return this._sourceMapService;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * A common access point for the client-side mapping service for source maps that
 | |
|    * any panel can use.  This is a "low-level" API that connects to
 | |
|    * the source map worker.
 | |
|    */
 | |
|   get sourceMapService() {
 | |
|     return this._createSourceMapService();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * A common access point for the client-side parser service that any panel can use.
 | |
|    */
 | |
|   get parserService() {
 | |
|     if (this._parserService) {
 | |
|       return this._parserService;
 | |
|     }
 | |
| 
 | |
|     const {
 | |
|       ParserDispatcher,
 | |
|     } = require("devtools/client/debugger/src/workers/parser/index");
 | |
| 
 | |
|     this._parserService = new ParserDispatcher();
 | |
|     this._parserService.start(
 | |
|       "resource://devtools/client/debugger/dist/parser-worker.js",
 | |
|       this.win
 | |
|     );
 | |
|     return this._parserService;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Clients wishing to use source maps but that want the toolbox to
 | |
|    * track the source and style sheet actor mapping can use this
 | |
|    * source map service.  This is a higher-level service than the one
 | |
|    * returned by |sourceMapService|, in that it automatically tracks
 | |
|    * source and style sheet actor IDs.
 | |
|    */
 | |
|   get sourceMapURLService() {
 | |
|     if (this._sourceMapURLService) {
 | |
|       return this._sourceMapURLService;
 | |
|     }
 | |
|     const sourceMaps = this._createSourceMapService();
 | |
|     this._sourceMapURLService = new SourceMapURLService(
 | |
|       this.commands,
 | |
|       sourceMaps
 | |
|     );
 | |
|     return this._sourceMapURLService;
 | |
|   },
 | |
| 
 | |
|   // Return HostType id for telemetry
 | |
|   _getTelemetryHostId() {
 | |
|     switch (this.hostType) {
 | |
|       case Toolbox.HostType.BOTTOM:
 | |
|         return 0;
 | |
|       case Toolbox.HostType.RIGHT:
 | |
|         return 1;
 | |
|       case Toolbox.HostType.WINDOW:
 | |
|         return 2;
 | |
|       case Toolbox.HostType.BROWSERTOOLBOX:
 | |
|         return 3;
 | |
|       case Toolbox.HostType.LEFT:
 | |
|         return 4;
 | |
|       case Toolbox.HostType.PAGE:
 | |
|         return 5;
 | |
|       default:
 | |
|         return 9;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // Return HostType string for telemetry
 | |
|   _getTelemetryHostString() {
 | |
|     switch (this.hostType) {
 | |
|       case Toolbox.HostType.BOTTOM:
 | |
|         return "bottom";
 | |
|       case Toolbox.HostType.LEFT:
 | |
|         return "left";
 | |
|       case Toolbox.HostType.RIGHT:
 | |
|         return "right";
 | |
|       case Toolbox.HostType.WINDOW:
 | |
|         return "window";
 | |
|       case Toolbox.HostType.PAGE:
 | |
|         return "page";
 | |
|       case Toolbox.HostType.BROWSERTOOLBOX:
 | |
|         return "other";
 | |
|       default:
 | |
|         return "bottom";
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _pingTelemetry() {
 | |
|     Services.prefs.setBoolPref("devtools.everOpened", true);
 | |
|     this.telemetry.toolOpened("toolbox", this.sessionId, this);
 | |
| 
 | |
|     this.telemetry
 | |
|       .getHistogramById(HOST_HISTOGRAM)
 | |
|       .add(this._getTelemetryHostId());
 | |
| 
 | |
|     // Log current theme. The question we want to answer is:
 | |
|     // "What proportion of users use which themes?"
 | |
|     const currentTheme = Services.prefs.getCharPref("devtools.theme");
 | |
|     this.telemetry.keyedScalarAdd(CURRENT_THEME_SCALAR, currentTheme, 1);
 | |
| 
 | |
|     const browserWin = this.topWindow;
 | |
|     this.telemetry.preparePendingEvent(browserWin, "open", "tools", null, [
 | |
|       "entrypoint",
 | |
|       "first_panel",
 | |
|       "host",
 | |
|       "shortcut",
 | |
|       "splitconsole",
 | |
|       "width",
 | |
|       "session_id",
 | |
|     ]);
 | |
|     this.telemetry.addEventProperty(
 | |
|       browserWin,
 | |
|       "open",
 | |
|       "tools",
 | |
|       null,
 | |
|       "host",
 | |
|       this._getTelemetryHostString()
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Create a simple object to store the state of a toolbox button. The checked state of
 | |
|    * a button can be updated arbitrarily outside of the scope of the toolbar and its
 | |
|    * controllers. In order to simplify this interaction this object emits an
 | |
|    * "updatechecked" event any time the isChecked value is updated, allowing any consuming
 | |
|    * components to listen and respond to updates.
 | |
|    *
 | |
|    * @param {Object} options:
 | |
|    *
 | |
|    * @property {String} id - The id of the button or command.
 | |
|    * @property {String} className - An optional additional className for the button.
 | |
|    * @property {String} description - The value that will display as a tooltip and in
 | |
|    *                    the options panel for enabling/disabling.
 | |
|    * @property {Boolean} disabled - An optional disabled state for the button.
 | |
|    * @property {Function} onClick - The function to run when the button is activated by
 | |
|    *                      click or keyboard shortcut. First argument will be the 'click'
 | |
|    *                      event, and second argument is the toolbox instance.
 | |
|    * @property {Boolean} isInStartContainer - Buttons can either be placed at the start
 | |
|    *                     of the toolbar, or at the end.
 | |
|    * @property {Function} setup - Function run immediately to listen for events changing
 | |
|    *                      whenever the button is checked or unchecked. The toolbox object
 | |
|    *                      is passed as first argument and a callback is passed as second
 | |
|    *                       argument, to be called whenever the checked state changes.
 | |
|    * @property {Function} teardown - Function run on toolbox close to let a chance to
 | |
|    *                      unregister listeners set when `setup` was called and avoid
 | |
|    *                      memory leaks. The same arguments than `setup` function are
 | |
|    *                      passed to `teardown`.
 | |
|    * @property {Function} isToolSupported - Function to automatically enable/disable
 | |
|    *                      the button based on the toolbox. If the toolbox don't support
 | |
|    *                      the button feature, this method should return false.
 | |
|    * @property {Function} isCurrentlyVisible - Function to automatically
 | |
|    *                      hide/show the button based on current state.
 | |
|    * @property {Function} isChecked - Optional function called to known if the button
 | |
|    *                      is toggled or not. The function should return true when
 | |
|    *                      the button should be displayed as toggled on.
 | |
|    */
 | |
|   _createButtonState(options) {
 | |
|     let isCheckedValue = false;
 | |
|     const {
 | |
|       id,
 | |
|       className,
 | |
|       description,
 | |
|       disabled,
 | |
|       onClick,
 | |
|       isInStartContainer,
 | |
|       setup,
 | |
|       teardown,
 | |
|       isToolSupported,
 | |
|       isCurrentlyVisible,
 | |
|       isChecked,
 | |
|       onKeyDown,
 | |
|       experimentalURL,
 | |
|     } = options;
 | |
|     const toolbox = this;
 | |
|     const button = {
 | |
|       id,
 | |
|       className,
 | |
|       description,
 | |
|       disabled,
 | |
|       async onClick(event) {
 | |
|         if (typeof onClick == "function") {
 | |
|           await onClick(event, toolbox);
 | |
|           button.emit("updatechecked");
 | |
|         }
 | |
|       },
 | |
|       onKeyDown(event) {
 | |
|         if (typeof onKeyDown == "function") {
 | |
|           onKeyDown(event, toolbox);
 | |
|         }
 | |
|       },
 | |
|       isToolSupported,
 | |
|       isCurrentlyVisible,
 | |
|       get isChecked() {
 | |
|         if (typeof isChecked == "function") {
 | |
|           return isChecked(toolbox);
 | |
|         }
 | |
|         return isCheckedValue;
 | |
|       },
 | |
|       set isChecked(value) {
 | |
|         // Note that if options.isChecked is given, this is ignored
 | |
|         isCheckedValue = value;
 | |
|         this.emit("updatechecked");
 | |
|       },
 | |
|       // The preference for having this button visible.
 | |
|       visibilityswitch: `devtools.${id}.enabled`,
 | |
|       // The toolbar has a container at the start and end of the toolbar for
 | |
|       // holding buttons. By default the buttons are placed in the end container.
 | |
|       isInStartContainer: !!isInStartContainer,
 | |
|       experimentalURL,
 | |
|     };
 | |
|     if (typeof setup == "function") {
 | |
|       const onChange = () => {
 | |
|         button.emit("updatechecked");
 | |
|       };
 | |
|       setup(this, onChange);
 | |
|       // Save a reference to the cleanup method that will unregister the onChange
 | |
|       // callback. Immediately bind the function argument so that we don't have to
 | |
|       // also save a reference to them.
 | |
|       button.teardown = teardown.bind(options, this, onChange);
 | |
|     }
 | |
|     button.isVisible = this._commandIsVisible(button);
 | |
| 
 | |
|     EventEmitter.decorate(button);
 | |
| 
 | |
|     return button;
 | |
|   },
 | |
| 
 | |
|   _splitConsoleOnKeypress(e) {
 | |
|     if (e.keyCode === KeyCodes.DOM_VK_ESCAPE) {
 | |
|       this.toggleSplitConsole();
 | |
|       // If the debugger is paused, don't let the ESC key stop any pending navigation.
 | |
|       // If the host is page, don't let the ESC stop the load of the webconsole frame.
 | |
|       if (
 | |
|         this.threadFront.state == "paused" ||
 | |
|         this.hostType === Toolbox.HostType.PAGE
 | |
|       ) {
 | |
|         e.preventDefault();
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Add a shortcut key that should work when a split console
 | |
|    * has focus to the toolbox.
 | |
|    *
 | |
|    * @param {String} key
 | |
|    *        The electron key shortcut.
 | |
|    * @param {Function} handler
 | |
|    *        The callback that should be called when the provided key shortcut is pressed.
 | |
|    * @param {String} whichTool
 | |
|    *        The tool the key belongs to. The corresponding handler will only be triggered
 | |
|    *        if this tool is active.
 | |
|    */
 | |
|   useKeyWithSplitConsole(key, handler, whichTool) {
 | |
|     this.shortcuts.on(key, event => {
 | |
|       if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) {
 | |
|         handler();
 | |
|         event.preventDefault();
 | |
|       }
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   _addWindowListeners() {
 | |
|     this.win.addEventListener("unload", this.destroy);
 | |
|     this.win.addEventListener("message", this._onBrowserMessage, true);
 | |
|   },
 | |
| 
 | |
|   _removeWindowListeners() {
 | |
|     // The host iframe's contentDocument may already be gone.
 | |
|     if (this.win) {
 | |
|       this.win.removeEventListener("unload", this.destroy);
 | |
|       this.win.removeEventListener("message", this._onBrowserMessage, true);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // Called whenever the chrome send a message
 | |
|   _onBrowserMessage(event) {
 | |
|     if (event.data?.name === "switched-host") {
 | |
|       this._onSwitchedHost(event.data);
 | |
|     }
 | |
|     if (event.data?.name === "switched-host-to-tab") {
 | |
|       this._onSwitchedHostToTab(event.data.browsingContextID);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _saveSplitConsoleHeight() {
 | |
|     Services.prefs.setIntPref(
 | |
|       SPLITCONSOLE_HEIGHT_PREF,
 | |
|       this.webconsolePanel.height
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Make sure that the console is showing up properly based on all the
 | |
|    * possible conditions.
 | |
|    *   1) If the console tab is selected, then regardless of split state
 | |
|    *      it should take up the full height of the deck, and we should
 | |
|    *      hide the deck and splitter.
 | |
|    *   2) If the console tab is not selected and it is split, then we should
 | |
|    *      show the splitter, deck, and console.
 | |
|    *   3) If the console tab is not selected and it is *not* split,
 | |
|    *      then we should hide the console and splitter, and show the deck
 | |
|    *      at full height.
 | |
|    */
 | |
|   _refreshConsoleDisplay() {
 | |
|     const deck = this.doc.getElementById("toolbox-deck");
 | |
|     const webconsolePanel = this.webconsolePanel;
 | |
|     const splitter = this.doc.getElementById("toolbox-console-splitter");
 | |
|     const openedConsolePanel = this.currentToolId === "webconsole";
 | |
| 
 | |
|     if (openedConsolePanel) {
 | |
|       deck.collapsed = true;
 | |
|       splitter.hidden = true;
 | |
|       webconsolePanel.collapsed = false;
 | |
|     } else {
 | |
|       deck.collapsed = false;
 | |
|       splitter.hidden = !this.splitConsole;
 | |
|       webconsolePanel.collapsed = !this.splitConsole;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Handle any custom key events.  Returns true if there was a custom key
 | |
|    * binding run.
 | |
|    * @param {string} toolId Which tool to run the command on (skip if not
 | |
|    * current)
 | |
|    */
 | |
|   fireCustomKey(toolId) {
 | |
|     const toolDefinition = gDevTools.getToolDefinition(toolId);
 | |
| 
 | |
|     if (
 | |
|       toolDefinition.onkey &&
 | |
|       (this.currentToolId === toolId ||
 | |
|         (toolId == "webconsole" && this.splitConsole))
 | |
|     ) {
 | |
|       toolDefinition.onkey(this.getCurrentPanel(), this);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Build the notification box as soon as needed.
 | |
|    */
 | |
|   get notificationBox() {
 | |
|     if (!this._notificationBox) {
 | |
|       let { NotificationBox, PriorityLevels } = this.browserRequire(
 | |
|         "devtools/client/shared/components/NotificationBox"
 | |
|       );
 | |
| 
 | |
|       NotificationBox = this.React.createFactory(NotificationBox);
 | |
| 
 | |
|       // Render NotificationBox and assign priority levels to it.
 | |
|       const box = this.doc.getElementById("toolbox-notificationbox");
 | |
|       this._notificationBox = Object.assign(
 | |
|         this.ReactDOM.render(NotificationBox({}), box),
 | |
|         PriorityLevels
 | |
|       );
 | |
|     }
 | |
|     return this._notificationBox;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Build the options for changing hosts. Called every time
 | |
|    * the host changes.
 | |
|    */
 | |
|   _buildDockOptions() {
 | |
|     if (!this.descriptorFront.isLocalTab) {
 | |
|       this.component.setDockOptionsEnabled(false);
 | |
|       this.component.setCanCloseToolbox(false);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.component.setDockOptionsEnabled(true);
 | |
|     this.component.setCanCloseToolbox(
 | |
|       this.hostType !== Toolbox.HostType.WINDOW
 | |
|     );
 | |
| 
 | |
|     const hostTypes = [];
 | |
|     for (const type in Toolbox.HostType) {
 | |
|       const position = Toolbox.HostType[type];
 | |
|       if (
 | |
|         position == Toolbox.HostType.BROWSERTOOLBOX ||
 | |
|         position == Toolbox.HostType.PAGE
 | |
|       ) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       hostTypes.push({
 | |
|         position,
 | |
|         switchHost: this.switchHost.bind(this, position),
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     this.component.setCurrentHostType(this.hostType);
 | |
|     this.component.setHostTypes(hostTypes);
 | |
|   },
 | |
| 
 | |
|   postMessage(msg) {
 | |
|     // We sometime try to send messages in middle of destroy(), where the
 | |
|     // toolbox iframe may already be detached.
 | |
|     if (!this._destroyer) {
 | |
|       // Toolbox document is still chrome and disallow identifying message
 | |
|       // origin via event.source as it is null. So use a custom id.
 | |
|       msg.frameId = this.frameId;
 | |
|       this.topWindow.postMessage(msg, "*");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * This will fetch the panel definitions from the constants in definitions module
 | |
|    * and populate the state within the ToolboxController component.
 | |
|    */
 | |
|   async _buildInitialPanelDefinitions() {
 | |
|     // Get the initial list of tab definitions. This list can be amended at a later time
 | |
|     // by tools registering themselves.
 | |
|     const definitions = gDevTools.getToolDefinitionArray();
 | |
|     definitions.forEach(definition => this._buildPanelForTool(definition));
 | |
| 
 | |
|     // Get the definitions that will only affect the main tab area.
 | |
|     this.panelDefinitions = definitions.filter(
 | |
|       definition =>
 | |
|         definition.isToolSupported(this) && definition.id !== "options"
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   async _setInitialMeatballState() {
 | |
|     let disableAutohide, pseudoLocale;
 | |
|     // Popup auto-hide disabling is only available in browser toolbox and webextension toolboxes.
 | |
|     if (
 | |
|       this.isBrowserToolbox ||
 | |
|       this.descriptorFront.isWebExtensionDescriptor
 | |
|     ) {
 | |
|       disableAutohide = await this._isDisableAutohideEnabled();
 | |
|     }
 | |
|     // Pseudo locale items are only displayed in the browser toolbox
 | |
|     if (this.isBrowserToolbox) {
 | |
|       pseudoLocale = await this.getPseudoLocale();
 | |
|     }
 | |
|     // Parallelize the asynchronous calls, so that the DOM is only updated once when
 | |
|     // updating the React components.
 | |
|     if (typeof disableAutohide == "boolean") {
 | |
|       this.component.setDisableAutohide(disableAutohide);
 | |
|     }
 | |
|     if (typeof pseudoLocale == "string") {
 | |
|       this.component.setPseudoLocale(pseudoLocale);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Initiate ToolboxController React component and all it's properties. Do the initial render.
 | |
|    *
 | |
|    * @param {Object} fluentBundles
 | |
|    *        A FluentBundle instance used to display any localized text in the React component.
 | |
|    */
 | |
|   _mountReactComponent(fluentBundles) {
 | |
|     // Ensure the toolbar doesn't try to render until the tool is ready.
 | |
|     const element = this.React.createElement(this.ToolboxController, {
 | |
|       L10N,
 | |
|       fluentBundles,
 | |
|       currentToolId: this.currentToolId,
 | |
|       selectTool: this.selectTool,
 | |
|       toggleOptions: this.toggleOptions,
 | |
|       toggleSplitConsole: this.toggleSplitConsole,
 | |
|       toggleNoAutohide: this.toggleNoAutohide,
 | |
|       disablePseudoLocale: this.disablePseudoLocale,
 | |
|       enableAccentedPseudoLocale: this.enableAccentedPseudoLocale,
 | |
|       enableBidiPseudoLocale: this.enableBidiPseudoLocale,
 | |
|       closeToolbox: this.closeToolbox,
 | |
|       focusButton: this._onToolbarFocus,
 | |
|       toolbox: this,
 | |
|       onTabsOrderUpdated: this._onTabsOrderUpdated,
 | |
|     });
 | |
| 
 | |
|     this.component = this.ReactDOM.render(element, this._componentMount);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Reset tabindex attributes across all focusable elements inside the toolbar.
 | |
|    * Only have one element with tabindex=0 at a time to make sure that tabbing
 | |
|    * results in navigating away from the toolbar container.
 | |
|    * @param  {FocusEvent} event
 | |
|    */
 | |
|   _onToolbarFocus(id) {
 | |
|     this.component.setFocusedButton(id);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * On left/right arrow press, attempt to move the focus inside the toolbar to
 | |
|    * the previous/next focusable element. This is not in the React component
 | |
|    * as it is difficult to coordinate between different component elements.
 | |
|    * The components are responsible for setting the correct tabindex value
 | |
|    * for if they are the focused element.
 | |
|    * @param  {KeyboardEvent} event
 | |
|    */
 | |
|   _onToolbarArrowKeypress(event) {
 | |
|     const { key, target, ctrlKey, shiftKey, altKey, metaKey } = event;
 | |
| 
 | |
|     // If any of the modifier keys are pressed do not attempt navigation as it
 | |
|     // might conflict with global shortcuts (Bug 1327972).
 | |
|     if (ctrlKey || shiftKey || altKey || metaKey) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const buttons = [...this._tabBar.querySelectorAll("button")];
 | |
|     const curIndex = buttons.indexOf(target);
 | |
| 
 | |
|     if (curIndex === -1) {
 | |
|       console.warn(
 | |
|         target +
 | |
|           " is not found among Developer Tools tab bar " +
 | |
|           "focusable elements."
 | |
|       );
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let newTarget;
 | |
|     const firstTabIndex = 0;
 | |
|     const lastTabIndex = buttons.length - 1;
 | |
|     const nextOrLastTabIndex = Math.min(lastTabIndex, curIndex + 1);
 | |
|     const previousOrFirstTabIndex = Math.max(firstTabIndex, curIndex - 1);
 | |
|     const ltr = this.direction === "ltr";
 | |
| 
 | |
|     if (key === "ArrowLeft") {
 | |
|       // Do nothing if already at the beginning.
 | |
|       if (
 | |
|         (ltr && curIndex === firstTabIndex) ||
 | |
|         (!ltr && curIndex === lastTabIndex)
 | |
|       ) {
 | |
|         return;
 | |
|       }
 | |
|       newTarget = buttons[ltr ? previousOrFirstTabIndex : nextOrLastTabIndex];
 | |
|     } else if (key === "ArrowRight") {
 | |
|       // Do nothing if already at the end.
 | |
|       if (
 | |
|         (ltr && curIndex === lastTabIndex) ||
 | |
|         (!ltr && curIndex === firstTabIndex)
 | |
|       ) {
 | |
|         return;
 | |
|       }
 | |
|       newTarget = buttons[ltr ? nextOrLastTabIndex : previousOrFirstTabIndex];
 | |
|     } else {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     newTarget.focus();
 | |
| 
 | |
|     event.preventDefault();
 | |
|     event.stopPropagation();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Add buttons to the UI as specified in devtools/client/definitions.js
 | |
|    */
 | |
|   _buildButtons() {
 | |
|     // Beyond the normal preference filtering
 | |
|     this.toolbarButtons = [
 | |
|       this._buildErrorCountButton(),
 | |
|       this._buildPickerButton(),
 | |
|       this._buildFrameButton(),
 | |
|     ];
 | |
| 
 | |
|     ToolboxButtons.forEach(definition => {
 | |
|       const button = this._createButtonState(definition);
 | |
|       this.toolbarButtons.push(button);
 | |
|     });
 | |
| 
 | |
|     this.component.setToolboxButtons(this.toolbarButtons);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Button to select a frame for the inspector to target.
 | |
|    */
 | |
|   _buildFrameButton() {
 | |
|     this.frameButton = this._createButtonState({
 | |
|       id: "command-button-frames",
 | |
|       description: L10N.getStr("toolbox.frames.tooltip"),
 | |
|       isToolSupported: toolbox => {
 | |
|         return toolbox.target.getTrait("frames");
 | |
|       },
 | |
|       isCurrentlyVisible: () => {
 | |
|         const hasFrames = this.frameMap.size > 1;
 | |
|         const isOnOptionsPanel = this.currentToolId === "options";
 | |
|         return hasFrames || isOnOptionsPanel;
 | |
|       },
 | |
|     });
 | |
| 
 | |
|     return this.frameButton;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Button to display the number of errors.
 | |
|    */
 | |
|   _buildErrorCountButton() {
 | |
|     this.errorCountButton = this._createButtonState({
 | |
|       id: "command-button-errorcount",
 | |
|       isInStartContainer: false,
 | |
|       isToolSupported: toolbox => true,
 | |
|       description: L10N.getStr("toolbox.errorCountButton.description"),
 | |
|     });
 | |
|     // Use updateErrorCountButton to set some properties so we don't have to repeat
 | |
|     // the logic here.
 | |
|     this.updateErrorCountButton();
 | |
| 
 | |
|     return this.errorCountButton;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Toggle the picker, but also decide whether or not the highlighter should
 | |
|    * focus the window. This is only desirable when the toolbox is mounted to the
 | |
|    * window. When devtools is free floating, then the target window should not
 | |
|    * pop in front of the viewer when the picker is clicked.
 | |
|    *
 | |
|    * Note: Toggle picker can be overwritten by panel other than the inspector to
 | |
|    * allow for custom picker behaviour.
 | |
|    */
 | |
|   async _onPickerClick() {
 | |
|     const focus =
 | |
|       this.hostType === Toolbox.HostType.BOTTOM ||
 | |
|       this.hostType === Toolbox.HostType.LEFT ||
 | |
|       this.hostType === Toolbox.HostType.RIGHT;
 | |
|     const currentPanel = this.getCurrentPanel();
 | |
|     if (currentPanel.togglePicker) {
 | |
|       currentPanel.togglePicker(focus);
 | |
|     } else {
 | |
|       this.nodePicker.togglePicker(focus);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * If the picker is activated, then allow the Escape key to deactivate the
 | |
|    * functionality instead of the default behavior of toggling the console.
 | |
|    */
 | |
|   _onPickerKeypress(event) {
 | |
|     if (event.keyCode === KeyCodes.DOM_VK_ESCAPE) {
 | |
|       const currentPanel = this.getCurrentPanel();
 | |
|       if (currentPanel.cancelPicker) {
 | |
|         currentPanel.cancelPicker();
 | |
|       } else {
 | |
|         this.nodePicker.stop({ canceled: true });
 | |
|       }
 | |
|       // Stop the console from toggling.
 | |
|       event.stopImmediatePropagation();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   async _onPickerStarting() {
 | |
|     if (this.isDestroying()) {
 | |
|       return;
 | |
|     }
 | |
|     this.tellRDMAboutPickerState(true, PICKER_TYPES.ELEMENT);
 | |
|     this.pickerButton.isChecked = true;
 | |
|     await this.selectTool("inspector", "inspect_dom");
 | |
|     // turn off color picker when node picker is starting
 | |
|     this.getPanel("inspector").hideEyeDropper();
 | |
|     this.on("select", this._onToolSelectedStopPicker);
 | |
|   },
 | |
| 
 | |
|   async _onPickerStarted() {
 | |
|     this.doc.addEventListener("keypress", this._onPickerKeypress, true);
 | |
|   },
 | |
| 
 | |
|   _onPickerStopped() {
 | |
|     if (this.isDestroying()) {
 | |
|       return;
 | |
|     }
 | |
|     this.tellRDMAboutPickerState(false, PICKER_TYPES.ELEMENT);
 | |
|     this.off("select", this._onToolSelectedStopPicker);
 | |
|     this.doc.removeEventListener("keypress", this._onPickerKeypress, true);
 | |
|     this.pickerButton.isChecked = false;
 | |
|   },
 | |
| 
 | |
|   _onToolSelectedStopPicker() {
 | |
|     this.nodePicker.stop({ canceled: true });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * When the picker is canceled, make sure the toolbox
 | |
|    * gets the focus.
 | |
|    */
 | |
|   _onPickerCanceled() {
 | |
|     if (this.hostType !== Toolbox.HostType.WINDOW) {
 | |
|       this.win.focus();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onPickerPicked(nodeFront) {
 | |
|     this.selection.setNodeFront(nodeFront, { reason: "picker-node-picked" });
 | |
|   },
 | |
| 
 | |
|   _onPickerPreviewed(nodeFront) {
 | |
|     this.selection.setNodeFront(nodeFront, { reason: "picker-node-previewed" });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * RDM sometimes simulates touch events. For this to work correctly at all times, it
 | |
|    * needs to know when the picker is active or not.
 | |
|    * This method communicates with the RDM Manager if it exists.
 | |
|    *
 | |
|    * @param {Boolean} state
 | |
|    * @param {String} pickerType
 | |
|    *        One of devtools/shared/picker-constants
 | |
|    */
 | |
|   async tellRDMAboutPickerState(state, pickerType) {
 | |
|     const { localTab } = this.target;
 | |
| 
 | |
|     if (!ResponsiveUIManager.isActiveForTab(localTab)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const ui = ResponsiveUIManager.getResponsiveUIForTab(localTab);
 | |
|     await ui.responsiveFront.setElementPickerState(state, pickerType);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * The element picker button enables the ability to select a DOM node by clicking
 | |
|    * it on the page.
 | |
|    */
 | |
|   _buildPickerButton() {
 | |
|     this.pickerButton = this._createButtonState({
 | |
|       id: "command-button-pick",
 | |
|       className: this._getPickerAdditionalClassName(),
 | |
|       description: this._getPickerTooltip(),
 | |
|       onClick: this._onPickerClick,
 | |
|       isInStartContainer: true,
 | |
|       isToolSupported: toolbox => {
 | |
|         return toolbox.target.getTrait("frames");
 | |
|       },
 | |
|     });
 | |
| 
 | |
|     return this.pickerButton;
 | |
|   },
 | |
| 
 | |
|   _getPickerAdditionalClassName() {
 | |
|     if (this.isDebugTargetFenix()) {
 | |
|       return "remote-fenix";
 | |
|     }
 | |
|     return null;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the tooltip for the element picker button.
 | |
|    * It has multiple possible keyboard shortcuts for macOS.
 | |
|    *
 | |
|    * @return {String}
 | |
|    */
 | |
|   _getPickerTooltip() {
 | |
|     let shortcut = L10N.getStr("toolbox.elementPicker.key");
 | |
|     shortcut = KeyShortcuts.parseElectronKey(this.win, shortcut);
 | |
|     shortcut = KeyShortcuts.stringify(shortcut);
 | |
|     const shortcutMac = L10N.getStr("toolbox.elementPicker.mac.key");
 | |
|     const isMac = Services.appinfo.OS === "Darwin";
 | |
| 
 | |
|     let label;
 | |
|     if (this.isDebugTargetFenix()) {
 | |
|       label = isMac
 | |
|         ? "toolbox.androidElementPicker.mac.tooltip"
 | |
|         : "toolbox.androidElementPicker.tooltip";
 | |
|     } else {
 | |
|       label = isMac
 | |
|         ? "toolbox.elementPicker.mac.tooltip"
 | |
|         : "toolbox.elementPicker.tooltip";
 | |
|     }
 | |
| 
 | |
|     return isMac
 | |
|       ? L10N.getFormatStr(label, shortcut, shortcutMac)
 | |
|       : L10N.getFormatStr(label, shortcut);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Apply the current cache setting from devtools.cache.disabled to this
 | |
|    * toolbox's tab.
 | |
|    */
 | |
|   async _applyCacheSettings() {
 | |
|     const pref = "devtools.cache.disabled";
 | |
|     const cacheDisabled = Services.prefs.getBoolPref(pref);
 | |
| 
 | |
|     await this.commands.targetConfigurationCommand.updateConfiguration({
 | |
|       cacheDisabled,
 | |
|     });
 | |
| 
 | |
|     // This event is only emitted for tests in order to know when to reload
 | |
|     if (flags.testing) {
 | |
|       this.emit("cache-reconfigured");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Apply the custom formatter setting (from `devtools.custom-formatters.enabled`) to this
 | |
|    * toolbox's tab.
 | |
|    */
 | |
|   async _applyCustomFormatterSetting() {
 | |
|     if (!this.commands) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const customFormatters =
 | |
|       Services.prefs.getBoolPref("devtools.custom-formatters", false) &&
 | |
|       Services.prefs.getBoolPref("devtools.custom-formatters.enabled", false);
 | |
| 
 | |
|     await this.commands.targetConfigurationCommand.updateConfiguration({
 | |
|       customFormatters,
 | |
|     });
 | |
| 
 | |
|     this.emitForTests("custom-formatters-reconfigured");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Apply the current service workers testing setting from
 | |
|    * devtools.serviceWorkers.testing.enabled to this toolbox's tab.
 | |
|    */
 | |
|   _applyServiceWorkersTestingSettings() {
 | |
|     const pref = "devtools.serviceWorkers.testing.enabled";
 | |
|     const serviceWorkersTestingEnabled = Services.prefs.getBoolPref(pref);
 | |
|     this.commands.targetConfigurationCommand.updateConfiguration({
 | |
|       serviceWorkersTestingEnabled,
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Update the visibility of the buttons.
 | |
|    */
 | |
|   updateToolboxButtonsVisibility() {
 | |
|     this.toolbarButtons.forEach(button => {
 | |
|       button.isVisible = this._commandIsVisible(button);
 | |
|     });
 | |
|     this.component.setToolboxButtons(this.toolbarButtons);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Update the buttons.
 | |
|    */
 | |
|   updateToolboxButtons() {
 | |
|     const inspectorFront = this.target.getCachedFront("inspector");
 | |
|     // two of the buttons have highlighters that need to be cleared
 | |
|     // on will-navigate, otherwise we hold on to the stale highlighter
 | |
|     const hasHighlighters =
 | |
|       inspectorFront &&
 | |
|       (inspectorFront.hasHighlighter("RulersHighlighter") ||
 | |
|         inspectorFront.hasHighlighter("MeasuringToolHighlighter"));
 | |
|     if (hasHighlighters) {
 | |
|       inspectorFront.destroyHighlighters();
 | |
|       this.component.setToolboxButtons(this.toolbarButtons);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Visually update picker button.
 | |
|    * This function is called on every "select" event. Newly selected panel can
 | |
|    * update the visual state of the picker button such as disabled state,
 | |
|    * additional CSS classes (className), and tooltip (description).
 | |
|    */
 | |
|   updatePickerButton() {
 | |
|     const button = this.pickerButton;
 | |
|     const currentPanel = this.getCurrentPanel();
 | |
| 
 | |
|     if (currentPanel?.updatePickerButton) {
 | |
|       currentPanel.updatePickerButton();
 | |
|     } else {
 | |
|       // If the current panel doesn't define a custom updatePickerButton,
 | |
|       // revert the button to its default state
 | |
|       button.description = this._getPickerTooltip();
 | |
|       button.className = this._getPickerAdditionalClassName();
 | |
|       button.disabled = null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Update the visual state of the Frame picker button.
 | |
|    */
 | |
|   updateFrameButton() {
 | |
|     if (this.isDestroying()) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (this.currentToolId === "options" && this.frameMap.size <= 1) {
 | |
|       // If the button is only visible because the user is on the Options panel, disable
 | |
|       // the button and set an appropriate description.
 | |
|       this.frameButton.disabled = true;
 | |
|       this.frameButton.description = L10N.getStr(
 | |
|         "toolbox.frames.disabled.tooltip"
 | |
|       );
 | |
|     } else {
 | |
|       // Otherwise, enable the button and update the description.
 | |
|       this.frameButton.disabled = false;
 | |
|       this.frameButton.description = L10N.getStr("toolbox.frames.tooltip");
 | |
|     }
 | |
| 
 | |
|     // Highlight the button when a child frame is selected and visible.
 | |
|     const selectedFrame = this.frameMap.get(this.selectedFrameId) || {};
 | |
| 
 | |
|     // We need to do something a bit different to avoid some test failures. This function
 | |
|     // can be called from onWillNavigate, and the current target might have this `traits`
 | |
|     // property nullifed, which is unfortunate as that's what isToolSupported is checking,
 | |
|     // so it will throw.
 | |
|     // So here, we check first if the button isn't going to be visible anyway (it only checks
 | |
|     // for this.frameMap size) so we don't call _commandIsVisible.
 | |
|     const isVisible = !this.frameButton.isCurrentlyVisible()
 | |
|       ? false
 | |
|       : this._commandIsVisible(this.frameButton);
 | |
| 
 | |
|     this.frameButton.isVisible = isVisible;
 | |
| 
 | |
|     if (isVisible) {
 | |
|       this.frameButton.isChecked = !selectedFrame.isTopLevel;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   updateErrorCountButton() {
 | |
|     this.errorCountButton.isVisible =
 | |
|       this._commandIsVisible(this.errorCountButton) && this._errorCount > 0;
 | |
|     this.errorCountButton.errorCount = this._errorCount;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Ensure the visibility of each toolbox button matches the preference value.
 | |
|    */
 | |
|   _commandIsVisible(button) {
 | |
|     const { isToolSupported, isCurrentlyVisible, visibilityswitch } = button;
 | |
| 
 | |
|     if (!Services.prefs.getBoolPref(visibilityswitch, true)) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (isToolSupported && !isToolSupported(this)) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (isCurrentlyVisible && !isCurrentlyVisible()) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Build a panel for a tool definition.
 | |
|    *
 | |
|    * @param {string} toolDefinition
 | |
|    *        Tool definition of the tool to build a tab for.
 | |
|    */
 | |
|   _buildPanelForTool(toolDefinition) {
 | |
|     if (!toolDefinition.isToolSupported(this)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const deck = this.doc.getElementById("toolbox-deck");
 | |
|     const id = toolDefinition.id;
 | |
| 
 | |
|     if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
 | |
|       toolDefinition.ordinal = MAX_ORDINAL;
 | |
|     }
 | |
| 
 | |
|     if (!toolDefinition.bgTheme) {
 | |
|       toolDefinition.bgTheme = "theme-toolbar";
 | |
|     }
 | |
|     const panel = this.doc.createXULElement("vbox");
 | |
|     panel.className = "toolbox-panel " + toolDefinition.bgTheme;
 | |
| 
 | |
|     // There is already a container for the webconsole frame.
 | |
|     if (!this.doc.getElementById("toolbox-panel-" + id)) {
 | |
|       panel.id = "toolbox-panel-" + id;
 | |
|     }
 | |
| 
 | |
|     deck.appendChild(panel);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Lazily created map of the additional tools registered to this toolbox.
 | |
|    *
 | |
|    * @returns {Map<string, object>}
 | |
|    *          a map of the tools definitions registered to this
 | |
|    *          particular toolbox (the key is the toolId string, the value
 | |
|    *          is the tool definition plain javascript object).
 | |
|    */
 | |
|   get additionalToolDefinitions() {
 | |
|     if (!this._additionalToolDefinitions) {
 | |
|       this._additionalToolDefinitions = new Map();
 | |
|     }
 | |
| 
 | |
|     return this._additionalToolDefinitions;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Retrieve the array of the additional tools registered to this toolbox.
 | |
|    *
 | |
|    * @return {Array<object>}
 | |
|    *         the array of additional tool definitions registered on this toolbox.
 | |
|    */
 | |
|   getAdditionalTools() {
 | |
|     if (this._additionalToolDefinitions) {
 | |
|       return Array.from(this.additionalToolDefinitions.values());
 | |
|     }
 | |
|     return [];
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the additional tools that have been registered and are visible.
 | |
|    *
 | |
|    * @return {Array<object>}
 | |
|    *         the array of additional tool definitions registered on this toolbox.
 | |
|    */
 | |
|   getVisibleAdditionalTools() {
 | |
|     return this.visibleAdditionalTools.map(toolId =>
 | |
|       this.additionalToolDefinitions.get(toolId)
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Test the existence of a additional tools registered to this toolbox by tool id.
 | |
|    *
 | |
|    * @param {string} toolId
 | |
|    *        the id of the tool to test for existence.
 | |
|    *
 | |
|    * @return {boolean}
 | |
|    *
 | |
|    */
 | |
|   hasAdditionalTool(toolId) {
 | |
|     return this.additionalToolDefinitions.has(toolId);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Register and load an additional tool on this particular toolbox.
 | |
|    *
 | |
|    * @param {object} definition
 | |
|    *        the additional tool definition to register and add to this toolbox.
 | |
|    */
 | |
|   addAdditionalTool(definition) {
 | |
|     if (!definition.id) {
 | |
|       throw new Error("Tool definition id is missing");
 | |
|     }
 | |
| 
 | |
|     if (this.isToolRegistered(definition.id)) {
 | |
|       throw new Error("Tool definition already registered: " + definition.id);
 | |
|     }
 | |
| 
 | |
|     this.additionalToolDefinitions.set(definition.id, definition);
 | |
|     this.visibleAdditionalTools = [
 | |
|       ...this.visibleAdditionalTools,
 | |
|       definition.id,
 | |
|     ];
 | |
| 
 | |
|     const buildPanel = () => this._buildPanelForTool(definition);
 | |
| 
 | |
|     if (this.isReady) {
 | |
|       buildPanel();
 | |
|     } else {
 | |
|       this.once("ready", buildPanel);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Retrieve the registered inspector extension sidebars
 | |
|    * (used by the inspector panel during its deferred initialization).
 | |
|    */
 | |
|   get inspectorExtensionSidebars() {
 | |
|     return this._inspectorExtensionSidebars;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Register an extension sidebar for the inspector panel.
 | |
|    *
 | |
|    * @param {String} id
 | |
|    *        An unique sidebar id
 | |
|    * @param {Object} options
 | |
|    * @param {String} options.title
 | |
|    *        A title for the sidebar
 | |
|    */
 | |
|   async registerInspectorExtensionSidebar(id, options) {
 | |
|     this._inspectorExtensionSidebars.set(id, options);
 | |
| 
 | |
|     // Defer the extension sidebar creation if the inspector
 | |
|     // has not been created yet (and do not create the inspector
 | |
|     // only to register an extension sidebar).
 | |
|     if (!this.target.getCachedFront("inspector")) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const inspector = this.getPanel("inspector");
 | |
|     if (!inspector) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     inspector.addExtensionSidebar(id, options);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Unregister an extension sidebar for the inspector panel.
 | |
|    *
 | |
|    * @param {String} id
 | |
|    *        An unique sidebar id
 | |
|    */
 | |
|   unregisterInspectorExtensionSidebar(id) {
 | |
|     // Unregister the sidebar from the toolbox if the toolbox is not already
 | |
|     // being destroyed (otherwise we would trigger a re-rendering of the
 | |
|     // inspector sidebar tabs while the toolbox is going away).
 | |
|     if (this._destroyer) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const sidebarDef = this._inspectorExtensionSidebars.get(id);
 | |
|     if (!sidebarDef) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._inspectorExtensionSidebars.delete(id);
 | |
| 
 | |
|     // Remove the created sidebar instance if the inspector panel
 | |
|     // has been already created.
 | |
|     if (!this.target.getCachedFront("inspector")) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const inspector = this.getPanel("inspector");
 | |
|     inspector.removeExtensionSidebar(id);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Unregister and unload an additional tool from this particular toolbox.
 | |
|    *
 | |
|    * @param {string} toolId
 | |
|    *        the id of the additional tool to unregister and remove.
 | |
|    */
 | |
|   removeAdditionalTool(toolId) {
 | |
|     // Early exit if the toolbox is already destroying itself.
 | |
|     if (this._destroyer) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (!this.hasAdditionalTool(toolId)) {
 | |
|       throw new Error(
 | |
|         "Tool definition not registered to this toolbox: " + toolId
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this.additionalToolDefinitions.delete(toolId);
 | |
|     this.visibleAdditionalTools = this.visibleAdditionalTools.filter(
 | |
|       id => id !== toolId
 | |
|     );
 | |
|     this.unloadTool(toolId);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Ensure the tool with the given id is loaded.
 | |
|    *
 | |
|    * @param {string} id
 | |
|    *        The id of the tool to load.
 | |
|    * @param {Object} options
 | |
|    *        Object that will be passed to the panel `open` method.
 | |
|    */
 | |
|   loadTool(id, options) {
 | |
|     let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
 | |
|     if (iframe) {
 | |
|       const panel = this._toolPanels.get(id);
 | |
|       return new Promise(resolve => {
 | |
|         if (panel) {
 | |
|           resolve(panel);
 | |
|         } else {
 | |
|           this.once(id + "-ready", initializedPanel => {
 | |
|             resolve(initializedPanel);
 | |
|           });
 | |
|         }
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     return new Promise((resolve, reject) => {
 | |
|       // Retrieve the tool definition (from the global or the per-toolbox tool maps)
 | |
|       const definition = this.getToolDefinition(id);
 | |
| 
 | |
|       if (!definition) {
 | |
|         reject(new Error("no such tool id " + id));
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       iframe = this.doc.createXULElement("iframe");
 | |
|       iframe.className = "toolbox-panel-iframe";
 | |
|       iframe.id = "toolbox-panel-iframe-" + id;
 | |
|       iframe.setAttribute("flex", 1);
 | |
|       iframe.setAttribute("forceOwnRefreshDriver", "");
 | |
|       iframe.tooltip = "aHTMLTooltip";
 | |
|       iframe.style.visibility = "hidden";
 | |
| 
 | |
|       gDevTools.emit(id + "-init", this, iframe);
 | |
|       this.emit(id + "-init", iframe);
 | |
| 
 | |
|       // If no parent yet, append the frame into default location.
 | |
|       if (!iframe.parentNode) {
 | |
|         const vbox = this.doc.getElementById("toolbox-panel-" + id);
 | |
|         vbox.appendChild(iframe);
 | |
|         vbox.visibility = "visible";
 | |
|       }
 | |
| 
 | |
|       const onLoad = async () => {
 | |
|         // Prevent flicker while loading by waiting to make visible until now.
 | |
|         iframe.style.visibility = "visible";
 | |
| 
 | |
|         // Try to set the dir attribute as early as possible.
 | |
|         this.setIframeDocumentDir(iframe);
 | |
| 
 | |
|         // The build method should return a panel instance, so events can
 | |
|         // be fired with the panel as an argument. However, in order to keep
 | |
|         // backward compatibility with existing extensions do a check
 | |
|         // for a promise return value.
 | |
|         let built = definition.build(iframe.contentWindow, this, this.commands);
 | |
| 
 | |
|         if (!(typeof built.then == "function")) {
 | |
|           const panel = built;
 | |
|           iframe.panel = panel;
 | |
| 
 | |
|           // The panel instance is expected to fire (and listen to) various
 | |
|           // framework events, so make sure it's properly decorated with
 | |
|           // appropriate API (on, off, once, emit).
 | |
|           // In this case we decorate panel instances directly returned by
 | |
|           // the tool definition 'build' method.
 | |
|           if (typeof panel.emit == "undefined") {
 | |
|             EventEmitter.decorate(panel);
 | |
|           }
 | |
| 
 | |
|           gDevTools.emit(id + "-build", this, panel);
 | |
|           this.emit(id + "-build", panel);
 | |
| 
 | |
|           // The panel can implement an 'open' method for asynchronous
 | |
|           // initialization sequence.
 | |
|           if (typeof panel.open == "function") {
 | |
|             built = panel.open(options);
 | |
|           } else {
 | |
|             built = new Promise(resolve => {
 | |
|               resolve(panel);
 | |
|             });
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         // Wait till the panel is fully ready and fire 'ready' events.
 | |
|         Promise.resolve(built).then(panel => {
 | |
|           this._toolPanels.set(id, panel);
 | |
| 
 | |
|           // Make sure to decorate panel object with event API also in case
 | |
|           // where the tool definition 'build' method returns only a promise
 | |
|           // and the actual panel instance is available as soon as the
 | |
|           // promise is resolved.
 | |
|           if (typeof panel.emit == "undefined") {
 | |
|             EventEmitter.decorate(panel);
 | |
|           }
 | |
| 
 | |
|           gDevTools.emit(id + "-ready", this, panel);
 | |
|           this.emit(id + "-ready", panel);
 | |
| 
 | |
|           resolve(panel);
 | |
|         }, console.error);
 | |
|       };
 | |
| 
 | |
|       iframe.setAttribute("src", definition.url);
 | |
|       if (definition.panelLabel) {
 | |
|         iframe.setAttribute("aria-label", definition.panelLabel);
 | |
|       }
 | |
| 
 | |
|       // Depending on the host, iframe.contentWindow is not always
 | |
|       // defined at this moment. If it is not defined, we use an
 | |
|       // event listener on the iframe DOM node. If it's defined,
 | |
|       // we use the chromeEventHandler. We can't use a listener
 | |
|       // on the DOM node every time because this won't work
 | |
|       // if the (xul chrome) iframe is loaded in a content docshell.
 | |
|       if (iframe.contentWindow) {
 | |
|         DOMHelpers.onceDOMReady(iframe.contentWindow, onLoad);
 | |
|       } else {
 | |
|         const callback = () => {
 | |
|           iframe.removeEventListener("DOMContentLoaded", callback);
 | |
|           onLoad();
 | |
|         };
 | |
| 
 | |
|         iframe.addEventListener("DOMContentLoaded", callback);
 | |
|       }
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Set the dir attribute on the content document element of the provided iframe.
 | |
|    *
 | |
|    * @param {IFrameElement} iframe
 | |
|    */
 | |
|   setIframeDocumentDir(iframe) {
 | |
|     const docEl = iframe.contentWindow?.document.documentElement;
 | |
|     if (!docEl || docEl.namespaceURI !== HTML_NS) {
 | |
|       // Bail out if the content window or document is not ready or if the document is not
 | |
|       // HTML.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (docEl.hasAttribute("dir")) {
 | |
|       // Set the dir attribute value only if dir is already present on the document.
 | |
|       docEl.setAttribute("dir", this.direction);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Mark all in collection as unselected; and id as selected
 | |
|    * @param {string} collection
 | |
|    *        DOM collection of items
 | |
|    * @param {string} id
 | |
|    *        The Id of the item within the collection to select
 | |
|    */
 | |
|   selectSingleNode(collection, id) {
 | |
|     [...collection].forEach(node => {
 | |
|       if (node.id === id) {
 | |
|         node.setAttribute("selected", "true");
 | |
|         node.setAttribute("aria-selected", "true");
 | |
|       } else {
 | |
|         node.removeAttribute("selected");
 | |
|         node.removeAttribute("aria-selected");
 | |
|       }
 | |
|       // The webconsole panel is in a special location due to split console
 | |
|       if (!node.id) {
 | |
|         node = this.webconsolePanel;
 | |
|       }
 | |
| 
 | |
|       const iframe = node.querySelector(".toolbox-panel-iframe");
 | |
|       if (iframe) {
 | |
|         let visible = node.id == id;
 | |
|         // Prevents hiding the split-console if it is currently enabled
 | |
|         if (node == this.webconsolePanel && this.splitConsole) {
 | |
|           visible = true;
 | |
|         }
 | |
|         this.setIframeVisible(iframe, visible);
 | |
|       }
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Make a privileged iframe visible/hidden.
 | |
|    *
 | |
|    * For now, XUL Iframes loading chrome documents (i.e. <iframe type!="content" />)
 | |
|    * can't be hidden at platform level. And so don't support 'visibilitychange' event.
 | |
|    *
 | |
|    * This helper workarounds that by at least being able to send these kind of events.
 | |
|    * It will help panel react differently depending on them being displayed or in
 | |
|    * background.
 | |
|    */
 | |
|   setIframeVisible(iframe, visible) {
 | |
|     const state = visible ? "visible" : "hidden";
 | |
|     const win = iframe.contentWindow;
 | |
|     const doc = win.document;
 | |
|     if (doc.visibilityState != state) {
 | |
|       // 1) Overload document's `visibilityState` attribute
 | |
|       // Use defineProperty, as by default `document.visbilityState` is read only.
 | |
|       Object.defineProperty(doc, "visibilityState", {
 | |
|         value: state,
 | |
|         configurable: true,
 | |
|       });
 | |
| 
 | |
|       // 2) Fake the 'visibilitychange' event
 | |
|       doc.dispatchEvent(new win.Event("visibilitychange"));
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Switch to the tool with the given id
 | |
|    *
 | |
|    * @param {string} id
 | |
|    *        The id of the tool to switch to
 | |
|    * @param {string} reason
 | |
|    *        Reason the tool was opened
 | |
|    * @param {Object} options
 | |
|    *        Object that will be passed to the panel
 | |
|    */
 | |
|   selectTool(id, reason = "unknown", options) {
 | |
|     this.emit("panel-changed");
 | |
| 
 | |
|     if (this.currentToolId == id) {
 | |
|       const panel = this._toolPanels.get(id);
 | |
|       if (panel) {
 | |
|         // We have a panel instance, so the tool is already fully loaded.
 | |
| 
 | |
|         // re-focus tool to get key events again
 | |
|         this.focusTool(id);
 | |
| 
 | |
|         // Return the existing panel in order to have a consistent return value.
 | |
|         return Promise.resolve(panel);
 | |
|       }
 | |
|       // Otherwise, if there is no panel instance, it is still loading,
 | |
|       // so we are racing another call to selectTool with the same id.
 | |
|       return this.once("select").then(() =>
 | |
|         Promise.resolve(this._toolPanels.get(id))
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (!this.isReady) {
 | |
|       throw new Error("Can't select tool, wait for toolbox 'ready' event");
 | |
|     }
 | |
| 
 | |
|     // Check if the tool exists.
 | |
|     if (
 | |
|       this.panelDefinitions.find(definition => definition.id === id) ||
 | |
|       id === "options" ||
 | |
|       this.additionalToolDefinitions.get(id)
 | |
|     ) {
 | |
|       if (this.currentToolId) {
 | |
|         this.telemetry.toolClosed(this.currentToolId, this.sessionId, this);
 | |
|       }
 | |
| 
 | |
|       this._pingTelemetrySelectTool(id, reason);
 | |
|     } else {
 | |
|       throw new Error("No tool found");
 | |
|     }
 | |
| 
 | |
|     // and select the right iframe
 | |
|     const toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
 | |
|     this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
 | |
| 
 | |
|     this.lastUsedToolId = this.currentToolId;
 | |
|     this.currentToolId = id;
 | |
|     this._refreshConsoleDisplay();
 | |
|     if (id != "options") {
 | |
|       Services.prefs.setCharPref(this._prefs.LAST_TOOL, id);
 | |
|     }
 | |
| 
 | |
|     return this.loadTool(id, options).then(panel => {
 | |
|       // focus the tool's frame to start receiving key events
 | |
|       this.focusTool(id);
 | |
| 
 | |
|       this.emit("select", id);
 | |
|       this.emit(id + "-selected", panel);
 | |
|       return panel;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   _pingTelemetrySelectTool(id, reason) {
 | |
|     const width = Math.ceil(this.win.outerWidth / 50) * 50;
 | |
|     const panelName = this.getTelemetryPanelNameOrOther(id);
 | |
|     const prevPanelName = this.getTelemetryPanelNameOrOther(this.currentToolId);
 | |
|     const cold = !this.getPanel(id);
 | |
|     const pending = [
 | |
|       "host",
 | |
|       "width",
 | |
|       "start_state",
 | |
|       "panel_name",
 | |
|       "cold",
 | |
|       "session_id",
 | |
|     ];
 | |
| 
 | |
|     // On first load this.currentToolId === undefined so we need to skip sending
 | |
|     // a devtools.main.exit telemetry event.
 | |
|     if (this.currentToolId) {
 | |
|       this.telemetry.recordEvent("exit", prevPanelName, null, {
 | |
|         host: this._hostType,
 | |
|         width,
 | |
|         panel_name: prevPanelName,
 | |
|         next_panel: panelName,
 | |
|         reason,
 | |
|         session_id: this.sessionId,
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     this.telemetry.addEventProperties(this.topWindow, "open", "tools", null, {
 | |
|       width,
 | |
|       session_id: this.sessionId,
 | |
|     });
 | |
| 
 | |
|     if (id === "webconsole") {
 | |
|       pending.push("message_count");
 | |
|     }
 | |
| 
 | |
|     this.telemetry.preparePendingEvent(this, "enter", panelName, null, pending);
 | |
| 
 | |
|     this.telemetry.addEventProperties(this, "enter", panelName, null, {
 | |
|       host: this._hostType,
 | |
|       start_state: reason,
 | |
|       panel_name: panelName,
 | |
|       cold,
 | |
|       session_id: this.sessionId,
 | |
|     });
 | |
| 
 | |
|     if (reason !== "initial_panel") {
 | |
|       const width = Math.ceil(this.win.outerWidth / 50) * 50;
 | |
|       this.telemetry.addEventProperty(
 | |
|         this,
 | |
|         "enter",
 | |
|         panelName,
 | |
|         null,
 | |
|         "width",
 | |
|         width
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // Cold webconsole event message_count is handled in
 | |
|     // devtools/client/webconsole/webconsole-wrapper.js
 | |
|     if (!cold && id === "webconsole") {
 | |
|       this.telemetry.addEventProperty(
 | |
|         this,
 | |
|         "enter",
 | |
|         "webconsole",
 | |
|         null,
 | |
|         "message_count",
 | |
|         0
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this.telemetry.toolOpened(id, this.sessionId, this);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Focus a tool's panel by id
 | |
|    * @param  {string} id
 | |
|    *         The id of tool to focus
 | |
|    */
 | |
|   focusTool(id, state = true) {
 | |
|     const iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
 | |
| 
 | |
|     if (state) {
 | |
|       iframe.focus();
 | |
|     } else {
 | |
|       iframe.blur();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Focus split console's input line
 | |
|    */
 | |
|   focusConsoleInput() {
 | |
|     const consolePanel = this.getPanel("webconsole");
 | |
|     if (consolePanel) {
 | |
|       consolePanel.focusInput();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Disable all network logs in the console
 | |
|    */
 | |
|   disableAllConsoleNetworkLogs() {
 | |
|     const consolePanel = this.getPanel("webconsole");
 | |
|     if (consolePanel) {
 | |
|       consolePanel.hud.ui.disableAllNetworkMessages();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * If the console is split and we are focusing an element outside
 | |
|    * of the console, then store the newly focused element, so that
 | |
|    * it can be restored once the split console closes.
 | |
|    */
 | |
|   _onFocus({ originalTarget }) {
 | |
|     // Ignore any non element nodes, or any elements contained
 | |
|     // within the webconsole frame.
 | |
|     const webconsoleURL = gDevTools.getToolDefinition("webconsole").url;
 | |
|     if (
 | |
|       originalTarget.nodeType !== 1 ||
 | |
|       originalTarget.baseURI === webconsoleURL
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._lastFocusedElement = originalTarget;
 | |
|   },
 | |
| 
 | |
|   _onTabsOrderUpdated() {
 | |
|     this._combineAndSortPanelDefinitions();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens the split console.
 | |
|    *
 | |
|    * @returns {Promise} a promise that resolves once the tool has been
 | |
|    *          loaded and focused.
 | |
|    */
 | |
|   openSplitConsole() {
 | |
|     this._splitConsole = true;
 | |
|     Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, true);
 | |
|     this._refreshConsoleDisplay();
 | |
| 
 | |
|     // Ensure split console is visible if console was already loaded in background
 | |
|     const iframe = this.webconsolePanel.querySelector(".toolbox-panel-iframe");
 | |
|     if (iframe) {
 | |
|       this.setIframeVisible(iframe, true);
 | |
|     }
 | |
| 
 | |
|     return this.loadTool("webconsole").then(() => {
 | |
|       this.component.setIsSplitConsoleActive(true);
 | |
|       this.telemetry.recordEvent("activate", "split_console", null, {
 | |
|         host: this._getTelemetryHostString(),
 | |
|         width: Math.ceil(this.win.outerWidth / 50) * 50,
 | |
|         session_id: this.sessionId,
 | |
|       });
 | |
|       this.emit("split-console");
 | |
|       this.focusConsoleInput();
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Closes the split console.
 | |
|    *
 | |
|    * @returns {Promise} a promise that resolves once the tool has been
 | |
|    *          closed.
 | |
|    */
 | |
|   closeSplitConsole() {
 | |
|     this._splitConsole = false;
 | |
|     Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, false);
 | |
|     this._refreshConsoleDisplay();
 | |
|     this.component.setIsSplitConsoleActive(false);
 | |
| 
 | |
|     this.telemetry.recordEvent("deactivate", "split_console", null, {
 | |
|       host: this._getTelemetryHostString(),
 | |
|       width: Math.ceil(this.win.outerWidth / 50) * 50,
 | |
|       session_id: this.sessionId,
 | |
|     });
 | |
| 
 | |
|     this.emit("split-console");
 | |
| 
 | |
|     if (this._lastFocusedElement) {
 | |
|       this._lastFocusedElement.focus();
 | |
|     }
 | |
|     return Promise.resolve();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Toggles the split state of the webconsole.  If the webconsole panel
 | |
|    * is already selected then this command is ignored.
 | |
|    *
 | |
|    * @returns {Promise} a promise that resolves once the tool has been
 | |
|    *          opened or closed.
 | |
|    */
 | |
|   toggleSplitConsole() {
 | |
|     if (this.currentToolId !== "webconsole") {
 | |
|       return this.splitConsole
 | |
|         ? this.closeSplitConsole()
 | |
|         : this.openSplitConsole();
 | |
|     }
 | |
| 
 | |
|     return Promise.resolve();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Toggles the options panel.
 | |
|    * If the option panel is already selected then select the last selected panel.
 | |
|    */
 | |
|   toggleOptions(event) {
 | |
|     // Flip back to the last used panel if we are already
 | |
|     // on the options panel.
 | |
|     if (
 | |
|       this.currentToolId === "options" &&
 | |
|       gDevTools.getToolDefinition(this.lastUsedToolId)
 | |
|     ) {
 | |
|       this.selectTool(this.lastUsedToolId, "toggle_settings_off");
 | |
|     } else {
 | |
|       this.selectTool("options", "toggle_settings_on");
 | |
|     }
 | |
| 
 | |
|     // preventDefault will avoid a Linux only bug when the focus is on a text input
 | |
|     // See Bug 1519087.
 | |
|     event.preventDefault();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Loads the tool next to the currently selected tool.
 | |
|    */
 | |
|   selectNextTool() {
 | |
|     const definitions = this.component.panelDefinitions;
 | |
|     const index = definitions.findIndex(({ id }) => id === this.currentToolId);
 | |
|     const definition =
 | |
|       index === -1 || index >= definitions.length - 1
 | |
|         ? definitions[0]
 | |
|         : definitions[index + 1];
 | |
|     return this.selectTool(definition.id, "select_next_key");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Loads the tool just left to the currently selected tool.
 | |
|    */
 | |
|   selectPreviousTool() {
 | |
|     const definitions = this.component.panelDefinitions;
 | |
|     const index = definitions.findIndex(({ id }) => id === this.currentToolId);
 | |
|     const definition =
 | |
|       index === -1 || index < 1
 | |
|         ? definitions[definitions.length - 1]
 | |
|         : definitions[index - 1];
 | |
|     return this.selectTool(definition.id, "select_prev_key");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Tells if the given tool is currently highlighted.
 | |
|    * (doesn't mean selected, its tab header will be green)
 | |
|    *
 | |
|    * @param {string} id
 | |
|    *        The id of the tool to check.
 | |
|    */
 | |
|   isHighlighted(id) {
 | |
|     return this.component.state.highlightedTools.has(id);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Highlights the tool's tab if it is not the currently selected tool.
 | |
|    *
 | |
|    * @param {string} id
 | |
|    *        The id of the tool to highlight
 | |
|    */
 | |
|   async highlightTool(id) {
 | |
|     if (!this.component) {
 | |
|       await this.isOpen;
 | |
|     }
 | |
|     this.component.highlightTool(id);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * De-highlights the tool's tab.
 | |
|    *
 | |
|    * @param {string} id
 | |
|    *        The id of the tool to unhighlight
 | |
|    */
 | |
|   async unhighlightTool(id) {
 | |
|     if (!this.component) {
 | |
|       await this.isOpen;
 | |
|     }
 | |
|     this.component.unhighlightTool(id);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Raise the toolbox host.
 | |
|    */
 | |
|   raise() {
 | |
|     this.postMessage({ name: "raise-host" });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Fired when user just started navigating away to another web page.
 | |
|    */
 | |
|   async _onWillNavigate({ isFrameSwitching } = {}) {
 | |
|     // On navigate, the server will resume all paused threads, but due to an
 | |
|     // issue which can cause loosing outgoing messages/RDP packets, the THREAD_STATE
 | |
|     // resources for the resumed state might not get received. So let assume it happens
 | |
|     // make use the UI is the appropriate state.
 | |
|     if (this._pausedTargets > 0) {
 | |
|       this.emit("toolbox-resumed");
 | |
|       this._pausedTargets = 0;
 | |
|       if (this.isHighlighted("jsdebugger")) {
 | |
|         this.unhighlightTool("jsdebugger");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Clearing the error count and the iframe list as soon as we navigate
 | |
|     this.setErrorCount(0);
 | |
|     if (!isFrameSwitching) {
 | |
|       this._updateFrames({ destroyAll: true });
 | |
|     }
 | |
|     this.updateToolboxButtons();
 | |
|     const toolId = this.currentToolId;
 | |
|     // For now, only inspector, webconsole, netmonitor and accessibility fire "reloaded" event
 | |
|     if (
 | |
|       toolId != "inspector" &&
 | |
|       toolId != "webconsole" &&
 | |
|       toolId != "netmonitor" &&
 | |
|       toolId != "accessibility"
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const start = this.win.performance.now();
 | |
|     const panel = this.getPanel(toolId);
 | |
|     // Ignore the timing if the panel is still loading
 | |
|     if (!panel) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     await panel.once("reloaded");
 | |
|     // The toolbox may have been destroyed while the panel was reloading
 | |
|     if (this.isDestroying()) {
 | |
|       return;
 | |
|     }
 | |
|     const delay = this.win.performance.now() - start;
 | |
| 
 | |
|     const telemetryKey = "DEVTOOLS_TOOLBOX_PAGE_RELOAD_DELAY_MS";
 | |
|     this.telemetry.getKeyedHistogramById(telemetryKey).add(toolId, delay);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Refresh the host's title.
 | |
|    */
 | |
|   _refreshHostTitle() {
 | |
|     let title;
 | |
| 
 | |
|     if (this.target.isXpcShellTarget) {
 | |
|       // This will only be displayed for local development and can remain
 | |
|       // hardcoded in english.
 | |
|       title = "XPCShell Toolbox";
 | |
|     } else if (this.isMultiProcessBrowserToolbox) {
 | |
|       const scope = Services.prefs.getCharPref(BROWSERTOOLBOX_SCOPE_PREF);
 | |
|       if (scope == BROWSERTOOLBOX_SCOPE_EVERYTHING) {
 | |
|         title = L10N.getStr("toolbox.multiProcessBrowserToolboxTitle");
 | |
|       } else if (scope == BROWSERTOOLBOX_SCOPE_PARENTPROCESS) {
 | |
|         title = L10N.getStr("toolbox.parentProcessBrowserToolboxTitle");
 | |
|       } else {
 | |
|         throw new Error("Unsupported scope: " + scope);
 | |
|       }
 | |
|     } else if (this.target.name && this.target.name != this.target.url) {
 | |
|       const url = this.target.isWebExtension
 | |
|         ? this.target.getExtensionPathName(this.target.url)
 | |
|         : getUnicodeUrl(this.target.url);
 | |
|       title = L10N.getFormatStr(
 | |
|         "toolbox.titleTemplate2",
 | |
|         this.target.name,
 | |
|         url
 | |
|       );
 | |
|     } else {
 | |
|       title = L10N.getFormatStr(
 | |
|         "toolbox.titleTemplate1",
 | |
|         getUnicodeUrl(this.target.url)
 | |
|       );
 | |
|     }
 | |
|     this.postMessage({
 | |
|       name: "set-host-title",
 | |
|       title,
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns an instance of the preference actor. This is a lazily initialized root
 | |
|    * actor that persists preferences to the debuggee, instead of just to the DevTools
 | |
|    * client. See the definition of the preference actor for more information.
 | |
|    */
 | |
|   get preferenceFront() {
 | |
|     if (!this._preferenceFrontRequest) {
 | |
|       // Set the _preferenceFrontRequest property to allow the resetPreference toolbox
 | |
|       // method to cleanup the preference set when the toolbox is closed.
 | |
|       this._preferenceFrontRequest = this.commands.client.mainRoot.getFront(
 | |
|         "preference"
 | |
|       );
 | |
|     }
 | |
|     return this._preferenceFrontRequest;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * See: https://firefox-source-docs.mozilla.org/l10n/fluent/tutorial.html#manually-testing-ui-with-pseudolocalization
 | |
|    *
 | |
|    * @param {"bidi" | "accented" | "none"} pseudoLocale
 | |
|    */
 | |
|   async changePseudoLocale(pseudoLocale) {
 | |
|     await this.isOpen;
 | |
|     const prefFront = await this.preferenceFront;
 | |
|     if (pseudoLocale === "none") {
 | |
|       await prefFront.clearUserPref(PSEUDO_LOCALE_PREF);
 | |
|     } else {
 | |
|       await prefFront.setCharPref(PSEUDO_LOCALE_PREF, pseudoLocale);
 | |
|     }
 | |
|     this.component.setPseudoLocale(pseudoLocale);
 | |
|     this._pseudoLocaleChanged = true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns the pseudo-locale when the target is browser chrome, otherwise undefined.
 | |
|    *
 | |
|    * @returns {"bidi" | "accented" | "none" | undefined}
 | |
|    */
 | |
|   async getPseudoLocale() {
 | |
|     if (!this.isBrowserToolbox) {
 | |
|       return undefined;
 | |
|     }
 | |
| 
 | |
|     const prefFront = await this.preferenceFront;
 | |
|     const locale = await prefFront.getCharPref(PSEUDO_LOCALE_PREF);
 | |
| 
 | |
|     switch (locale) {
 | |
|       case "bidi":
 | |
|       case "accented":
 | |
|         return locale;
 | |
|       default:
 | |
|         return "none";
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   async toggleNoAutohide() {
 | |
|     const front = await this.preferenceFront;
 | |
| 
 | |
|     const toggledValue = !(await this._isDisableAutohideEnabled());
 | |
| 
 | |
|     front.setBoolPref(DISABLE_AUTOHIDE_PREF, toggledValue);
 | |
| 
 | |
|     if (
 | |
|       this.isBrowserToolbox ||
 | |
|       this.descriptorFront.isWebExtensionDescriptor
 | |
|     ) {
 | |
|       this.component.setDisableAutohide(toggledValue);
 | |
|     }
 | |
|     this._autohideHasBeenToggled = true;
 | |
|   },
 | |
| 
 | |
|   async _isDisableAutohideEnabled() {
 | |
|     if (
 | |
|       !this.isBrowserToolbox &&
 | |
|       !this.descriptorFront.isWebExtensionDescriptor
 | |
|     ) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     const prefFront = await this.preferenceFront;
 | |
|     return prefFront.getBoolPref(DISABLE_AUTOHIDE_PREF);
 | |
|   },
 | |
| 
 | |
|   async _listFrames(event) {
 | |
|     if (
 | |
|       !this.target.getTrait("frames") ||
 | |
|       this.target.targetForm.ignoreSubFrames
 | |
|     ) {
 | |
|       // We are not targetting a regular WindowGlobalTargetActor (it can be either an
 | |
|       // addon or browser toolbox actor), or EFT is enabled.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       const { frames } = await this.target.listFrames();
 | |
|       this._updateFrames({ frames });
 | |
|     } catch (e) {
 | |
|       console.error("Error while listing frames", e);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called by the iframe picker when the user selected a frame.
 | |
|    *
 | |
|    * @param {String} frameIdOrTargetActorId
 | |
|    */
 | |
|   onIframePickerFrameSelected(frameIdOrTargetActorId) {
 | |
|     if (!this.frameMap.has(frameIdOrTargetActorId)) {
 | |
|       console.error(
 | |
|         `Can't focus on frame "${frameIdOrTargetActorId}", it is not a known frame`
 | |
|       );
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const frameInfo = this.frameMap.get(frameIdOrTargetActorId);
 | |
|     // If there is no targetFront in the frameData, this means EFT is not enabled.
 | |
|     // Send packet to the backend to select specified frame and  wait for 'frameUpdate'
 | |
|     // event packet to update the UI.
 | |
|     if (!frameInfo.targetFront) {
 | |
|       this.target.switchToFrame({ windowId: frameIdOrTargetActorId });
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Here, EFT is enabled, so we want to focus the toolbox on the specific targetFront
 | |
|     // that was selected by the user. This will trigger this._onTargetSelected which will
 | |
|     // take care of updating the iframe picker state.
 | |
|     this.commands.targetCommand.selectTarget(frameInfo.targetFront);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Highlight a frame in the page
 | |
|    *
 | |
|    * @param {String} frameIdOrTargetActorId
 | |
|    */
 | |
|   async onHighlightFrame(frameIdOrTargetActorId) {
 | |
|     // Only enable frame highlighting when the top level document is targeted
 | |
|     if (!this.rootFrameSelected) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     const frameInfo = this.frameMap.get(frameIdOrTargetActorId);
 | |
|     if (!frameInfo) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     let nodeFront;
 | |
|     if (frameInfo.targetFront) {
 | |
|       const inspectorFront = await frameInfo.targetFront.getFront("inspector");
 | |
|       nodeFront = await inspectorFront.walker.documentElement();
 | |
|     } else {
 | |
|       const inspectorFront = await this.target.getFront("inspector");
 | |
|       nodeFront = await inspectorFront.walker.getNodeActorFromWindowID(
 | |
|         frameIdOrTargetActorId
 | |
|       );
 | |
|     }
 | |
|     const highlighter = this.getHighlighter();
 | |
|     return highlighter.highlight(nodeFront);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Handles changes in document frames.
 | |
|    *
 | |
|    * @param {Object} data
 | |
|    * @param {Boolean} data.destroyAll: All frames have been destroyed.
 | |
|    * @param {Number} data.selected: A frame has been selected
 | |
|    * @param {Object} data.frameData: Some frame data were updated
 | |
|    * @param {String} data.frameData.url: new frame URL (it might have been blank or about:blank)
 | |
|    * @param {String} data.frameData.title: new frame title
 | |
|    * @param {Number|String} data.frameData.id: frame ID / targetFront actorID when EFT is enabled.
 | |
|    * @param {Array<Object>} data.frames: List of frames. Every frame can have:
 | |
|    * @param {Number|String} data.frames[].id: frame ID / targetFront actorID when EFT is enabled.
 | |
|    * @param {String} data.frames[].url: frame URL
 | |
|    * @param {String} data.frames[].title: frame title
 | |
|    * @param {Boolean} data.frames[].destroy: Set to true if destroyed
 | |
|    * @param {Boolean} data.frames[].isTopLevel: true for top level window
 | |
|    */
 | |
|   _updateFrames(data) {
 | |
|     // At the moment, frames `id` can either be outerWindowID (a Number),
 | |
|     // or a targetActorID (a String).
 | |
|     // In order to have the same type of data as a key of `frameMap`, we transform any
 | |
|     // outerWindowID into a string.
 | |
|     // This can be removed once EFT is enabled by default
 | |
|     if (data.selected) {
 | |
|       data.selected = data.selected.toString();
 | |
|     } else if (data.frameData) {
 | |
|       data.frameData.id = data.frameData.id.toString();
 | |
|     } else if (data.frames) {
 | |
|       data.frames.forEach(frame => {
 | |
|         if (frame.id) {
 | |
|           frame.id = frame.id.toString();
 | |
|         }
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // Store (synchronize) data about all existing frames on the backend
 | |
|     if (data.destroyAll) {
 | |
|       this.frameMap.clear();
 | |
|       this.selectedFrameId = null;
 | |
|     } else if (data.selected) {
 | |
|       // If we select the top level target, default back to no particular selected document.
 | |
|       if (data.selected == this.target.actorID) {
 | |
|         this.selectedFrameId = null;
 | |
|       } else {
 | |
|         this.selectedFrameId = data.selected;
 | |
|       }
 | |
|     } else if (data.frameData && this.frameMap.has(data.frameData.id)) {
 | |
|       const existingFrameData = this.frameMap.get(data.frameData.id);
 | |
|       if (
 | |
|         existingFrameData.title == data.frameData.title &&
 | |
|         existingFrameData.url == data.frameData.url
 | |
|       ) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this.frameMap.set(data.frameData.id, {
 | |
|         ...existingFrameData,
 | |
|         url: data.frameData.url,
 | |
|         title: data.frameData.title,
 | |
|       });
 | |
|     } else if (data.frames) {
 | |
|       data.frames.forEach(frame => {
 | |
|         if (frame.destroy) {
 | |
|           this.frameMap.delete(frame.id);
 | |
| 
 | |
|           // Reset the currently selected frame if it's destroyed.
 | |
|           if (this.selectedFrameId == frame.id) {
 | |
|             this.selectedFrameId = null;
 | |
|           }
 | |
|         } else {
 | |
|           this.frameMap.set(frame.id, frame);
 | |
|         }
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // If there is no selected frame select the first top level
 | |
|     // frame by default. Note that there might be more top level
 | |
|     // frames in case of the BrowserToolbox.
 | |
|     if (!this.selectedFrameId) {
 | |
|       const frames = [...this.frameMap.values()];
 | |
|       const topFrames = frames.filter(frame => frame.isTopLevel);
 | |
|       this.selectedFrameId = topFrames.length ? topFrames[0].id : null;
 | |
|     }
 | |
| 
 | |
|     // Debounce the update to avoid unnecessary flickering/rendering.
 | |
|     if (!this.debouncedToolbarUpdate) {
 | |
|       this.debouncedToolbarUpdate = debounce(
 | |
|         () => {
 | |
|           // Toolbox may have been destroyed in the meantime
 | |
|           if (this.component) {
 | |
|             this.component.setToolboxButtons(this.toolbarButtons);
 | |
|           }
 | |
|           this.debouncedToolbarUpdate = null;
 | |
|         },
 | |
|         200,
 | |
|         this
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     const updateUiElements = () => {
 | |
|       // We may need to hide/show the frames button now.
 | |
|       this.updateFrameButton();
 | |
| 
 | |
|       if (this.debouncedToolbarUpdate) {
 | |
|         this.debouncedToolbarUpdate();
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     // This may have been called before the toolbox is ready (= the dom elements for
 | |
|     // the iframe picker don't exist yet).
 | |
|     if (!this.isReady) {
 | |
|       this.once("ready").then(() => updateUiElements);
 | |
|     } else {
 | |
|       updateUiElements();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns whether a root frame (with no parent frame) is selected.
 | |
|    */
 | |
|   get rootFrameSelected() {
 | |
|     // If the frame switcher is disabled, we won't have a selected frame ID.
 | |
|     // In this case, we're always showing the root frame.
 | |
|     if (!this.selectedFrameId) {
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     return this.frameMap.get(this.selectedFrameId).isTopLevel;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Switch to the last used host for the toolbox UI.
 | |
|    */
 | |
|   switchToPreviousHost() {
 | |
|     return this.switchHost("previous");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window,
 | |
|    * and focus the window when done.
 | |
|    *
 | |
|    * @param {string} hostType
 | |
|    *        The host type of the new host object
 | |
|    */
 | |
|   switchHost(hostType) {
 | |
|     if (hostType == this.hostType || !this.descriptorFront.isLocalTab) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     // chromeEventHandler will change after swapping hosts, remove events relying on it.
 | |
|     this._removeChromeEventHandlerEvents();
 | |
| 
 | |
|     this.emit("host-will-change", hostType);
 | |
| 
 | |
|     // ToolboxHostManager is going to call swapFrameLoaders which mess up with
 | |
|     // focus. We have to blur before calling it in order to be able to restore
 | |
|     // the focus after, in _onSwitchedHost.
 | |
|     this.focusTool(this.currentToolId, false);
 | |
| 
 | |
|     // Host code on the chrome side will send back a message once the host
 | |
|     // switched
 | |
|     this.postMessage({
 | |
|       name: "switch-host",
 | |
|       hostType,
 | |
|     });
 | |
| 
 | |
|     return this.once("host-changed");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Request to Firefox UI to move the toolbox to another tab.
 | |
|    * This is used when we move a toolbox to a new popup opened by the tab we were currently debugging.
 | |
|    * We also move the toolbox back to the original tab we were debugging if we select it via Firefox tabs.
 | |
|    *
 | |
|    * @param {String} tabBrowsingContextID
 | |
|    *        The BrowsingContext ID of the tab we want to move to.
 | |
|    * @returns {Promise<undefined>}
 | |
|    *        This will resolve only once we moved to the new tab.
 | |
|    */
 | |
|   switchHostToTab(tabBrowsingContextID) {
 | |
|     this.postMessage({
 | |
|       name: "switch-host-to-tab",
 | |
|       tabBrowsingContextID,
 | |
|     });
 | |
| 
 | |
|     return this.once("switched-host-to-tab");
 | |
|   },
 | |
| 
 | |
|   _onSwitchedHost({ hostType }) {
 | |
|     this._hostType = hostType;
 | |
| 
 | |
|     this._buildDockOptions();
 | |
| 
 | |
|     // chromeEventHandler changed after swapping hosts, add again events relying on it.
 | |
|     this._addChromeEventHandlerEvents();
 | |
| 
 | |
|     // We blurred the tools at start of switchHost, but also when clicking on
 | |
|     // host switching button. We now have to restore the focus.
 | |
|     this.focusTool(this.currentToolId, true);
 | |
| 
 | |
|     this.emit("host-changed");
 | |
|     this.telemetry
 | |
|       .getHistogramById(HOST_HISTOGRAM)
 | |
|       .add(this._getTelemetryHostId());
 | |
| 
 | |
|     this.component.setCurrentHostType(hostType);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Event handler fired when the toolbox was moved to another tab.
 | |
|    * This fires when the toolbox itself requests to be moved to another tab,
 | |
|    * but also when we select the original tab where the toolbox originally was.
 | |
|    *
 | |
|    * @param {String} browsingContextID
 | |
|    *        The BrowsingContext ID of the tab the toolbox has been moved to.
 | |
|    */
 | |
|   _onSwitchedHostToTab(browsingContextID) {
 | |
|     const targets = this.commands.targetCommand.getAllTargets([
 | |
|       this.commands.targetCommand.TYPES.FRAME,
 | |
|     ]);
 | |
|     const target = targets.find(
 | |
|       target => target.browsingContextID == browsingContextID
 | |
|     );
 | |
| 
 | |
|     this.commands.targetCommand.selectTarget(target);
 | |
| 
 | |
|     this.emit("switched-host-to-tab");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Test the availability of a tool (both globally registered tools and
 | |
|    * additional tools registered to this toolbox) by tool id.
 | |
|    *
 | |
|    * @param  {string} toolId
 | |
|    *         Id of the tool definition to search in the per-toolbox or globally
 | |
|    *         registered tools.
 | |
|    *
 | |
|    * @returns {bool}
 | |
|    *         Returns true if the tool is registered globally or on this toolbox.
 | |
|    */
 | |
|   isToolRegistered(toolId) {
 | |
|     return !!this.getToolDefinition(toolId);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Return the tool definition registered globally or additional tools registered
 | |
|    * to this toolbox.
 | |
|    *
 | |
|    * @param  {string} toolId
 | |
|    *         Id of the tool definition to retrieve for the per-toolbox and globally
 | |
|    *         registered tools.
 | |
|    *
 | |
|    * @returns {object}
 | |
|    *         The plain javascript object that represents the requested tool definition.
 | |
|    */
 | |
|   getToolDefinition(toolId) {
 | |
|     return (
 | |
|       gDevTools.getToolDefinition(toolId) ||
 | |
|       this.additionalToolDefinitions.get(toolId)
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Internal helper that removes a loaded tool from the toolbox,
 | |
|    * it removes a loaded tool panel and tab from the toolbox without removing
 | |
|    * its definition, so that it can still be listed in options and re-added later.
 | |
|    *
 | |
|    * @param  {string} toolId
 | |
|    *         Id of the tool to be removed.
 | |
|    */
 | |
|   unloadTool(toolId) {
 | |
|     if (typeof toolId != "string") {
 | |
|       throw new Error("Unexpected non-string toolId received.");
 | |
|     }
 | |
| 
 | |
|     if (this._toolPanels.has(toolId)) {
 | |
|       const instance = this._toolPanels.get(toolId);
 | |
|       instance.destroy();
 | |
|       this._toolPanels.delete(toolId);
 | |
|     }
 | |
| 
 | |
|     const panel = this.doc.getElementById("toolbox-panel-" + toolId);
 | |
| 
 | |
|     // Select another tool.
 | |
|     if (this.currentToolId == toolId) {
 | |
|       const index = this.panelDefinitions.findIndex(({ id }) => id === toolId);
 | |
|       const nextTool = this.panelDefinitions[index + 1];
 | |
|       const previousTool = this.panelDefinitions[index - 1];
 | |
|       let toolNameToSelect;
 | |
| 
 | |
|       if (nextTool) {
 | |
|         toolNameToSelect = nextTool.id;
 | |
|       }
 | |
|       if (previousTool) {
 | |
|         toolNameToSelect = previousTool.id;
 | |
|       }
 | |
|       if (toolNameToSelect) {
 | |
|         this.selectTool(toolNameToSelect, "tool_unloaded");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Remove this tool from the current panel definitions.
 | |
|     this.panelDefinitions = this.panelDefinitions.filter(
 | |
|       ({ id }) => id !== toolId
 | |
|     );
 | |
|     this.visibleAdditionalTools = this.visibleAdditionalTools.filter(
 | |
|       id => id !== toolId
 | |
|     );
 | |
|     this._combineAndSortPanelDefinitions();
 | |
| 
 | |
|     if (panel) {
 | |
|       panel.remove();
 | |
|     }
 | |
| 
 | |
|     if (this.hostType == Toolbox.HostType.WINDOW) {
 | |
|       const doc = this.win.parent.document;
 | |
|       const key = doc.getElementById("key_" + toolId);
 | |
|       if (key) {
 | |
|         key.remove();
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Handler for the tool-registered event.
 | |
|    * @param  {string} toolId
 | |
|    *         Id of the tool that was registered
 | |
|    */
 | |
|   _toolRegistered(toolId) {
 | |
|     // Tools can either be in the global devtools, or added to this specific toolbox
 | |
|     // as an additional tool.
 | |
|     let definition = gDevTools.getToolDefinition(toolId);
 | |
|     let isAdditionalTool = false;
 | |
|     if (!definition) {
 | |
|       definition = this.additionalToolDefinitions.get(toolId);
 | |
|       isAdditionalTool = true;
 | |
|     }
 | |
| 
 | |
|     if (definition.isToolSupported(this)) {
 | |
|       if (isAdditionalTool) {
 | |
|         this.visibleAdditionalTools = [...this.visibleAdditionalTools, toolId];
 | |
|         this._combineAndSortPanelDefinitions();
 | |
|       } else {
 | |
|         this.panelDefinitions = this.panelDefinitions.concat(definition);
 | |
|       }
 | |
|       this._buildPanelForTool(definition);
 | |
| 
 | |
|       // Emit the event so tools can listen to it from the toolbox level
 | |
|       // instead of gDevTools.
 | |
|       this.emit("tool-registered", toolId);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Handler for the tool-unregistered event.
 | |
|    * @param  {string} toolId
 | |
|    *         id of the tool that was unregistered
 | |
|    */
 | |
|   _toolUnregistered(toolId) {
 | |
|     this.unloadTool(toolId);
 | |
| 
 | |
|     // Emit the event so tools can listen to it from the toolbox level
 | |
|     // instead of gDevTools
 | |
|     this.emit("tool-unregistered", toolId);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * A helper function that returns an object containing methods to show and hide the
 | |
|    * Box Model Highlighter on a given NodeFront or node grip (object with metadata which
 | |
|    * can be used to obtain a NodeFront for a node), as well as helpers to listen to the
 | |
|    * higligher show and hide events. The event helpers are used in tests where it is
 | |
|    * cumbersome to load the Inspector panel in order to listen to highlighter events.
 | |
|    *
 | |
|    * @returns {Object} an object of the following shape:
 | |
|    *   - {AsyncFunction} highlight: A function that will show a Box Model Highlighter
 | |
|    *                     for the provided NodeFront or node grip.
 | |
|    *   - {AsyncFunction} unhighlight: A function that will hide any Box Model Highlighter
 | |
|    *                     that is visible. If the `highlight` promise isn't settled yet,
 | |
|    *                     it will wait until it's done and then unhighlight to prevent
 | |
|    *                     zombie highlighters.
 | |
|    *   - {AsyncFunction} waitForHighlighterShown: Returns a promise which resolves with
 | |
|    *                     the "highlighter-shown" event data once the highlighter is shown.
 | |
|    *   - {AsyncFunction} waitForHighlighterHidden: Returns a promise which resolves with
 | |
|    *                     the "highlighter-hidden" event data once the highlighter is
 | |
|    *                     hidden.
 | |
|    *
 | |
|    */
 | |
|   getHighlighter() {
 | |
|     let pendingHighlight;
 | |
| 
 | |
|     /**
 | |
|      * Return a promise wich resolves with a reference to the Inspector panel.
 | |
|      */
 | |
|     const _getInspector = async () => {
 | |
|       const inspector = this.getPanel("inspector");
 | |
|       if (inspector) {
 | |
|         return inspector;
 | |
|       }
 | |
| 
 | |
|       return this.loadTool("inspector");
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * Returns a promise which resolves when a Box Model Highlighter emits the given event
 | |
|      *
 | |
|      * @param  {String} eventName
 | |
|      *         Name of the event to listen to.
 | |
|      * @return {Promise}
 | |
|      *         Promise which resolves when the highlighter event occurs.
 | |
|      *         Resolves with the data payload attached to the event.
 | |
|      */
 | |
|     async function _waitForHighlighterEvent(eventName) {
 | |
|       const inspector = await _getInspector();
 | |
|       return new Promise(resolve => {
 | |
|         function _handler(data) {
 | |
|           if (data.type === inspector.highlighters.TYPES.BOXMODEL) {
 | |
|             inspector.highlighters.off(eventName, _handler);
 | |
|             resolve(data);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         inspector.highlighters.on(eventName, _handler);
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       // highlight might be triggered right before a test finishes. Wrap it
 | |
|       // with safeAsyncMethod to avoid intermittents.
 | |
|       highlight: this._safeAsyncAfterDestroy(async (object, options) => {
 | |
|         pendingHighlight = (async () => {
 | |
|           let nodeFront = object;
 | |
| 
 | |
|           if (!(nodeFront instanceof NodeFront)) {
 | |
|             const inspectorFront = await this.target.getFront("inspector");
 | |
|             nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(object);
 | |
|           }
 | |
| 
 | |
|           if (!nodeFront) {
 | |
|             return null;
 | |
|           }
 | |
| 
 | |
|           const inspector = await _getInspector();
 | |
|           return inspector.highlighters.showHighlighterTypeForNode(
 | |
|             inspector.highlighters.TYPES.BOXMODEL,
 | |
|             nodeFront,
 | |
|             options
 | |
|           );
 | |
|         })();
 | |
|         return pendingHighlight;
 | |
|       }),
 | |
|       unhighlight: this._safeAsyncAfterDestroy(async () => {
 | |
|         if (pendingHighlight) {
 | |
|           await pendingHighlight;
 | |
|           pendingHighlight = null;
 | |
|         }
 | |
| 
 | |
|         const inspector = await _getInspector();
 | |
|         return inspector.highlighters.hideHighlighterType(
 | |
|           inspector.highlighters.TYPES.BOXMODEL
 | |
|         );
 | |
|       }),
 | |
| 
 | |
|       waitForHighlighterShown: this._safeAsyncAfterDestroy(async () => {
 | |
|         return _waitForHighlighterEvent("highlighter-shown");
 | |
|       }),
 | |
| 
 | |
|       waitForHighlighterHidden: this._safeAsyncAfterDestroy(async () => {
 | |
|         return _waitForHighlighterEvent("highlighter-hidden");
 | |
|       }),
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Shortcut to avoid throwing errors when an async method fails after toolbox
 | |
|    * destroy. Should be used with methods that might be triggered by a user
 | |
|    * input, regardless of the toolbox lifecycle.
 | |
|    */
 | |
|   _safeAsyncAfterDestroy(fn) {
 | |
|     return safeAsyncMethod(fn, () => !!this._destroyer);
 | |
|   },
 | |
| 
 | |
|   async _onNewSelectedNodeFront() {
 | |
|     // Emit a "selection-changed" event when the toolbox.selection has been set
 | |
|     // to a new node (or cleared). Currently used in the WebExtensions APIs (to
 | |
|     // provide the `devtools.panels.elements.onSelectionChanged` event).
 | |
|     this.emit("selection-changed");
 | |
| 
 | |
|     const targetFrontActorID = this.selection?.nodeFront?.targetFront?.actorID;
 | |
|     if (targetFrontActorID) {
 | |
|       this.selectTarget(targetFrontActorID);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onToolSelected() {
 | |
|     this._refreshHostTitle();
 | |
| 
 | |
|     this.updatePickerButton();
 | |
|     this.updateFrameButton();
 | |
|     this.updateErrorCountButton();
 | |
| 
 | |
|     // Calling setToolboxButtons in case the visibility of a button changed.
 | |
|     this.component.setToolboxButtons(this.toolbarButtons);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Listener for "inspectObject" event on console top level target actor.
 | |
|    */
 | |
|   _onInspectObject(packet) {
 | |
|     this.inspectObjectActor(packet.objectActor, packet.inspectFromAnnotation);
 | |
|   },
 | |
| 
 | |
|   async inspectObjectActor(objectActor, inspectFromAnnotation) {
 | |
|     const objectGrip = objectActor?.getGrip
 | |
|       ? objectActor.getGrip()
 | |
|       : objectActor;
 | |
| 
 | |
|     if (
 | |
|       objectGrip.preview &&
 | |
|       objectGrip.preview.nodeType === domNodeConstants.ELEMENT_NODE
 | |
|     ) {
 | |
|       await this.viewElementInInspector(objectGrip, inspectFromAnnotation);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (objectGrip.class == "Function") {
 | |
|       if (!objectGrip.location) {
 | |
|         console.error("Missing location in Function objectGrip", objectGrip);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       const { url, line, column } = objectGrip.location;
 | |
|       await this.viewSourceInDebugger(url, line, column);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (objectGrip.type !== "null" && objectGrip.type !== "undefined") {
 | |
|       // Open then split console and inspect the object in the variables view,
 | |
|       // when the objectActor doesn't represent an undefined or null value.
 | |
|       if (this.currentToolId != "webconsole") {
 | |
|         await this.openSplitConsole();
 | |
|       }
 | |
| 
 | |
|       const panel = this.getPanel("webconsole");
 | |
|       panel.hud.ui.inspectObjectActor(objectActor);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the toolbox's notification component
 | |
|    *
 | |
|    * @return The notification box component.
 | |
|    */
 | |
|   getNotificationBox() {
 | |
|     return this.notificationBox;
 | |
|   },
 | |
| 
 | |
|   async closeToolbox() {
 | |
|     await this.destroy();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Public API to check is the current toolbox is currently being destroyed.
 | |
|    */
 | |
|   isDestroying() {
 | |
|     return this._destroyer;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Remove all UI elements, detach from target and clear up
 | |
|    */
 | |
|   destroy() {
 | |
|     // If several things call destroy then we give them all the same
 | |
|     // destruction promise so we're sure to destroy only once
 | |
|     if (this._destroyer) {
 | |
|       return this._destroyer;
 | |
|     }
 | |
| 
 | |
|     // This pattern allows to immediately return the destroyer promise.
 | |
|     // See Bug 1602727 for more details.
 | |
|     let destroyerResolve;
 | |
|     this._destroyer = new Promise(r => (destroyerResolve = r));
 | |
|     this._destroyToolbox().then(destroyerResolve);
 | |
| 
 | |
|     return this._destroyer;
 | |
|   },
 | |
| 
 | |
|   async _destroyToolbox() {
 | |
|     this.emit("destroy");
 | |
| 
 | |
|     // This flag will be checked by Fronts in order to decide if they should
 | |
|     // skip their destroy.
 | |
|     this.commands.client.isToolboxDestroy = true;
 | |
| 
 | |
|     this.off("select", this._onToolSelected);
 | |
|     this.off("host-changed", this._refreshHostTitle);
 | |
| 
 | |
|     gDevTools.off("tool-registered", this._toolRegistered);
 | |
|     gDevTools.off("tool-unregistered", this._toolUnregistered);
 | |
| 
 | |
|     Services.prefs.removeObserver(
 | |
|       "devtools.cache.disabled",
 | |
|       this._applyCacheSettings
 | |
|     );
 | |
|     Services.prefs.removeObserver(
 | |
|       "devtools.custom-formatters.enabled",
 | |
|       this._applyCustomFormatterSetting
 | |
|     );
 | |
|     Services.prefs.removeObserver(
 | |
|       "devtools.serviceWorkers.testing.enabled",
 | |
|       this._applyServiceWorkersTestingSettings
 | |
|     );
 | |
|     Services.prefs.removeObserver(
 | |
|       BROWSERTOOLBOX_SCOPE_PREF,
 | |
|       this._refreshHostTitle
 | |
|     );
 | |
| 
 | |
|     // We normally handle toolClosed from selectTool() but in the event of the
 | |
|     // toolbox closing we need to handle it here instead.
 | |
|     this.telemetry.toolClosed(this.currentToolId, this.sessionId, this);
 | |
| 
 | |
|     this._lastFocusedElement = null;
 | |
|     this._pausedTargets = null;
 | |
| 
 | |
|     if (this._sourceMapService) {
 | |
|       this._sourceMapService.stopSourceMapWorker();
 | |
|       this._sourceMapService = null;
 | |
|     }
 | |
| 
 | |
|     if (this._parserService) {
 | |
|       this._parserService.stop();
 | |
|       this._parserService = null;
 | |
|     }
 | |
| 
 | |
|     if (this.webconsolePanel) {
 | |
|       this._saveSplitConsoleHeight();
 | |
|       this.webconsolePanel.removeEventListener(
 | |
|         "resize",
 | |
|         this._saveSplitConsoleHeight
 | |
|       );
 | |
|       this.webconsolePanel = null;
 | |
|     }
 | |
|     if (this._componentMount) {
 | |
|       this._tabBar.removeEventListener(
 | |
|         "keypress",
 | |
|         this._onToolbarArrowKeypress
 | |
|       );
 | |
|       this.ReactDOM.unmountComponentAtNode(this._componentMount);
 | |
|       this.component = null;
 | |
|       this._componentMount = null;
 | |
|       this._tabBar = null;
 | |
|     }
 | |
|     this.destroyHarAutomation();
 | |
| 
 | |
|     for (const [id, panel] of this._toolPanels) {
 | |
|       try {
 | |
|         gDevTools.emit(id + "-destroy", this, panel);
 | |
|         this.emit(id + "-destroy", panel);
 | |
| 
 | |
|         const rv = panel.destroy();
 | |
|         if (rv) {
 | |
|           console.error(
 | |
|             `Panel ${id}'s destroy method returned something whereas it shouldn't (and should be synchronous).`
 | |
|           );
 | |
|         }
 | |
|       } catch (e) {
 | |
|         // We don't want to stop here if any panel fail to close.
 | |
|         console.error("Panel " + id + ":", e);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.browserRequire = null;
 | |
|     this._toolNames = null;
 | |
| 
 | |
|     // Reset preferences set by the toolbox, then remove the preference front.
 | |
|     const onResetPreference = this.resetPreference().then(() => {
 | |
|       this._preferenceFrontRequest = null;
 | |
|     });
 | |
| 
 | |
|     this.commands.targetCommand.unwatchTargets({
 | |
|       types: this.commands.targetCommand.ALL_TYPES,
 | |
|       onAvailable: this._onTargetAvailable,
 | |
|       onSelected: this._onTargetSelected,
 | |
|       onDestroyed: this._onTargetDestroyed,
 | |
|     });
 | |
| 
 | |
|     const watchedResources = [
 | |
|       this.resourceCommand.TYPES.CONSOLE_MESSAGE,
 | |
|       this.resourceCommand.TYPES.ERROR_MESSAGE,
 | |
|       this.resourceCommand.TYPES.DOCUMENT_EVENT,
 | |
|       this.resourceCommand.TYPES.THREAD_STATE,
 | |
|     ];
 | |
| 
 | |
|     if (!this.isBrowserToolbox) {
 | |
|       watchedResources.push(this.resourceCommand.TYPES.NETWORK_EVENT);
 | |
|     }
 | |
| 
 | |
|     this.resourceCommand.unwatchResources(watchedResources, {
 | |
|       onAvailable: this._onResourceAvailable,
 | |
|     });
 | |
| 
 | |
|     // Unregister buttons listeners
 | |
|     this.toolbarButtons.forEach(button => {
 | |
|       if (typeof button.teardown == "function") {
 | |
|         // teardown arguments have already been bound in _createButtonState
 | |
|         button.teardown();
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     // We need to grab a reference to win before this._host is destroyed.
 | |
|     const win = this.win;
 | |
|     const host = this._getTelemetryHostString();
 | |
|     const width = Math.ceil(win.outerWidth / 50) * 50;
 | |
|     const prevPanelName = this.getTelemetryPanelNameOrOther(this.currentToolId);
 | |
| 
 | |
|     this.telemetry.toolClosed("toolbox", this.sessionId, this);
 | |
|     this.telemetry.recordEvent("exit", prevPanelName, null, {
 | |
|       host,
 | |
|       width,
 | |
|       panel_name: this.getTelemetryPanelNameOrOther(this.currentToolId),
 | |
|       next_panel: "none",
 | |
|       reason: "toolbox_close",
 | |
|       session_id: this.sessionId,
 | |
|     });
 | |
|     this.telemetry.recordEvent("close", "tools", null, {
 | |
|       host,
 | |
|       width,
 | |
|       session_id: this.sessionId,
 | |
|     });
 | |
| 
 | |
|     // Wait for the preferences to be reset before destroying the target descriptor (which will destroy the preference front)
 | |
|     const onceDestroyed = new Promise(resolve => {
 | |
|       resolve(
 | |
|         onResetPreference
 | |
|           .catch(console.error)
 | |
|           .then(async () => {
 | |
|             // Destroy the node picker *after* destroying the panel,
 | |
|             // which may still try to access it. (And might spawn a new one)
 | |
|             if (this._nodePicker) {
 | |
|               this._nodePicker.destroy();
 | |
|               this._nodePicker = null;
 | |
|             }
 | |
|             this.selection.destroy();
 | |
|             this.selection = null;
 | |
| 
 | |
|             if (this._netMonitorAPI) {
 | |
|               this._netMonitorAPI.destroy();
 | |
|               this._netMonitorAPI = null;
 | |
|             }
 | |
| 
 | |
|             if (this._sourceMapURLService) {
 | |
|               await this._sourceMapURLService.waitForSourcesLoading();
 | |
|               this._sourceMapURLService.destroy();
 | |
|               this._sourceMapURLService = null;
 | |
|             }
 | |
| 
 | |
|             this._removeWindowListeners();
 | |
|             this._removeChromeEventHandlerEvents();
 | |
| 
 | |
|             this._store = null;
 | |
| 
 | |
|             // Notify toolbox-host-manager that the host can be destroyed.
 | |
|             this.emit("toolbox-unload");
 | |
| 
 | |
|             // targetCommand need to be notified that the toolbox is being torn down.
 | |
|             // This is done after other destruction tasks since it may tear down
 | |
|             // fronts and the debugger transport which earlier destroy methods may
 | |
|             // require to complete.
 | |
|             //
 | |
|             // For similar reasons, only destroy the target-list after every
 | |
|             // other outstanding cleanup is done. Destroying the target list
 | |
|             // will lead to destroy frame targets which can temporarily make
 | |
|             // some fronts unresponsive and block the cleanup.
 | |
|             this.commands.targetCommand.destroy();
 | |
|             return this.descriptorFront.destroy();
 | |
|           }, console.error)
 | |
|           .then(() => {
 | |
|             this.emit("destroyed");
 | |
| 
 | |
|             // Free _host after the call to destroyed in order to let a chance
 | |
|             // to destroyed listeners to still query toolbox attributes
 | |
|             this._host = null;
 | |
|             this._win = null;
 | |
|             this._toolPanels.clear();
 | |
|             this.descriptorFront = null;
 | |
|             this.resourceCommand = null;
 | |
|             this.commands = null;
 | |
| 
 | |
|             // Force GC to prevent long GC pauses when running tests and to free up
 | |
|             // memory in general when the toolbox is closed.
 | |
|             if (flags.testing) {
 | |
|               win.windowUtils.garbageCollect();
 | |
|             }
 | |
|           })
 | |
|           .catch(console.error)
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     const leakCheckObserver = ({ wrappedJSObject: barrier }) => {
 | |
|       // Make the leak detector wait until this toolbox is properly destroyed.
 | |
|       barrier.client.addBlocker(
 | |
|         "DevTools: Wait until toolbox is destroyed",
 | |
|         onceDestroyed
 | |
|       );
 | |
|     };
 | |
| 
 | |
|     const topic = "shutdown-leaks-before-check";
 | |
|     Services.obs.addObserver(leakCheckObserver, topic);
 | |
| 
 | |
|     await onceDestroyed;
 | |
| 
 | |
|     Services.obs.removeObserver(leakCheckObserver, topic);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Open the textbox context menu at given coordinates.
 | |
|    * Panels in the toolbox can call this on contextmenu events with event.screenX/Y
 | |
|    * instead of having to implement their own copy/paste/selectAll menu.
 | |
|    * @param {Number} x
 | |
|    * @param {Number} y
 | |
|    */
 | |
|   openTextBoxContextMenu(x, y) {
 | |
|     const menu = createEditContextMenu(this.topWindow, "toolbox-menu");
 | |
| 
 | |
|     // Fire event for tests
 | |
|     menu.once("open", () => this.emit("menu-open"));
 | |
|     menu.once("close", () => this.emit("menu-close"));
 | |
| 
 | |
|     menu.popup(x, y, this.doc);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    *  Retrieve the current textbox context menu, if available.
 | |
|    */
 | |
|   getTextBoxContextMenu() {
 | |
|     return this.topDoc.getElementById("toolbox-menu");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Reset preferences set by the toolbox.
 | |
|    */
 | |
|   async resetPreference() {
 | |
|     if (
 | |
|       // No preferences have been changed, so there is nothing to reset.
 | |
|       !this._preferenceFrontRequest ||
 | |
|       // Did any pertinent prefs actually change? For autohide and the pseudo-locale,
 | |
|       // only reset prefs in the Browser Toolbox if it's been toggled in the UI
 | |
|       // (don't reset the pref if it was already set before opening)
 | |
|       (!this._autohideHasBeenToggled && !this._pseudoLocaleChanged)
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const preferenceFront = await this.preferenceFront;
 | |
| 
 | |
|     if (this._autohideHasBeenToggled) {
 | |
|       await preferenceFront.clearUserPref(DISABLE_AUTOHIDE_PREF);
 | |
|     }
 | |
|     if (this._pseudoLocaleChanged) {
 | |
|       await preferenceFront.clearUserPref(PSEUDO_LOCALE_PREF);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // HAR Automation
 | |
| 
 | |
|   async initHarAutomation() {
 | |
|     const autoExport = Services.prefs.getBoolPref(
 | |
|       "devtools.netmonitor.har.enableAutoExportToFile"
 | |
|     );
 | |
|     if (autoExport) {
 | |
|       this.harAutomation = new HarAutomation();
 | |
|       await this.harAutomation.initialize(this);
 | |
|     }
 | |
|   },
 | |
|   destroyHarAutomation() {
 | |
|     if (this.harAutomation) {
 | |
|       this.harAutomation.destroy();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns gViewSourceUtils for viewing source.
 | |
|    */
 | |
|   get gViewSourceUtils() {
 | |
|     return this.win.gViewSourceUtils;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Open a CSS file when there is no line or column information available.
 | |
|    *
 | |
|    * @param {string} url The URL of the CSS file to open.
 | |
|    */
 | |
|   async viewGeneratedSourceInStyleEditor(url) {
 | |
|     if (typeof url !== "string") {
 | |
|       console.warn("Failed to open generated source, no url given");
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     // The style editor hides the generated file if the file has original
 | |
|     // sources, so we have no choice but to open whichever original file
 | |
|     // corresponds to the first line of the generated file.
 | |
|     return viewSource.viewSourceInStyleEditor(this, url, 1);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Given a URL for a stylesheet (generated or original), open in the style
 | |
|    * editor if possible. Falls back to plain "view-source:".
 | |
|    * If the stylesheet has a sourcemap, we will attempt to open the original
 | |
|    * version of the file instead of the generated version.
 | |
|    */
 | |
|   async viewSourceInStyleEditorByURL(url, line, column) {
 | |
|     if (typeof url !== "string") {
 | |
|       console.warn("Failed to open source, no url given");
 | |
|       return false;
 | |
|     }
 | |
|     if (typeof line !== "number") {
 | |
|       console.warn(
 | |
|         "No line given when navigating to source. If you're seeing this, there is a bug."
 | |
|       );
 | |
| 
 | |
|       // This is a fallback in case of programming errors, but in a perfect
 | |
|       // world, viewSourceInStyleEditorByURL would always get a line/colum.
 | |
|       line = 1;
 | |
|       column = null;
 | |
|     }
 | |
| 
 | |
|     return viewSource.viewSourceInStyleEditor(this, url, line, column);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens source in style editor. Falls back to plain "view-source:".
 | |
|    * If the stylesheet has a sourcemap, we will attempt to open the original
 | |
|    * version of the file instead of the generated version.
 | |
|    */
 | |
|   async viewSourceInStyleEditorByFront(stylesheetFront, line, column) {
 | |
|     if (!stylesheetFront || typeof stylesheetFront !== "object") {
 | |
|       console.warn("Failed to open source, no stylesheet given");
 | |
|       return false;
 | |
|     }
 | |
|     if (typeof line !== "number") {
 | |
|       console.warn(
 | |
|         "No line given when navigating to source. If you're seeing this, there is a bug."
 | |
|       );
 | |
| 
 | |
|       // This is a fallback in case of programming errors, but in a perfect
 | |
|       // world, viewSourceInStyleEditorByFront would always get a line/colum.
 | |
|       line = 1;
 | |
|       column = null;
 | |
|     }
 | |
| 
 | |
|     return viewSource.viewSourceInStyleEditor(
 | |
|       this,
 | |
|       stylesheetFront,
 | |
|       line,
 | |
|       column
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   async viewElementInInspector(objectGrip, reason) {
 | |
|     // Open the inspector and select the DOM Element.
 | |
|     await this.loadTool("inspector");
 | |
|     const inspector = this.getPanel("inspector");
 | |
|     const nodeFound = await inspector.inspectNodeActor(objectGrip, reason);
 | |
|     if (nodeFound) {
 | |
|       await this.selectTool("inspector", reason);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Open a JS file when there is no line or column information available.
 | |
|    *
 | |
|    * @param {string} url The URL of the JS file to open.
 | |
|    */
 | |
|   async viewGeneratedSourceInDebugger(url) {
 | |
|     if (typeof url !== "string") {
 | |
|       console.warn("Failed to open generated source, no url given");
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     return viewSource.viewSourceInDebugger(this, url, null, null, null, null);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens source in debugger, the sourcemapped location will be selected in
 | |
|    * the debugger panel, if the given location resolves to a know sourcemapped one.
 | |
|    *
 | |
|    * Falls back to plain "view-source:".
 | |
|    *
 | |
|    * @see devtools/client/shared/source-utils.js
 | |
|    */
 | |
|   async viewSourceInDebugger(
 | |
|     sourceURL,
 | |
|     sourceLine,
 | |
|     sourceColumn,
 | |
|     sourceId,
 | |
|     reason
 | |
|   ) {
 | |
|     if (typeof sourceURL !== "string" && typeof sourceId !== "string") {
 | |
|       console.warn("Failed to open generated source, no url/id given");
 | |
|       return false;
 | |
|     }
 | |
|     if (typeof sourceLine !== "number") {
 | |
|       console.warn(
 | |
|         "No line given when navigating to source. If you're seeing this, there is a bug."
 | |
|       );
 | |
| 
 | |
|       // This is a fallback in case of programming errors, but in a perfect
 | |
|       // world, viewSourceInDebugger would always get a line/colum.
 | |
|       sourceLine = 1;
 | |
|       sourceColumn = null;
 | |
|     }
 | |
| 
 | |
|     return viewSource.viewSourceInDebugger(
 | |
|       this,
 | |
|       sourceURL,
 | |
|       sourceLine,
 | |
|       sourceColumn,
 | |
|       sourceId,
 | |
|       reason
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens source in plain "view-source:".
 | |
|    * @see devtools/client/shared/source-utils.js
 | |
|    */
 | |
|   viewSource(sourceURL, sourceLine) {
 | |
|     return viewSource.viewSource(this, sourceURL, sourceLine);
 | |
|   },
 | |
| 
 | |
|   // Support for WebExtensions API (`devtools.network.*`)
 | |
| 
 | |
|   /**
 | |
|    * Return Netmonitor API object. This object offers Network monitor
 | |
|    * public API that can be consumed by other panels or WE API.
 | |
|    */
 | |
|   async getNetMonitorAPI() {
 | |
|     const netPanel = this.getPanel("netmonitor");
 | |
| 
 | |
|     // Return Net panel if it exists.
 | |
|     if (netPanel) {
 | |
|       return netPanel.panelWin.Netmonitor.api;
 | |
|     }
 | |
| 
 | |
|     if (this._netMonitorAPI) {
 | |
|       return this._netMonitorAPI;
 | |
|     }
 | |
| 
 | |
|     // Create and initialize Network monitor API object.
 | |
|     // This object is only connected to the backend - not to the UI.
 | |
|     this._netMonitorAPI = new NetMonitorAPI();
 | |
|     await this._netMonitorAPI.connect(this);
 | |
| 
 | |
|     return this._netMonitorAPI;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns data (HAR) collected by the Network panel.
 | |
|    */
 | |
|   async getHARFromNetMonitor() {
 | |
|     const netMonitor = await this.getNetMonitorAPI();
 | |
|     let har = await netMonitor.getHar();
 | |
| 
 | |
|     // Return default empty HAR file if needed.
 | |
|     har = har || buildHarLog(Services.appinfo);
 | |
| 
 | |
|     // Return the log directly to be compatible with
 | |
|     // Chrome WebExtension API.
 | |
|     return har.log;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Add listener for `onRequestFinished` events.
 | |
|    *
 | |
|    * @param {Object} listener
 | |
|    *        The listener to be called it's expected to be
 | |
|    *        a function that takes ({harEntry, requestId})
 | |
|    *        as first argument.
 | |
|    */
 | |
|   async addRequestFinishedListener(listener) {
 | |
|     const netMonitor = await this.getNetMonitorAPI();
 | |
|     netMonitor.addRequestFinishedListener(listener);
 | |
|   },
 | |
| 
 | |
|   async removeRequestFinishedListener(listener) {
 | |
|     const netMonitor = await this.getNetMonitorAPI();
 | |
|     netMonitor.removeRequestFinishedListener(listener);
 | |
| 
 | |
|     // Destroy Network monitor API object if the following is true:
 | |
|     // 1) there is no listener
 | |
|     // 2) the Net panel doesn't exist/use the API object (if the panel
 | |
|     //    exists it's also responsible for destroying it,
 | |
|     //    see `NetMonitorPanel.open` for more details)
 | |
|     const netPanel = this.getPanel("netmonitor");
 | |
|     const hasListeners = netMonitor.hasRequestFinishedListeners();
 | |
|     if (this._netMonitorAPI && !hasListeners && !netPanel) {
 | |
|       this._netMonitorAPI.destroy();
 | |
|       this._netMonitorAPI = null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Used to lazily fetch HTTP response content within
 | |
|    * `onRequestFinished` event listener.
 | |
|    *
 | |
|    * @param {String} requestId
 | |
|    *        Id of the request for which the response content
 | |
|    *        should be fetched.
 | |
|    */
 | |
|   async fetchResponseContent(requestId) {
 | |
|     const netMonitor = await this.getNetMonitorAPI();
 | |
|     return netMonitor.fetchResponseContent(requestId);
 | |
|   },
 | |
| 
 | |
|   // Support management of installed WebExtensions that provide a devtools_page.
 | |
| 
 | |
|   /**
 | |
|    * List the subset of the active WebExtensions which have a devtools_page (used by
 | |
|    * toolbox-options.js to create the list of the tools provided by the enabled
 | |
|    * WebExtensions).
 | |
|    * @see devtools/client/framework/toolbox-options.js
 | |
|    */
 | |
|   listWebExtensions() {
 | |
|     // Return the array of the enabled webextensions (we can't use the prefs list here,
 | |
|     // because some of them may be disabled by the Addon Manager and still have a devtools
 | |
|     // preference).
 | |
|     return Array.from(this._webExtensions).map(([uuid, { name, pref }]) => {
 | |
|       return { uuid, name, pref };
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Add a WebExtension to the list of the active extensions (given the extension UUID,
 | |
|    * a unique id assigned to an extension when it is installed, and its name),
 | |
|    * and emit a "webextension-registered" event to allow toolbox-options.js
 | |
|    * to refresh the listed tools accordingly.
 | |
|    * @see browser/components/extensions/ext-devtools.js
 | |
|    */
 | |
|   registerWebExtension(extensionUUID, { name, pref }) {
 | |
|     // Ensure that an installed extension (active in the AddonManager) which
 | |
|     // provides a devtools page is going to be listed in the toolbox options
 | |
|     // (and refresh its name if it was already listed).
 | |
|     this._webExtensions.set(extensionUUID, { name, pref });
 | |
|     this.emit("webextension-registered", extensionUUID);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Remove an active WebExtension from the list of the active extensions (given the
 | |
|    * extension UUID, a unique id assigned to an extension when it is installed, and its
 | |
|    * name), and emit a "webextension-unregistered" event to allow toolbox-options.js
 | |
|    * to refresh the listed tools accordingly.
 | |
|    * @see browser/components/extensions/ext-devtools.js
 | |
|    */
 | |
|   unregisterWebExtension(extensionUUID) {
 | |
|     // Ensure that an extension that has been disabled/uninstalled from the AddonManager
 | |
|     // is going to be removed from the toolbox options.
 | |
|     this._webExtensions.delete(extensionUUID);
 | |
|     this.emit("webextension-unregistered", extensionUUID);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * A helper function which returns true if the extension with the given UUID is listed
 | |
|    * as active for the toolbox and has its related devtools about:config preference set
 | |
|    * to true.
 | |
|    * @see browser/components/extensions/ext-devtools.js
 | |
|    */
 | |
|   isWebExtensionEnabled(extensionUUID) {
 | |
|     const extInfo = this._webExtensions.get(extensionUUID);
 | |
|     return extInfo && Services.prefs.getBoolPref(extInfo.pref, false);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns a panel id in the case of built in panels or "other" in the case of
 | |
|    * third party panels. This is necessary due to limitations in addon id strings,
 | |
|    * the permitted length of event telemetry property values and what we actually
 | |
|    * want to see in our telemetry.
 | |
|    *
 | |
|    * @param {String} id
 | |
|    *        The panel id we would like to process.
 | |
|    */
 | |
|   getTelemetryPanelNameOrOther(id) {
 | |
|     if (!this._toolNames) {
 | |
|       const definitions = gDevTools.getToolDefinitionArray();
 | |
|       const definitionIds = definitions.map(definition => definition.id);
 | |
| 
 | |
|       this._toolNames = new Set(definitionIds);
 | |
|     }
 | |
| 
 | |
|     if (!this._toolNames.has(id)) {
 | |
|       return "other";
 | |
|     }
 | |
| 
 | |
|     return id;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets basic information on the DebugTargetInfo component
 | |
|    */
 | |
|   _setDebugTargetData() {
 | |
|     // Note that local WebExtension are debugged via WINDOW host,
 | |
|     // but we still want to display target data.
 | |
|     if (
 | |
|       this.hostType === Toolbox.HostType.PAGE ||
 | |
|       this.descriptorFront.isWebExtensionDescriptor
 | |
|     ) {
 | |
|       // Displays DebugTargetInfo which shows the basic information of debug target,
 | |
|       // if `about:devtools-toolbox` URL opens directly.
 | |
|       // DebugTargetInfo requires this._debugTargetData to be populated
 | |
|       this.component.setDebugTargetData(this._getDebugTargetData());
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onResourceAvailable(resources) {
 | |
|     let errors = this._errorCount || 0;
 | |
| 
 | |
|     const { TYPES } = this.resourceCommand;
 | |
|     for (const resource of resources) {
 | |
|       const { resourceType } = resource;
 | |
|       if (
 | |
|         resourceType === TYPES.ERROR_MESSAGE &&
 | |
|         // ERROR_MESSAGE resources can be warnings/info, but here we only want to count errors
 | |
|         resource.pageError.error
 | |
|       ) {
 | |
|         errors++;
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       if (resourceType === TYPES.CONSOLE_MESSAGE) {
 | |
|         const { level } = resource.message;
 | |
|         if (level === "error" || level === "exception" || level === "assert") {
 | |
|           errors++;
 | |
|         }
 | |
| 
 | |
|         // Reset the count on console.clear
 | |
|         if (level === "clear") {
 | |
|           errors = 0;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // Only consider top level document, and ignore remote iframes top document
 | |
|       if (
 | |
|         resourceType === TYPES.DOCUMENT_EVENT &&
 | |
|         resource.name === "will-navigate" &&
 | |
|         resource.targetFront.isTopLevel
 | |
|       ) {
 | |
|         this._onWillNavigate({
 | |
|           isFrameSwitching: resource.isFrameSwitching,
 | |
|         });
 | |
|         // While we will call `setErrorCount(0)` from onWillNavigate, we also need to reset
 | |
|         // `errors` local variable in order to clear previous errors processed in the same
 | |
|         // throttling bucket as this will-navigate resource.
 | |
|         errors = 0;
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         resourceType === TYPES.DOCUMENT_EVENT &&
 | |
|         !resource.isFrameSwitching &&
 | |
|         // `url` is set on the targetFront when we receive dom-loading, and `title` when
 | |
|         // `dom-interactive` is received. Here we're only updating the window title in
 | |
|         // the "newer" event.
 | |
|         resource.name === "dom-interactive"
 | |
|       ) {
 | |
|         // the targetFront title and url are updated on dom-interactive, so delay refreshing
 | |
|         // the host title a bit in order for the event listener in targetCommand to be
 | |
|         // executed.
 | |
|         setTimeout(() => {
 | |
|           this._updateFrames({
 | |
|             frameData: {
 | |
|               id: resource.targetFront.actorID,
 | |
|               url: resource.targetFront.url,
 | |
|               title: resource.targetFront.title,
 | |
|             },
 | |
|           });
 | |
| 
 | |
|           if (resource.targetFront.isTopLevel) {
 | |
|             this._refreshHostTitle();
 | |
|             this._setDebugTargetData();
 | |
|           }
 | |
|         }, 0);
 | |
|       }
 | |
| 
 | |
|       if (resourceType == TYPES.THREAD_STATE) {
 | |
|         this._onThreadStateChanged(resource);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.setErrorCount(errors);
 | |
|   },
 | |
| 
 | |
|   _onResourceUpdated(resources) {
 | |
|     let errors = this._errorCount || 0;
 | |
| 
 | |
|     for (const { update } of resources) {
 | |
|       // In order to match webconsole behaviour, we treat 4xx and 5xx network calls as errors.
 | |
|       if (
 | |
|         update.resourceType === this.resourceCommand.TYPES.NETWORK_EVENT &&
 | |
|         update.resourceUpdates.status &&
 | |
|         update.resourceUpdates.status.toString().match(REGEX_4XX_5XX)
 | |
|       ) {
 | |
|         errors++;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.setErrorCount(errors);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Set the number of errors in the toolbar icon.
 | |
|    *
 | |
|    * @param {Number} count
 | |
|    */
 | |
|   setErrorCount(count) {
 | |
|     // Don't re-render if the number of errors changed
 | |
|     if (!this.component || this._errorCount === count) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._errorCount = count;
 | |
| 
 | |
|     // Update button properties and trigger a render of the toolbox
 | |
|     this.updateErrorCountButton();
 | |
|     this._throttledSetToolboxButtons();
 | |
|   },
 | |
| };
 | 
