tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

toolbox.js (162638B)


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