/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

{
  // start private scope for gBrowser
  /**
   * A set of known icons to use for internal pages. These are hardcoded so we can
   * start loading them faster than ContentLinkHandler would normally find them.
   */
  const FAVICON_DEFAULTS = {
    "about:newtab": "chrome://branding/content/icon32.png",
    "about:home": "chrome://branding/content/icon32.png",
    "about:welcome": "chrome://branding/content/icon32.png",
    "about:privatebrowsing":
      "chrome://browser/skin/privatebrowsing/favicon.svg",
  };

  const {
    LOAD_FLAGS_NONE,
    LOAD_FLAGS_FROM_EXTERNAL,
    LOAD_FLAGS_FIRST_LOAD,
    LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL,
    LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
    LOAD_FLAGS_FIXUP_SCHEME_TYPOS,
    LOAD_FLAGS_FORCE_ALLOW_DATA_URI,
    LOAD_FLAGS_DISABLE_TRR,
  } = Ci.nsIWebNavigation;

  /**
   * Updates the User Context UI indicators if the browser is in a non-default context
   */
  function updateUserContextUIIndicator() {
    function replaceContainerClass(classType, element, value) {
      let prefix = "identity-" + classType + "-";
      if (value && element.classList.contains(prefix + value)) {
        return;
      }
      for (let className of element.classList) {
        if (className.startsWith(prefix)) {
          element.classList.remove(className);
        }
      }
      if (value) {
        element.classList.add(prefix + value);
      }
    }

    let hbox = document.getElementById("userContext-icons");

    let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid");
    if (!userContextId) {
      replaceContainerClass("color", hbox, "");
      hbox.hidden = true;
      return;
    }

    let identity =
      ContextualIdentityService.getPublicIdentityFromId(userContextId);
    if (!identity) {
      replaceContainerClass("color", hbox, "");
      hbox.hidden = true;
      return;
    }

    replaceContainerClass("color", hbox, identity.color);

    let label = ContextualIdentityService.getUserContextLabel(userContextId);
    document.getElementById("userContext-label").setAttribute("value", label);
    // Also set the container label as the tooltip so we can only show the icon
    // in small windows.
    hbox.setAttribute("tooltiptext", label);

    let indicator = document.getElementById("userContext-indicator");
    replaceContainerClass("icon", indicator, identity.icon);

    hbox.hidden = false;
  }

  window._gBrowser = {
    init() {
      ChromeUtils.defineESModuleGetters(this, {
        AsyncTabSwitcher: "resource:///modules/AsyncTabSwitcher.sys.mjs",
        PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs",
        UrlbarProviderOpenTabs:
          "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
      });
      XPCOMUtils.defineLazyServiceGetters(this, {
        MacSharingService: [
          "@mozilla.org/widget/macsharingservice;1",
          "nsIMacSharingService",
        ],
      });
      XPCOMUtils.defineLazyGetter(this, "tabLocalization", () => {
        return new Localization(
          ["browser/tabbrowser.ftl", "branding/brand.ftl"],
          true
        );
      });

      if (AppConstants.MOZ_CRASHREPORTER) {
        ChromeUtils.defineESModuleGetters(this, {
          TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs",
        });
      }

      Services.obs.addObserver(this, "contextual-identity-updated");

      Services.els.addSystemEventListener(document, "keydown", this, false);
      Services.els.addSystemEventListener(document, "keypress", this, false);
      document.addEventListener("visibilitychange", this);
      window.addEventListener("framefocusrequested", this);

      this.tabContainer.init();
      this._setupInitialBrowserAndTab();

      if (
        Services.prefs.getIntPref("browser.display.document_color_use") == 2
      ) {
        this.tabpanels.style.backgroundColor = Services.prefs.getBoolPref(
          "browser.display.use_system_colors"
        )
          ? "canvas"
          : Services.prefs.getCharPref("browser.display.background_color");
      }

      this._setFindbarData();

      // We take over setting the document title, so remove the l10n id to
      // avoid it being re-translated and overwriting document content if
      // we ever switch languages at runtime. After a language change, the
      // window title will update at the next tab or location change.
      document.querySelector("title").removeAttribute("data-l10n-id");

      this._setupEventListeners();
      this._initialized = true;
    },

    ownerGlobal: window,

    ownerDocument: document,

    closingTabsEnum: {
      ALL: 0,
      OTHER: 1,
      TO_START: 2,
      TO_END: 3,
      MULTI_SELECTED: 4,
    },

    _lastRelatedTabMap: new WeakMap(),

    mProgressListeners: [],

    mTabsProgressListeners: [],

    _tabListeners: new Map(),

    _tabFilters: new Map(),

    _isBusy: false,

    _awaitingToggleCaretBrowsingPrompt: false,

    arrowKeysShouldWrap: AppConstants == "macosx",

    _previewMode: false,

    _lastFindValue: "",

    _contentWaitingCount: 0,

    _tabLayerCache: [],

    tabAnimationsInProgress: 0,

    /**
     * Binding from browser to tab
     */
    _tabForBrowser: new WeakMap(),

    /**
     * `_createLazyBrowser` will define properties on the unbound lazy browser
     * which correspond to properties defined in MozBrowser which will be bound to
     * the browser when it is inserted into the document.  If any of these
     * properties are accessed by consumers, `_insertBrowser` is called and
     * the browser is inserted to ensure that things don't break.  This list
     * provides the names of properties that may be called while the browser
     * is in its unbound (lazy) state.
     */
    _browserBindingProperties: [
      "canGoBack",
      "canGoForward",
      "goBack",
      "goForward",
      "permitUnload",
      "reload",
      "reloadWithFlags",
      "stop",
      "loadURI",
      "fixupAndLoadURIString",
      "gotoIndex",
      "currentURI",
      "documentURI",
      "remoteType",
      "preferences",
      "imageDocument",
      "isRemoteBrowser",
      "messageManager",
      "getTabBrowser",
      "finder",
      "fastFind",
      "sessionHistory",
      "contentTitle",
      "characterSet",
      "fullZoom",
      "textZoom",
      "tabHasCustomZoom",
      "webProgress",
      "addProgressListener",
      "removeProgressListener",
      "audioPlaybackStarted",
      "audioPlaybackStopped",
      "resumeMedia",
      "mute",
      "unmute",
      "blockedPopups",
      "lastURI",
      "purgeSessionHistory",
      "stopScroll",
      "startScroll",
      "userTypedValue",
      "userTypedClear",
      "didStartLoadSinceLastUserTyping",
      "audioMuted",
    ],

    _removingTabs: new Set(),

    _multiSelectedTabsSet: new WeakSet(),

    _lastMultiSelectedTabRef: null,

    _clearMultiSelectionLocked: false,

    _clearMultiSelectionLockedOnce: false,

    _multiSelectChangeStarted: false,

    _multiSelectChangeAdditions: new Set(),

    _multiSelectChangeRemovals: new Set(),

    _multiSelectChangeSelected: false,

    /**
     * Tab close requests are ignored if the window is closing anyway,
     * e.g. when holding Ctrl+W.
     */
    _windowIsClosing: false,

    preloadedBrowser: null,

    /**
     * This defines a proxy which allows us to access browsers by
     * index without actually creating a full array of browsers.
     */
    browsers: new Proxy([], {
      has: (target, name) => {
        if (typeof name == "string" && Number.isInteger(parseInt(name))) {
          return name in gBrowser.tabs;
        }
        return false;
      },
      get: (target, name) => {
        if (name == "length") {
          return gBrowser.tabs.length;
        }
        if (typeof name == "string" && Number.isInteger(parseInt(name))) {
          if (!(name in gBrowser.tabs)) {
            return undefined;
          }
          return gBrowser.tabs[name].linkedBrowser;
        }
        return target[name];
      },
    }),

    /**
     * List of browsers whose docshells must be active in order for print preview
     * to work.
     */
    _printPreviewBrowsers: new Set(),

    _switcher: null,

    _soundPlayingAttrRemovalTimer: 0,

    _hoverTabTimer: null,

    _featureCallout: null,

    _featureCalloutPanelId: null,

    get tabContainer() {
      delete this.tabContainer;
      return (this.tabContainer = document.getElementById("tabbrowser-tabs"));
    },

    get tabs() {
      return this.tabContainer.allTabs;
    },

    get tabbox() {
      delete this.tabbox;
      return (this.tabbox = document.getElementById("tabbrowser-tabbox"));
    },

    get tabpanels() {
      delete this.tabpanels;
      return (this.tabpanels = document.getElementById("tabbrowser-tabpanels"));
    },

    addEventListener(...args) {
      this.tabpanels.addEventListener(...args);
    },

    removeEventListener(...args) {
      this.tabpanels.removeEventListener(...args);
    },

    dispatchEvent(...args) {
      return this.tabpanels.dispatchEvent(...args);
    },

    get visibleTabs() {
      return this.tabContainer._getVisibleTabs();
    },

    get _numPinnedTabs() {
      for (var i = 0; i < this.tabs.length; i++) {
        if (!this.tabs[i].pinned) {
          break;
        }
      }
      return i;
    },

    set selectedTab(val) {
      if (
        gSharedTabWarning.willShowSharedTabWarning(val) ||
        document.documentElement.hasAttribute("window-modal-open") ||
        (gNavToolbox.collapsed && !this._allowTabChange)
      ) {
        return;
      }
      // Update the tab
      this.tabbox.selectedTab = val;
    },

    get selectedTab() {
      return this._selectedTab;
    },

    get selectedBrowser() {
      return this._selectedBrowser;
    },

    get featureCallout() {
      return this._featureCallout;
    },

    set featureCallout(val) {
      this._featureCallout = val;
    },

    get instantiateFeatureCalloutTour() {
      return this._instantiateFeatureCalloutTour;
    },

    get featureCalloutPanelId() {
      return this._featureCalloutPanelId;
    },

    _instantiateFeatureCalloutTour(browser, panelId) {
      this._featureCalloutPanelId = panelId;
      const { FeatureCallout } = ChromeUtils.importESModule(
        "resource:///modules/FeatureCallout.sys.mjs"
      );
      // Note - once we have additional browser chrome messages,
      // only use PDF.js pref value when navigating to PDF viewer
      this._featureCallout = new FeatureCallout({
        win: window,
        browser,
        prefName: "browser.pdfjs.feature-tour",
        page: "chrome",
        theme: { preset: "pdfjs", simulateContent: true },
      });
    },
    _setupInitialBrowserAndTab() {
      // See browser.js for the meaning of window.arguments.
      // Bug 1485961 covers making this more sane.
      let userContextId = window.arguments && window.arguments[5];

      let openWindowInfo = window.docShell.treeOwner
        .QueryInterface(Ci.nsIInterfaceRequestor)
        .getInterface(Ci.nsIAppWindow).initialOpenWindowInfo;

      if (!openWindowInfo && window.arguments && window.arguments[11]) {
        openWindowInfo = window.arguments[11];
      }

      let tabArgument = gBrowserInit.getTabToAdopt();

      // If we have a tab argument with browser, we use its remoteType. Otherwise,
      // if e10s is disabled or there's a parent process opener (e.g. parent
      // process about: page) for the content tab, we use a parent
      // process remoteType. Otherwise, we check the URI to determine
      // what to do - if there isn't one, we default to the default remote type.
      //
      // When adopting a tab, we'll also use that tab's browsingContextGroupId,
      // if available, to ensure we don't spawn a new process.
      let remoteType;
      let initialBrowsingContextGroupId;

      if (tabArgument && tabArgument.hasAttribute("usercontextid")) {
        // The window's first argument is a tab if and only if we are swapping tabs.
        // We must set the browser's usercontextid so that the newly created remote
        // tab child has the correct usercontextid.
        userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10);
      }

      if (tabArgument && tabArgument.linkedBrowser) {
        remoteType = tabArgument.linkedBrowser.remoteType;
        initialBrowsingContextGroupId =
          tabArgument.linkedBrowser.browsingContext?.group.id;
      } else if (openWindowInfo) {
        userContextId = openWindowInfo.originAttributes.userContextId;
        if (openWindowInfo.isRemote) {
          remoteType = E10SUtils.DEFAULT_REMOTE_TYPE;
        } else {
          remoteType = E10SUtils.NOT_REMOTE;
        }
      } else {
        let uriToLoad = gBrowserInit.uriToLoadPromise;
        if (uriToLoad && Array.isArray(uriToLoad)) {
          uriToLoad = uriToLoad[0]; // we only care about the first item
        }

        if (uriToLoad && typeof uriToLoad == "string") {
          let oa = E10SUtils.predictOriginAttributes({
            window,
            userContextId,
          });
          remoteType = E10SUtils.getRemoteTypeForURI(
            uriToLoad,
            gMultiProcessBrowser,
            gFissionBrowser,
            E10SUtils.DEFAULT_REMOTE_TYPE,
            null,
            oa
          );
        } else {
          // If we reach here, we don't have the url to load. This means that
          // `uriToLoad` is most likely a promise which is waiting on SessionStore
          // initialization. We can't delay setting up the browser here, as that
          // would mean that `gBrowser.selectedBrowser` might not always exist,
          // which is the current assumption.

          // In this case we default to the privileged about process as that's
          // the best guess we can make, and we'll likely need it eventually.
          remoteType = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
        }
      }

      let createOptions = {
        uriIsAboutBlank: false,
        userContextId,
        initialBrowsingContextGroupId,
        remoteType,
        openWindowInfo,
      };
      let browser = this.createBrowser(createOptions);
      browser.setAttribute("primary", "true");
      if (gBrowserAllowScriptsToCloseInitialTabs) {
        browser.setAttribute("allowscriptstoclose", "true");
      }
      browser.droppedLinkHandler = handleDroppedLink;
      browser.loadURI = URILoadingWrapper.loadURI.bind(
        URILoadingWrapper,
        browser
      );
      browser.fixupAndLoadURIString =
        URILoadingWrapper.fixupAndLoadURIString.bind(
          URILoadingWrapper,
          browser
        );

      let uniqueId = this._generateUniquePanelID();
      let panel = this.getPanel(browser);
      panel.id = uniqueId;
      this.tabpanels.appendChild(panel);

      let tab = this.tabs[0];
      tab.linkedPanel = uniqueId;
      this._selectedTab = tab;
      this._selectedBrowser = browser;
      tab.permanentKey = browser.permanentKey;
      tab._tPos = 0;
      tab._fullyOpen = true;
      tab.linkedBrowser = browser;

      if (userContextId) {
        tab.setAttribute("usercontextid", userContextId);
        ContextualIdentityService.setTabStyle(tab);
      }

      this._tabForBrowser.set(browser, tab);

      this._appendStatusPanel();

      // This is the initial browser, so it's usually active; the default is false
      // so we have to update it:
      browser.docShellIsActive = this.shouldActivateDocShell(browser);

      // Hook the browser up with a progress listener.
      let tabListener = new TabProgressListener(tab, browser, true, false);
      let filter = Cc[
        "@mozilla.org/appshell/component/browser-status-filter;1"
      ].createInstance(Ci.nsIWebProgress);
      filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
      this._tabListeners.set(tab, tabListener);
      this._tabFilters.set(tab, filter);
      browser.webProgress.addProgressListener(
        filter,
        Ci.nsIWebProgress.NOTIFY_ALL
      );
    },

    /**
     * BEGIN FORWARDED BROWSER PROPERTIES.  IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
     * MAKE SURE TO ADD IT HERE AS WELL.
     */
    get canGoBack() {
      return this.selectedBrowser.canGoBack;
    },

    get canGoForward() {
      return this.selectedBrowser.canGoForward;
    },

    goBack(requireUserInteraction) {
      return this.selectedBrowser.goBack(requireUserInteraction);
    },

    goForward(requireUserInteraction) {
      return this.selectedBrowser.goForward(requireUserInteraction);
    },

    reload() {
      return this.selectedBrowser.reload();
    },

    reloadWithFlags(aFlags) {
      return this.selectedBrowser.reloadWithFlags(aFlags);
    },

    stop() {
      return this.selectedBrowser.stop();
    },

    /**
     * throws exception for unknown schemes
     */
    loadURI(uri, params) {
      return this.selectedBrowser.loadURI(uri, params);
    },
    /**
     * throws exception for unknown schemes
     */
    fixupAndLoadURIString(uriString, params) {
      return this.selectedBrowser.fixupAndLoadURIString(uriString, params);
    },

    gotoIndex(aIndex) {
      return this.selectedBrowser.gotoIndex(aIndex);
    },

    get currentURI() {
      return this.selectedBrowser.currentURI;
    },

    get finder() {
      return this.selectedBrowser.finder;
    },

    get docShell() {
      return this.selectedBrowser.docShell;
    },

    get webNavigation() {
      return this.selectedBrowser.webNavigation;
    },

    get webProgress() {
      return this.selectedBrowser.webProgress;
    },

    get contentWindow() {
      return this.selectedBrowser.contentWindow;
    },

    get sessionHistory() {
      return this.selectedBrowser.sessionHistory;
    },

    get markupDocumentViewer() {
      return this.selectedBrowser.markupDocumentViewer;
    },

    get contentDocument() {
      return this.selectedBrowser.contentDocument;
    },

    get contentTitle() {
      return this.selectedBrowser.contentTitle;
    },

    get contentPrincipal() {
      return this.selectedBrowser.contentPrincipal;
    },

    get securityUI() {
      return this.selectedBrowser.securityUI;
    },

    set fullZoom(val) {
      this.selectedBrowser.fullZoom = val;
    },

    get fullZoom() {
      return this.selectedBrowser.fullZoom;
    },

    set textZoom(val) {
      this.selectedBrowser.textZoom = val;
    },

    get textZoom() {
      return this.selectedBrowser.textZoom;
    },

    get isSyntheticDocument() {
      return this.selectedBrowser.isSyntheticDocument;
    },

    set userTypedValue(val) {
      this.selectedBrowser.userTypedValue = val;
    },

    get userTypedValue() {
      return this.selectedBrowser.userTypedValue;
    },

    _setFindbarData() {
      // Ensure we know what the find bar key is in the content process:
      let { sharedData } = Services.ppmm;
      if (!sharedData.has("Findbar:Shortcut")) {
        let keyEl = document.getElementById("key_find");
        let mods = keyEl
          .getAttribute("modifiers")
          .replace(
            /accel/i,
            AppConstants.platform == "macosx" ? "meta" : "control"
          );
        sharedData.set("Findbar:Shortcut", {
          key: keyEl.getAttribute("key"),
          shiftKey: mods.includes("shift"),
          ctrlKey: mods.includes("control"),
          altKey: mods.includes("alt"),
          metaKey: mods.includes("meta"),
        });
      }
    },

    isFindBarInitialized(aTab) {
      return (aTab || this.selectedTab)._findBar != undefined;
    },

    /**
     * Get the already constructed findbar
     */
    getCachedFindBar(aTab = this.selectedTab) {
      return aTab._findBar;
    },

    /**
     * Get the findbar, and create it if it doesn't exist.
     * @return the find bar (or null if the window or tab is closed/closing in the interim).
     */
    async getFindBar(aTab = this.selectedTab) {
      let findBar = this.getCachedFindBar(aTab);
      if (findBar) {
        return findBar;
      }

      // Avoid re-entrancy by caching the promise we're about to return.
      if (!aTab._pendingFindBar) {
        aTab._pendingFindBar = this._createFindBar(aTab);
      }
      return aTab._pendingFindBar;
    },

    /**
     * Create a findbar instance.
     * @param aTab the tab to create the find bar for.
     * @return the created findbar, or null if the window or tab is closed/closing.
     */
    async _createFindBar(aTab) {
      let findBar = document.createXULElement("findbar");
      let browser = this.getBrowserForTab(aTab);

      browser.parentNode.insertAdjacentElement("afterend", findBar);

      await new Promise(r => requestAnimationFrame(r));
      delete aTab._pendingFindBar;
      if (window.closed || aTab.closing) {
        return null;
      }

      findBar.browser = browser;
      findBar._findField.value = this._lastFindValue;

      aTab._findBar = findBar;

      let event = document.createEvent("Events");
      event.initEvent("TabFindInitialized", true, false);
      aTab.dispatchEvent(event);

      return findBar;
    },

    _appendStatusPanel() {
      this.selectedBrowser.insertAdjacentElement("afterend", StatusPanel.panel);
    },

    _updateTabBarForPinnedTabs() {
      this.tabContainer._unlockTabSizing();
      this.tabContainer._positionPinnedTabs();
      this.tabContainer._updateCloseButtons();
    },

    _notifyPinnedStatus(aTab) {
      aTab.linkedBrowser.browsingContext.isAppTab = aTab.pinned;

      let event = document.createEvent("Events");
      event.initEvent(aTab.pinned ? "TabPinned" : "TabUnpinned", true, false);
      aTab.dispatchEvent(event);
    },

    pinTab(aTab) {
      if (aTab.pinned) {
        return;
      }

      this.showTab(aTab);
      this.moveTabTo(aTab, this._numPinnedTabs);
      aTab.setAttribute("pinned", "true");
      this._updateTabBarForPinnedTabs();
      this._notifyPinnedStatus(aTab);
    },

    unpinTab(aTab) {
      if (!aTab.pinned) {
        return;
      }

      this.moveTabTo(aTab, this._numPinnedTabs - 1);
      aTab.removeAttribute("pinned");
      aTab.style.marginInlineStart = "";
      aTab._pinnedUnscrollable = false;
      this._updateTabBarForPinnedTabs();
      this._notifyPinnedStatus(aTab);
    },

    previewTab(aTab, aCallback) {
      let currentTab = this.selectedTab;
      try {
        // Suppress focus, ownership and selected tab changes
        this._previewMode = true;
        this.selectedTab = aTab;
        aCallback();
      } finally {
        this.selectedTab = currentTab;
        this._previewMode = false;
      }
    },

    syncThrobberAnimations(aTab) {
      aTab.ownerGlobal.promiseDocumentFlushed(() => {
        if (!aTab.container) {
          return;
        }

        const animations = Array.from(
          aTab.container.getElementsByTagName("tab")
        )
          .map(tab => {
            const throbber = tab.throbber;
            return throbber ? throbber.getAnimations({ subtree: true }) : [];
          })
          .reduce((a, b) => a.concat(b))
          .filter(
            anim =>
              CSSAnimation.isInstance(anim) &&
              (anim.animationName === "tab-throbber-animation" ||
                anim.animationName === "tab-throbber-animation-rtl") &&
              anim.playState === "running"
          );

        // Synchronize with the oldest running animation, if any.
        const firstStartTime = Math.min(
          ...animations.map(anim =>
            anim.startTime === null ? Infinity : anim.startTime
          )
        );
        if (firstStartTime === Infinity) {
          return;
        }
        requestAnimationFrame(() => {
          for (let animation of animations) {
            // If |animation| has been cancelled since this rAF callback
            // was scheduled we don't want to set its startTime since
            // that would restart it. We check for a cancelled animation
            // by looking for a null currentTime rather than checking
            // the playState, since reading the playState of
            // a CSSAnimation object will flush style.
            if (animation.currentTime !== null) {
              animation.startTime = firstStartTime;
            }
          }
        });
      });
    },

    getBrowserAtIndex(aIndex) {
      return this.browsers[aIndex];
    },

    getBrowserForOuterWindowID(aID) {
      for (let b of this.browsers) {
        if (b.outerWindowID == aID) {
          return b;
        }
      }

      return null;
    },

    getTabForBrowser(aBrowser) {
      return this._tabForBrowser.get(aBrowser);
    },

    getPanel(aBrowser) {
      return this.getBrowserContainer(aBrowser).parentNode;
    },

    getBrowserContainer(aBrowser) {
      return (aBrowser || this.selectedBrowser).parentNode.parentNode;
    },

    getTabNotificationDeck() {
      if (!this._tabNotificationDeck) {
        let template = document.getElementById(
          "tab-notification-deck-template"
        );
        template.replaceWith(template.content);
        this._tabNotificationDeck = document.getElementById(
          "tab-notification-deck"
        );
      }
      return this._tabNotificationDeck;
    },

    _nextNotificationBoxId: 0,
    getNotificationBox(aBrowser) {
      let browser = aBrowser || this.selectedBrowser;
      if (!browser._notificationBox) {
        browser._notificationBox = new MozElements.NotificationBox(element => {
          element.setAttribute("notificationside", "top");
          element.setAttribute(
            "name",
            `tab-notification-box-${this._nextNotificationBoxId++}`
          );
          this.getTabNotificationDeck().append(element);
          if (browser == this.selectedBrowser) {
            this._updateVisibleNotificationBox(browser);
          }
        });
      }
      return browser._notificationBox;
    },

    readNotificationBox(aBrowser) {
      let browser = aBrowser || this.selectedBrowser;
      return browser._notificationBox || null;
    },

    _updateVisibleNotificationBox(aBrowser) {
      if (!this._tabNotificationDeck) {
        // If the deck hasn't been created we don't need to create it here.
        return;
      }
      let notificationBox = this.readNotificationBox(aBrowser);
      this.getTabNotificationDeck().selectedViewName = notificationBox
        ? notificationBox.stack.getAttribute("name")
        : "";
    },

    getTabModalPromptBox(aBrowser) {
      let browser = aBrowser || this.selectedBrowser;
      if (!browser.tabModalPromptBox) {
        browser.tabModalPromptBox = new TabModalPromptBox(browser);
      }
      return browser.tabModalPromptBox;
    },

    getTabDialogBox(aBrowser) {
      if (!aBrowser) {
        throw new Error("aBrowser is required");
      }
      if (!aBrowser.tabDialogBox) {
        aBrowser.tabDialogBox = new TabDialogBox(aBrowser);
      }
      return aBrowser.tabDialogBox;
    },

    getTabFromAudioEvent(aEvent) {
      if (!aEvent.isTrusted) {
        return null;
      }

      var browser = aEvent.originalTarget;
      var tab = this.getTabForBrowser(browser);
      return tab;
    },

    _callProgressListeners(
      aBrowser,
      aMethod,
      aArguments,
      aCallGlobalListeners = true,
      aCallTabsListeners = true
    ) {
      var rv = true;

      function callListeners(listeners, args) {
        for (let p of listeners) {
          if (aMethod in p) {
            try {
              if (!p[aMethod].apply(p, args)) {
                rv = false;
              }
            } catch (e) {
              // don't inhibit other listeners
              console.error(e);
            }
          }
        }
      }

      aBrowser = aBrowser || this.selectedBrowser;

      if (aCallGlobalListeners && aBrowser == this.selectedBrowser) {
        callListeners(this.mProgressListeners, aArguments);
      }

      if (aCallTabsListeners) {
        aArguments.unshift(aBrowser);

        callListeners(this.mTabsProgressListeners, aArguments);
      }

      return rv;
    },

    /**
     * Sets an icon for the tab if the URI is defined in FAVICON_DEFAULTS.
     */
    setDefaultIcon(aTab, aURI) {
      if (aURI && aURI.spec in FAVICON_DEFAULTS) {
        this.setIcon(aTab, FAVICON_DEFAULTS[aURI.spec]);
      }
    },

    setIcon(
      aTab,
      aIconURL = "",
      aOriginalURL = aIconURL,
      aLoadingPrincipal = null
    ) {
      let makeString = url => (url instanceof Ci.nsIURI ? url.spec : url);

      aIconURL = makeString(aIconURL);
      aOriginalURL = makeString(aOriginalURL);

      let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];

      if (
        aIconURL &&
        !aLoadingPrincipal &&
        !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
      ) {
        console.error(
          `Attempt to set a remote URL ${aIconURL} as a tab icon without a loading principal.`
        );
        return;
      }

      let browser = this.getBrowserForTab(aTab);
      browser.mIconURL = aIconURL;

      if (aIconURL != aTab.getAttribute("image")) {
        if (aIconURL) {
          if (aLoadingPrincipal) {
            aTab.setAttribute("iconloadingprincipal", aLoadingPrincipal);
          } else {
            aTab.removeAttribute("iconloadingprincipal");
          }
          aTab.setAttribute("image", aIconURL);
        } else {
          aTab.removeAttribute("image");
          aTab.removeAttribute("iconloadingprincipal");
        }
        this._tabAttrModified(aTab, ["image"]);
      }

      // The aOriginalURL argument is currently only used by tests.
      this._callProgressListeners(browser, "onLinkIconAvailable", [
        aIconURL,
        aOriginalURL,
      ]);
    },

    getIcon(aTab) {
      let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser;
      return browser.mIconURL;
    },

    setPageInfo(aURL, aDescription, aPreviewImage) {
      if (aURL) {
        let pageInfo = {
          url: aURL,
          description: aDescription,
          previewImageURL: aPreviewImage,
        };
        PlacesUtils.history.update(pageInfo).catch(console.error);
      }
    },

    getWindowTitleForBrowser(aBrowser) {
      let docElement = document.documentElement;
      let title = "";

      // If location bar is hidden and the URL type supports a host,
      // add the scheme and host to the title to prevent spoofing.
      // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239
      try {
        if (docElement.getAttribute("chromehidden").includes("location")) {
          const uri = Services.io.createExposableURI(aBrowser.currentURI);
          let prefix = uri.prePath;
          if (uri.scheme == "about") {
            prefix = uri.spec;
          } else if (uri.scheme == "moz-extension") {
            const ext = WebExtensionPolicy.getByHostname(uri.host);
            if (ext && ext.name) {
              let extensionLabel = document.getElementById(
                "urlbar-label-extension"
              );
              prefix = `${extensionLabel.value} (${ext.name})`;
            }
          }
          title = prefix + " - ";
        }
      } catch (e) {
        // ignored
      }

      if (docElement.hasAttribute("titlepreface")) {
        title += docElement.getAttribute("titlepreface");
      }

      let tab = this.getTabForBrowser(aBrowser);
      if (tab._labelIsContentTitle) {
        // Strip out any null bytes in the content title, since the
        // underlying widget implementations of nsWindow::SetTitle pass
        // null-terminated strings to system APIs.
        title += tab.getAttribute("label").replace(/\0/g, "");
      }

      let dataSuffix =
        docElement.getAttribute("privatebrowsingmode") == "temporary"
          ? "Private"
          : "Default";
      if (title) {
        // We're using a function rather than just using `title` as the
        // new substring to avoid `$$`, `$'` etc. having a special
        // meaning to `replace`.
        // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_a_parameter
        // and the documentation for functions for more info about this.
        return docElement.dataset["contentTitle" + dataSuffix].replace(
          "CONTENTTITLE",
          () => title
        );
      }

      return docElement.dataset["title" + dataSuffix];
    },

    updateTitlebar() {
      document.title = this.getWindowTitleForBrowser(this.selectedBrowser);
    },

    updateCurrentBrowser(aForceUpdate) {
      let newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
      if (this.selectedBrowser == newBrowser && !aForceUpdate) {
        return;
      }

      let newTab = this.getTabForBrowser(newBrowser);

      if (
        this._featureCallout &&
        this._featureCalloutPanelId !== newTab.linkedPanel
      ) {
        this._featureCallout.endTour(true);
        this._featureCallout = null;
      }

      // For now, only check for Feature Callout messages
      // when viewing PDFs. Later, we can expand this to check
      // for callout messages on every change of tab location.
      if (
        !this._featureCallout &&
        newBrowser.contentPrincipal.originNoSuffix === "resource://pdf.js"
      ) {
        this._instantiateFeatureCalloutTour(newBrowser, newTab.linkedPanel);
        window.gBrowser.featureCallout.showFeatureCallout();
      }

      if (!aForceUpdate) {
        TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");

        if (gMultiProcessBrowser) {
          this._asyncTabSwitching = true;
          this._getSwitcher().requestTab(newTab);
          this._asyncTabSwitching = false;
        }

        document.commandDispatcher.lock();
      }

      let oldTab = this.selectedTab;

      // Preview mode should not reset the owner
      if (!this._previewMode && !oldTab.selected) {
        oldTab.owner = null;
      }

      let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
      if (lastRelatedTab) {
        if (!lastRelatedTab.selected) {
          lastRelatedTab.owner = null;
        }
      }
      this._lastRelatedTabMap = new WeakMap();

      let oldBrowser = this.selectedBrowser;

      if (!gMultiProcessBrowser) {
        oldBrowser.removeAttribute("primary");
        oldBrowser.docShellIsActive = false;
        newBrowser.setAttribute("primary", "true");
        newBrowser.docShellIsActive = !document.hidden;
      }

      if (gURLBar) {
        oldBrowser._urlbarSelectionStart = gURLBar.selectionStart;
        oldBrowser._urlbarSelectionEnd = gURLBar.selectionEnd;
      }

      this._selectedBrowser = newBrowser;
      this._selectedTab = newTab;
      this.showTab(newTab);

      this._appendStatusPanel();

      this._updateVisibleNotificationBox(newBrowser);

      let oldBrowserPopupsBlocked =
        oldBrowser.popupBlocker.getBlockedPopupCount();
      let newBrowserPopupsBlocked =
        newBrowser.popupBlocker.getBlockedPopupCount();
      if (oldBrowserPopupsBlocked != newBrowserPopupsBlocked) {
        newBrowser.popupBlocker.updateBlockedPopupsUI();
      }

      // Update the URL bar.
      let webProgress = newBrowser.webProgress;
      this._callProgressListeners(
        null,
        "onLocationChange",
        [webProgress, null, newBrowser.currentURI, 0, true],
        true,
        false
      );

      let securityUI = newBrowser.securityUI;
      if (securityUI) {
        this._callProgressListeners(
          null,
          "onSecurityChange",
          [webProgress, null, securityUI.state],
          true,
          false
        );
        // Include the true final argument to indicate that this event is
        // simulated (instead of being observed by the webProgressListener).
        this._callProgressListeners(
          null,
          "onContentBlockingEvent",
          [webProgress, null, newBrowser.getContentBlockingEvents(), true],
          true,
          false
        );
      }

      let listener = this._tabListeners.get(newTab);
      if (listener && listener.mStateFlags) {
        this._callProgressListeners(
          null,
          "onUpdateCurrentBrowser",
          [
            listener.mStateFlags,
            listener.mStatus,
            listener.mMessage,
            listener.mTotalProgress,
          ],
          true,
          false
        );
      }

      if (!this._previewMode) {
        newTab.recordTimeFromUnloadToReload();
        newTab.updateLastAccessed();
        oldTab.updateLastAccessed();

        let oldFindBar = oldTab._findBar;
        if (
          oldFindBar &&
          oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
          !oldFindBar.hidden
        ) {
          this._lastFindValue = oldFindBar._findField.value;
        }

        this.updateTitlebar();

        newTab.removeAttribute("titlechanged");
        newTab.attention = false;

        // The tab has been selected, it's not unselected anymore.
        // (1) Call the current tab's finishUnselectedTabHoverTimer()
        //     to save a telemetry record.
        // (2) Call the current browser's unselectedTabHover() with false
        //     to dispatch an event.
        newTab.finishUnselectedTabHoverTimer();
        newBrowser.unselectedTabHover(false);
      }

      // If the new tab is busy, and our current state is not busy, then
      // we need to fire a start to all progress listeners.
      if (newTab.hasAttribute("busy") && !this._isBusy) {
        this._isBusy = true;
        this._callProgressListeners(
          null,
          "onStateChange",
          [
            webProgress,
            null,
            Ci.nsIWebProgressListener.STATE_START |
              Ci.nsIWebProgressListener.STATE_IS_NETWORK,
            0,
          ],
          true,
          false
        );
      }

      // If the new tab is not busy, and our current state is busy, then
      // we need to fire a stop to all progress listeners.
      if (!newTab.hasAttribute("busy") && this._isBusy) {
        this._isBusy = false;
        this._callProgressListeners(
          null,
          "onStateChange",
          [
            webProgress,
            null,
            Ci.nsIWebProgressListener.STATE_STOP |
              Ci.nsIWebProgressListener.STATE_IS_NETWORK,
            0,
          ],
          true,
          false
        );
      }

      // TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code
      // that might rely upon the other changes suppressed.
      // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window
      if (!this._previewMode) {
        // We've selected the new tab, so go ahead and notify listeners.
        let event = new CustomEvent("TabSelect", {
          bubbles: true,
          cancelable: false,
          detail: {
            previousTab: oldTab,
          },
        });
        newTab.dispatchEvent(event);

        this._tabAttrModified(oldTab, ["selected"]);
        this._tabAttrModified(newTab, ["selected"]);

        this.readNotificationBox(newBrowser)?.shown();

        this._startMultiSelectChange();
        this._multiSelectChangeSelected = true;
        this.clearMultiSelectedTabs();
        if (this._multiSelectChangeAdditions.size) {
          // Some tab has been multiselected just before switching tabs.
          // The tab that was selected at that point should also be multiselected.
          this.addToMultiSelectedTabs(oldTab);
        }

        if (oldBrowser != newBrowser && oldBrowser.getInPermitUnload) {
          oldBrowser.getInPermitUnload(inPermitUnload => {
            if (!inPermitUnload) {
              return;
            }
            // Since the user is switching away from a tab that has
            // a beforeunload prompt active, we remove the prompt.
            // This prevents confusing user flows like the following:
            //   1. User attempts to close Firefox
            //   2. User switches tabs (ingoring a beforeunload prompt)
            //   3. User returns to tab, presses "Leave page"
            let promptBox = this.getTabModalPromptBox(oldBrowser);
            let prompts = promptBox.listPrompts();
            // There might not be any prompts here if the tab was closed
            // while in an onbeforeunload prompt, which will have
            // destroyed aforementioned prompt already, so check there's
            // something to remove, first:
            if (prompts.length) {
              // NB: This code assumes that the beforeunload prompt
              //     is the top-most prompt on the tab.
              prompts[prompts.length - 1].abortPrompt();
            }
          });
        }

        if (!gMultiProcessBrowser) {
          this._adjustFocusBeforeTabSwitch(oldTab, newTab);
          this._adjustFocusAfterTabSwitch(newTab);
        }

        // Bug 1781806 - A forced update can indicate the tab was already
        // selected. To ensure the internal state of the Urlbar is kept in
        // sync, notify it as if focus changed. Alternatively, if there is no
        // force update but the load context is not using remote tabs, there
        // can be a focus change due to the _adjustFocus above.
        if (aForceUpdate || !gMultiProcessBrowser) {
          gURLBar.afterTabSwitchFocusChange();
        }
      }

      updateUserContextUIIndicator();
      gPermissionPanel.updateSharingIndicator();

      // Enable touch events to start a native dragging
      // session to allow the user to easily drag the selected tab.
      // This is currently only supported on Windows.
      oldTab.removeAttribute("touchdownstartsdrag");
      newTab.setAttribute("touchdownstartsdrag", "true");

      if (!gMultiProcessBrowser) {
        document.commandDispatcher.unlock();

        let event = new CustomEvent("TabSwitchDone", {
          bubbles: true,
          cancelable: true,
        });
        this.dispatchEvent(event);
      }

      if (!aForceUpdate) {
        TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
      }
    },

    _adjustFocusBeforeTabSwitch(oldTab, newTab) {
      if (this._previewMode) {
        return;
      }

      let oldBrowser = oldTab.linkedBrowser;
      let newBrowser = newTab.linkedBrowser;

      oldBrowser._urlbarFocused = gURLBar && gURLBar.focused;

      if (this._asyncTabSwitching) {
        newBrowser._userTypedValueAtBeforeTabSwitch = newBrowser.userTypedValue;
      }

      if (this.isFindBarInitialized(oldTab)) {
        let findBar = this.getCachedFindBar(oldTab);
        oldTab._findBarFocused =
          !findBar.hidden &&
          findBar._findField.getAttribute("focused") == "true";
      }

      let activeEl = document.activeElement;
      // If focus is on the old tab, move it to the new tab.
      if (activeEl == oldTab) {
        newTab.focus();
      } else if (
        gMultiProcessBrowser &&
        activeEl != newBrowser &&
        activeEl != newTab
      ) {
        // In e10s, if focus isn't already in the tabstrip or on the new browser,
        // and the new browser's previous focus wasn't in the url bar but focus is
        // there now, we need to adjust focus further.
        let keepFocusOnUrlBar =
          newBrowser && newBrowser._urlbarFocused && gURLBar && gURLBar.focused;
        if (!keepFocusOnUrlBar) {
          // Clear focus so that _adjustFocusAfterTabSwitch can detect if
          // some element has been focused and respect that.
          document.activeElement.blur();
        }
      }
    },

    _adjustFocusAfterTabSwitch(newTab) {
      // Don't steal focus from the tab bar.
      if (document.activeElement == newTab) {
        return;
      }

      let newBrowser = this.getBrowserForTab(newTab);

      if (newBrowser.hasAttribute("tabDialogShowing")) {
        newBrowser.tabDialogBox.focus();
        return;
      }
      if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
        // If there's a tabmodal prompt showing, focus it.
        let prompts = newBrowser.tabModalPromptBox.listPrompts();
        let prompt = prompts[prompts.length - 1];
        // @tabmodalPromptShowing is also set for other tab modal prompts
        // (e.g. the Payment Request dialog) so there may not be a <tabmodalprompt>.
        // Bug 1492814 will implement this for the Payment Request dialog.
        if (prompt) {
          prompt.Dialog.setDefaultFocus();
          return;
        }
      }

      // Focus the location bar if it was previously focused for that tab.
      // In full screen mode, only bother making the location bar visible
      // if the tab is a blank one.
      if (newBrowser._urlbarFocused && gURLBar) {
        let selectURL = () => {
          if (this._asyncTabSwitching) {
            // Set _awaitingSetURI flag to suppress popup notification
            // explicitly while tab switching asynchronously.
            newBrowser._awaitingSetURI = true;

            // The onLocationChange event called in updateCurrentBrowser() will
            // be captured in browser.js, then it calls gURLBar.setURI(). In case
            // of that doing processing of here before doing above processing,
            // the selection status that gURLBar.select() does will be releasing
            // by gURLBar.setURI(). To resolve it, we call gURLBar.select() after
            // finishing gURLBar.setURI().
            const currentActiveElement = document.activeElement;
            gURLBar.inputField.addEventListener(
              "SetURI",
              () => {
                delete newBrowser._awaitingSetURI;

                // If the user happened to type into the URL bar for this browser
                // by the time we got here, focusing will cause the text to be
                // selected which could cause them to overwrite what they've
                // already typed in.
                let userTypedValueAtBeforeTabSwitch =
                  newBrowser._userTypedValueAtBeforeTabSwitch;
                delete newBrowser._userTypedValueAtBeforeTabSwitch;
                if (
                  newBrowser.userTypedValue &&
                  newBrowser.userTypedValue != userTypedValueAtBeforeTabSwitch
                ) {
                  return;
                }

                if (currentActiveElement != document.activeElement) {
                  return;
                }

                gURLBar.setSelectionRange(
                  newBrowser._urlbarSelectionStart,
                  newBrowser._urlbarSelectionEnd
                );
              },
              { once: true }
            );
          } else {
            gURLBar.setSelectionRange(
              newBrowser._urlbarSelectionStart,
              newBrowser._urlbarSelectionEnd
            );
          }
        };

        // This inDOMFullscreen attribute indicates that the page has something
        // such as a video in fullscreen mode. Opening a new tab will cancel
        // fullscreen mode, so we need to wait for that to happen and then
        // select the url field.
        if (window.document.documentElement.hasAttribute("inDOMFullscreen")) {
          window.addEventListener("MozDOMFullscreen:Exited", selectURL, {
            once: true,
            wantsUntrusted: false,
          });
          return;
        }

        if (!window.fullScreen || newTab.isEmpty) {
          selectURL();
          return;
        }
      }

      // Focus the find bar if it was previously focused for that tab.
      if (
        gFindBarInitialized &&
        !gFindBar.hidden &&
        this.selectedTab._findBarFocused
      ) {
        gFindBar._findField.focus();
        return;
      }

      // Don't focus the content area if something has been focused after the
      // tab switch was initiated.
      if (gMultiProcessBrowser && document.activeElement != document.body) {
        return;
      }

      // We're now committed to focusing the content area.
      let fm = Services.focus;
      let focusFlags = fm.FLAG_NOSCROLL;

      if (!gMultiProcessBrowser) {
        let newFocusedElement = fm.getFocusedElementForWindow(
          window.content,
          true,
          {}
        );

        // for anchors, use FLAG_SHOWRING so that it is clear what link was
        // last clicked when switching back to that tab
        if (
          newFocusedElement &&
          (HTMLAnchorElement.isInstance(newFocusedElement) ||
            newFocusedElement.getAttributeNS(
              "http://www.w3.org/1999/xlink",
              "type"
            ) == "simple")
        ) {
          focusFlags |= fm.FLAG_SHOWRING;
        }
      }

      fm.setFocus(newBrowser, focusFlags);
    },

    _tabAttrModified(aTab, aChanged) {
      if (aTab.closing) {
        return;
      }

      let event = new CustomEvent("TabAttrModified", {
        bubbles: true,
        cancelable: false,
        detail: {
          changed: aChanged,
        },
      });
      aTab.dispatchEvent(event);
    },

    resetBrowserSharing(aBrowser) {
      let tab = this.getTabForBrowser(aBrowser);
      if (!tab) {
        return;
      }
      // If WebRTC was used, leave object to enable tracking of grace periods.
      tab._sharingState = tab._sharingState?.webRTC ? { webRTC: {} } : {};
      tab.removeAttribute("sharing");
      this._tabAttrModified(tab, ["sharing"]);
      if (aBrowser == this.selectedBrowser) {
        gPermissionPanel.updateSharingIndicator();
      }
    },

    updateBrowserSharing(aBrowser, aState) {
      let tab = this.getTabForBrowser(aBrowser);
      if (!tab) {
        return;
      }
      if (tab._sharingState == null) {
        tab._sharingState = {};
      }
      tab._sharingState = Object.assign(tab._sharingState, aState);

      if ("webRTC" in aState) {
        if (tab._sharingState.webRTC?.sharing) {
          if (tab._sharingState.webRTC.paused) {
            tab.removeAttribute("sharing");
          } else {
            tab.setAttribute("sharing", aState.webRTC.sharing);
          }
        } else {
          tab.removeAttribute("sharing");
        }
        this._tabAttrModified(tab, ["sharing"]);
      }

      if (aBrowser == this.selectedBrowser) {
        gPermissionPanel.updateSharingIndicator();
      }
    },

    getTabSharingState(aTab) {
      // Normalize the state object for consumers (ie.extensions).
      let state = Object.assign(
        {},
        aTab._sharingState && aTab._sharingState.webRTC
      );
      return {
        camera: !!state.camera,
        microphone: !!state.microphone,
        screen: state.screen && state.screen.replace("Paused", ""),
      };
    },

    setInitialTabTitle(aTab, aTitle, aOptions = {}) {
      // Convert some non-content title (actually a url) to human readable title
      if (!aOptions.isContentTitle && isBlankPageURL(aTitle)) {
        aTitle = this.tabContainer.emptyTabTitle;
      }

      if (aTitle) {
        if (!aTab.getAttribute("label")) {
          aTab._labelIsInitialTitle = true;
        }

        this._setTabLabel(aTab, aTitle, aOptions);
      }
    },

    _dataURLRegEx: /^data:[^,]+;base64,/i,

    setTabTitle(aTab) {
      var browser = this.getBrowserForTab(aTab);
      var title = browser.contentTitle;

      if (aTab.hasAttribute("customizemode")) {
        title = this.tabLocalization.formatValueSync(
          "tabbrowser-customizemode-tab-title"
        );
      }

      // Don't replace an initially set label with the URL while the tab
      // is loading.
      if (aTab._labelIsInitialTitle) {
        if (!title) {
          return false;
        }
        delete aTab._labelIsInitialTitle;
      }

      let isURL = false;
      let isContentTitle = !!title;
      if (!title) {
        // See if we can use the URI as the title.
        if (browser.currentURI.displaySpec) {
          try {
            title = Services.io.createExposableURI(
              browser.currentURI
            ).displaySpec;
          } catch (ex) {
            title = browser.currentURI.displaySpec;
          }
        }

        if (title && !isBlankPageURL(title)) {
          isURL = true;
          if (title.length <= 500 || !this._dataURLRegEx.test(title)) {
            // Try to unescape not-ASCII URIs using the current character set.
            try {
              let characterSet = browser.characterSet;
              title = Services.textToSubURI.unEscapeNonAsciiURI(
                characterSet,
                title
              );
            } catch (ex) {
              /* Do nothing. */
            }
          }
        } else {
          // No suitable URI? Fall back to our untitled string.
          title = this.tabContainer.emptyTabTitle;
        }
      }

      return this._setTabLabel(aTab, title, { isContentTitle, isURL });
    },

    // While an auth prompt from a base domain different than the current sites is open, we do not want to show the tab title of the current site,
    // but of the origin that is requesting authentication.
    // This is to prevent possible auth spoofing scenarios.
    // See bug 791594 for reference.
    setTabLabelForAuthPrompts(aTab, aLabel) {
      return this._setTabLabel(aTab, aLabel);
    },

    _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
      if (!aLabel || aLabel.includes("about:reader?")) {
        return false;
      }

      // If it's a long data: URI that uses base64 encoding, truncate to a
      // reasonable length rather than trying to display the entire thing,
      // which can hang or crash the browser.
      // We can't shorten arbitrary URIs like this, as bidi etc might mean
      // we need the trailing characters for display. But a base64-encoded
      // data-URI is plain ASCII, so this is OK for tab-title display.
      // (See bug 1408854.)
      if (isURL && aLabel.length > 500 && this._dataURLRegEx.test(aLabel)) {
        aLabel = aLabel.substring(0, 500) + "\u2026";
      }

      aTab._fullLabel = aLabel;

      if (!isContentTitle) {
        // Remove protocol and "www."
        if (!("_regex_shortenURLForTabLabel" in this)) {
          this._regex_shortenURLForTabLabel = /^[^:]+:\/\/(?:www\.)?/;
        }
        aLabel = aLabel.replace(this._regex_shortenURLForTabLabel, "");
      }

      aTab._labelIsContentTitle = isContentTitle;

      if (aTab.getAttribute("label") == aLabel) {
        return false;
      }

      let dwu = window.windowUtils;
      let isRTL =
        dwu.getDirectionFromText(aLabel) == Ci.nsIDOMWindowUtils.DIRECTION_RTL;

      aTab.setAttribute("label", aLabel);
      aTab.setAttribute("labeldirection", isRTL ? "rtl" : "ltr");
      aTab.toggleAttribute("labelendaligned", isRTL != (document.dir == "rtl"));

      // Dispatch TabAttrModified event unless we're setting the label
      // before the TabOpen event was dispatched.
      if (!beforeTabOpen) {
        this._tabAttrModified(aTab, ["label"]);
      }

      if (aTab.selected) {
        this.updateTitlebar();
      }

      return true;
    },

    loadTabs(
      aURIs,
      {
        allowInheritPrincipal,
        allowThirdPartyFixup,
        inBackground,
        newIndex,
        postDatas,
        replace,
        targetTab,
        triggeringPrincipal,
        csp,
        userContextId,
        fromExternal,
      } = {}
    ) {
      if (!aURIs.length) {
        return;
      }

      // The tab selected after this new tab is closed (i.e. the new tab's
      // "owner") is the next adjacent tab (i.e. not the previously viewed tab)
      // when several urls are opened here (i.e. closing the first should select
      // the next of many URLs opened) or if the pref to have UI links opened in
      // the background is set (i.e. the link is not being opened modally)
      //
      // i.e.
      //    Number of URLs    Load UI Links in BG       Focus Last Viewed?
      //    == 1              false                     YES
      //    == 1              true                      NO
      //    > 1               false/true                NO
      var multiple = aURIs.length > 1;
      var owner = multiple || inBackground ? null : this.selectedTab;
      var firstTabAdded = null;
      var targetTabIndex = -1;

      if (typeof newIndex != "number") {
        newIndex = -1;
      }

      // When bulk opening tabs, such as from a bookmark folder, we want to insertAfterCurrent
      // if necessary, but we also will set the bulkOrderedOpen flag so that the bookmarks
      // open in the same order they are in the folder.
      if (
        multiple &&
        newIndex < 0 &&
        Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent")
      ) {
        newIndex = this.selectedTab._tPos + 1;
      }

      if (replace) {
        let browser;
        if (targetTab) {
          browser = this.getBrowserForTab(targetTab);
          targetTabIndex = targetTab._tPos;
        } else {
          browser = this.selectedBrowser;
          targetTabIndex = this.tabContainer.selectedIndex;
        }
        let flags = LOAD_FLAGS_NONE;
        if (allowThirdPartyFixup) {
          flags |=
            LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
        }
        if (!allowInheritPrincipal) {
          flags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
        }
        if (fromExternal) {
          flags |= LOAD_FLAGS_FROM_EXTERNAL;
        }
        try {
          browser.fixupAndLoadURIString(aURIs[0], {
            flags,
            postData: postDatas && postDatas[0],
            triggeringPrincipal,
            csp,
          });
        } catch (e) {
          // Ignore failure in case a URI is wrong, so we can continue
          // opening the next ones.
        }
      } else {
        let params = {
          allowInheritPrincipal,
          ownerTab: owner,
          skipAnimation: multiple,
          allowThirdPartyFixup,
          postData: postDatas && postDatas[0],
          userContextId,
          triggeringPrincipal,
          bulkOrderedOpen: multiple,
          csp,
          fromExternal,
        };
        if (newIndex > -1) {
          params.index = newIndex;
        }
        firstTabAdded = this.addTab(aURIs[0], params);
        if (newIndex > -1) {
          targetTabIndex = firstTabAdded._tPos;
        }
      }

      let tabNum = targetTabIndex;
      for (let i = 1; i < aURIs.length; ++i) {
        let params = {
          allowInheritPrincipal,
          skipAnimation: true,
          allowThirdPartyFixup,
          postData: postDatas && postDatas[i],
          userContextId,
          triggeringPrincipal,
          bulkOrderedOpen: true,
          csp,
          fromExternal,
        };
        if (targetTabIndex > -1) {
          params.index = ++tabNum;
        }
        this.addTab(aURIs[i], params);
      }

      if (firstTabAdded && !inBackground) {
        this.selectedTab = firstTabAdded;
      }
    },

    updateBrowserRemoteness(aBrowser, { newFrameloader, remoteType } = {}) {
      let isRemote = aBrowser.getAttribute("remote") == "true";

      // We have to be careful with this here, as the "no remote type" is null,
      // not a string. Make sure to check only for undefined, since null is
      // allowed.
      if (remoteType === undefined) {
        throw new Error("Remote type must be set!");
      }

      let shouldBeRemote = remoteType !== E10SUtils.NOT_REMOTE;

      if (!gMultiProcessBrowser && shouldBeRemote) {
        throw new Error(
          "Cannot switch to remote browser in a window " +
            "without the remote tabs load context."
        );
      }

      // Abort if we're not going to change anything
      let oldRemoteType = aBrowser.remoteType;
      if (
        isRemote == shouldBeRemote &&
        !newFrameloader &&
        (!isRemote || oldRemoteType == remoteType)
      ) {
        return false;
      }

      let tab = this.getTabForBrowser(aBrowser);
      // aBrowser needs to be inserted now if it hasn't been already.
      this._insertBrowser(tab);

      let evt = document.createEvent("Events");
      evt.initEvent("BeforeTabRemotenessChange", true, false);
      tab.dispatchEvent(evt);

      let wasActive = document.activeElement == aBrowser;

      // Unhook our progress listener.
      let filter = this._tabFilters.get(tab);
      let listener = this._tabListeners.get(tab);
      aBrowser.webProgress.removeProgressListener(filter);
      filter.removeProgressListener(listener);

      // We'll be creating a new listener, so destroy the old one.
      listener.destroy();

      let oldDroppedLinkHandler = aBrowser.droppedLinkHandler;
      let oldUserTypedValue = aBrowser.userTypedValue;
      let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping();

      // Change the "remote" attribute.

      // Make sure the browser is destroyed so it unregisters from observer notifications
      aBrowser.destroy();

      if (shouldBeRemote) {
        aBrowser.setAttribute("remote", "true");
        aBrowser.setAttribute("remoteType", remoteType);
      } else {
        aBrowser.setAttribute("remote", "false");
        aBrowser.removeAttribute("remoteType");
      }

      // This call actually switches out our frameloaders. Do this as late as
      // possible before rebuilding the browser, as we'll need the new browser
      // state set up completely first.
      aBrowser.changeRemoteness({
        remoteType,
      });

      // Once we have new frameloaders, this call sets the browser back up.
      aBrowser.construct();

      aBrowser.userTypedValue = oldUserTypedValue;
      if (hadStartedLoad) {
        aBrowser.urlbarChangeTracker.startedLoad();
      }

      aBrowser.droppedLinkHandler = oldDroppedLinkHandler;

      // This shouldn't really be necessary, however, this has the side effect
      // of sending MozLayerTreeReady / MozLayerTreeCleared events for remote
      // frames, which the tab switcher depends on.
      //
      // eslint-disable-next-line no-self-assign
      aBrowser.docShellIsActive = aBrowser.docShellIsActive;

      // Create a new tab progress listener for the new browser we just injected,
      // since tab progress listeners have logic for handling the initial about:blank
      // load
      listener = new TabProgressListener(tab, aBrowser, true, false);
      this._tabListeners.set(tab, listener);
      filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);

      // Restore the progress listener.
      aBrowser.webProgress.addProgressListener(
        filter,
        Ci.nsIWebProgress.NOTIFY_ALL
      );

      // Restore the securityUI state.
      let securityUI = aBrowser.securityUI;
      let state = securityUI
        ? securityUI.state
        : Ci.nsIWebProgressListener.STATE_IS_INSECURE;
      this._callProgressListeners(
        aBrowser,
        "onSecurityChange",
        [aBrowser.webProgress, null, state],
        true,
        false
      );
      let event = aBrowser.getContentBlockingEvents();
      // Include the true final argument to indicate that this event is
      // simulated (instead of being observed by the webProgressListener).
      this._callProgressListeners(
        aBrowser,
        "onContentBlockingEvent",
        [aBrowser.webProgress, null, event, true],
        true,
        false
      );

      if (shouldBeRemote) {
        // Switching the browser to be remote will connect to a new child
        // process so the browser can no longer be considered to be
        // crashed.
        tab.removeAttribute("crashed");
        // we call updatetabIndicatorAttr here, rather than _tabAttrModified, so as
        // to be consistent with how "crashed" attribute changes are handled elsewhere
        this.tabContainer.updateTabIndicatorAttr(tab);
      }

      if (wasActive) {
        aBrowser.focus();
      }

      // If the findbar has been initialised, reset its browser reference.
      if (this.isFindBarInitialized(tab)) {
        this.getCachedFindBar(tab).browser = aBrowser;
      }

      evt = document.createEvent("Events");
      evt.initEvent("TabRemotenessChange", true, false);
      tab.dispatchEvent(evt);

      return true;
    },

    updateBrowserRemotenessByURL(aBrowser, aURL, aOptions = {}) {
      if (!gMultiProcessBrowser) {
        return this.updateBrowserRemoteness(aBrowser, {
          remoteType: E10SUtils.NOT_REMOTE,
        });
      }

      let oldRemoteType = aBrowser.remoteType;

      let oa = E10SUtils.predictOriginAttributes({ browser: aBrowser });

      aOptions.remoteType = E10SUtils.getRemoteTypeForURI(
        aURL,
        gMultiProcessBrowser,
        gFissionBrowser,
        oldRemoteType,
        aBrowser.currentURI,
        oa
      );

      // If this URL can't load in the current browser then flip it to the
      // correct type.
      if (oldRemoteType != aOptions.remoteType || aOptions.newFrameloader) {
        return this.updateBrowserRemoteness(aBrowser, aOptions);
      }

      return false;
    },

    createBrowser({
      isPreloadBrowser,
      name,
      openWindowInfo,
      remoteType,
      initialBrowsingContextGroupId,
      uriIsAboutBlank,
      userContextId,
      skipLoad,
      initiallyActive,
    } = {}) {
      let b = document.createXULElement("browser");
      // Use the JSM global to create the permanentKey, so that if the
      // permanentKey is held by something after this window closes, it
      // doesn't keep the window alive.
      b.permanentKey = new (Cu.getGlobalForObject(Services).Object)();

      // Ensure that SessionStore has flushed any session history state from the
      // content process before we this browser's remoteness.
      if (!Services.appinfo.sessionHistoryInParent) {
        b.prepareToChangeRemoteness = () =>
          SessionStore.prepareToChangeRemoteness(b);
        b.afterChangeRemoteness = switchId => {
          let tab = this.getTabForBrowser(b);
          SessionStore.finishTabRemotenessChange(tab, switchId);
          return true;
        };
      }

      const defaultBrowserAttributes = {
        contextmenu: "contentAreaContextMenu",
        message: "true",
        messagemanagergroup: "browsers",
        tooltip: "aHTMLTooltip",
        type: "content",
      };
      for (let attribute in defaultBrowserAttributes) {
        b.setAttribute(attribute, defaultBrowserAttributes[attribute]);
      }

      if (gMultiProcessBrowser || remoteType) {
        b.setAttribute("maychangeremoteness", "true");
      }

      if (!initiallyActive) {
        b.setAttribute("initiallyactive", "false");
      }

      if (userContextId) {
        b.setAttribute("usercontextid", userContextId);
      }

      if (remoteType) {
        b.setAttribute("remoteType", remoteType);
        b.setAttribute("remote", "true");
      }

      if (!isPreloadBrowser) {
        b.setAttribute("autocompletepopup", "PopupAutoComplete");
      }

      /*
       * This attribute is meant to describe if the browser is the
       * preloaded browser. When the preloaded browser is created, the
       * 'preloadedState' attribute for that browser is set to "preloaded", and
       * when a new tab is opened, and it is time to show that preloaded
       * browser, the 'preloadedState' attribute for that browser is removed.
       *
       * See more details on Bug 1420285.
       */
      if (isPreloadBrowser) {
        b.setAttribute("preloadedState", "preloaded");
      }

      // Ensure that the browser will be created in a specific initial
      // BrowsingContextGroup. This may change the process selection behaviour
      // of the newly created browser, and is often used in combination with
      // "remoteType" to ensure that the initial about:blank load occurs
      // within the same process as another window.
      if (initialBrowsingContextGroupId) {
        b.setAttribute(
          "initialBrowsingContextGroupId",
          initialBrowsingContextGroupId
        );
      }

      // Propagate information about the opening content window to the browser.
      if (openWindowInfo) {
        b.openWindowInfo = openWindowInfo;
      }

      // This will be used by gecko to control the name of the opened
      // window.
      if (name) {
        // XXX: The `name` property is special in HTML and XUL. Should
        // we use a different attribute name for this?
        b.setAttribute("name", name);
      }

      let notificationbox = document.createXULElement("notificationbox");
      notificationbox.setAttribute("notificationside", "top");

      let stack = document.createXULElement("stack");
      stack.className = "browserStack";
      stack.appendChild(b);

      let decorator = document.createXULElement("hbox");
      decorator.className = "browserDecorator";
      stack.appendChild(decorator);

      let browserContainer = document.createXULElement("vbox");
      browserContainer.className = "browserContainer";
      browserContainer.appendChild(notificationbox);
      browserContainer.appendChild(stack);

      let browserSidebarContainer = document.createXULElement("hbox");
      browserSidebarContainer.className = "browserSidebarContainer";
      browserSidebarContainer.appendChild(browserContainer);

      // Prevent the superfluous initial load of a blank document
      // if we're going to load something other than about:blank.
      if (!uriIsAboutBlank || skipLoad) {
        b.setAttribute("nodefaultsrc", "true");
      }

      return b;
    },

    _createLazyBrowser(aTab) {
      let browser = aTab.linkedBrowser;

      let names = this._browserBindingProperties;

      for (let i = 0; i < names.length; i++) {
        let name = names[i];
        let getter;
        let setter;
        switch (name) {
          case "audioMuted":
            getter = () => aTab.hasAttribute("muted");
            break;
          case "contentTitle":
            getter = () => SessionStore.getLazyTabValue(aTab, "title");
            break;
          case "currentURI":
            getter = () => {
              // Avoid recreating the same nsIURI object over and over again...
              if (browser._cachedCurrentURI) {
                return browser._cachedCurrentURI;
              }
              let url =
                SessionStore.getLazyTabValue(aTab, "url") || "about:blank";
              return (browser._cachedCurrentURI = Services.io.newURI(url));
            };
            break;
          case "didStartLoadSinceLastUserTyping":
            getter = () => () => false;
            break;
          case "fullZoom":
          case "textZoom":
            getter = () => 1;
            break;
          case "tabHasCustomZoom":
            getter = () => false;
            break;
          case "getTabBrowser":
            getter = () => () => this;
            break;
          case "isRemoteBrowser":
            getter = () => browser.getAttribute("remote") == "true";
            break;
          case "permitUnload":
            getter = () => () => ({ permitUnload: true });
            break;
          case "reload":
          case "reloadWithFlags":
            getter = () => params => {
              // Wait for load handler to be instantiated before
              // initializing the reload.
              aTab.addEventListener(
                "SSTabRestoring",
                () => {
                  browser[name](params);
                },
                { once: true }
              );
              gBrowser._insertBrowser(aTab);
            };
            break;
          case "remoteType":
            getter = () => {
              let url =
                SessionStore.getLazyTabValue(aTab, "url") || "about:blank";
              // Avoid recreating the same nsIURI object over and over again...
              let uri;
              if (browser._cachedCurrentURI) {
                uri = browser._cachedCurrentURI;
              } else {
                uri = browser._cachedCurrentURI = Services.io.newURI(url);
              }
              let oa = E10SUtils.predictOriginAttributes({
                browser,
                userContextId: aTab.getAttribute("usercontextid"),
              });
              return E10SUtils.getRemoteTypeForURI(
                url,
                gMultiProcessBrowser,
                gFissionBrowser,
                undefined,
                uri,
                oa
              );
            };
            break;
          case "userTypedValue":
          case "userTypedClear":
            getter = () => SessionStore.getLazyTabValue(aTab, name);
            break;
          default:
            getter = () => {
              if (AppConstants.NIGHTLY_BUILD) {
                let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
                Services.console.logStringMessage(message + new Error().stack);
              }
              this._insertBrowser(aTab);
              return browser[name];
            };
            setter = value => {
              if (AppConstants.NIGHTLY_BUILD) {
                let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
                Services.console.logStringMessage(message + new Error().stack);
              }
              this._insertBrowser(aTab);
              return (browser[name] = value);
            };
        }
        Object.defineProperty(browser, name, {
          get: getter,
          set: setter,
          configurable: true,
          enumerable: true,
        });
      }
    },

    _insertBrowser(aTab, aInsertedOnTabCreation) {
      "use strict";

      // If browser is already inserted or window is closed don't do anything.
      if (aTab.linkedPanel || window.closed) {
        return;
      }

      let browser = aTab.linkedBrowser;

      // If browser is a lazy browser, delete the substitute properties.
      if (this._browserBindingProperties[0] in browser) {
        for (let name of this._browserBindingProperties) {
          delete browser[name];
        }
      }

      let { uriIsAboutBlank, usingPreloadedContent } = aTab._browserParams;
      delete aTab._browserParams;
      delete browser._cachedCurrentURI;

      let panel = this.getPanel(browser);
      let uniqueId = this._generateUniquePanelID();
      panel.id = uniqueId;
      aTab.linkedPanel = uniqueId;

      // Inject the <browser> into the DOM if necessary.
      if (!panel.parentNode) {
        // NB: this appendChild call causes us to run constructors for the
        // browser element, which fires off a bunch of notifications. Some
        // of those notifications can cause code to run that inspects our
        // state, so it is important that the tab element is fully
        // initialized by this point.
        this.tabpanels.appendChild(panel);
      }

      // wire up a progress listener for the new browser object.
      let tabListener = new TabProgressListener(
        aTab,
        browser,
        uriIsAboutBlank,
        usingPreloadedContent
      );
      const filter = Cc[
        "@mozilla.org/appshell/component/browser-status-filter;1"
      ].createInstance(Ci.nsIWebProgress);
      filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
      browser.webProgress.addProgressListener(
        filter,
        Ci.nsIWebProgress.NOTIFY_ALL
      );
      this._tabListeners.set(aTab, tabListener);
      this._tabFilters.set(aTab, filter);

      browser.droppedLinkHandler = handleDroppedLink;
      browser.loadURI = URILoadingWrapper.loadURI.bind(
        URILoadingWrapper,
        browser
      );
      browser.fixupAndLoadURIString =
        URILoadingWrapper.fixupAndLoadURIString.bind(
          URILoadingWrapper,
          browser
        );

      // Most of the time, we start our browser's docShells out as inactive,
      // and then maintain activeness in the tab switcher. Preloaded about:newtab's
      // are already created with their docShell's as inactive, but then explicitly
      // render their layers to ensure that we can switch to them quickly. We avoid
      // setting docShellIsActive to false again in this case, since that'd cause
      // the layers for the preloaded tab to be dropped, and we'd see a flash
      // of empty content instead.
      //
      // So for all browsers except for the preloaded case, we set the browser
      // docShell to inactive.
      if (!usingPreloadedContent) {
        browser.docShellIsActive = false;
      }

      // If we transitioned from one browser to two browsers, we need to set
      // hasSiblings=false on both the existing browser and the new browser.
      if (this.tabs.length == 2) {
        this.tabs[0].linkedBrowser.browsingContext.hasSiblings = true;
        this.tabs[1].linkedBrowser.browsingContext.hasSiblings = true;
      } else {
        aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
      }

      if (aTab.userContextId) {
        browser.setAttribute("usercontextid", aTab.userContextId);
      }

      browser.browsingContext.isAppTab = aTab.pinned;

      // We don't want to update the container icon and identifier if
      // this is not the selected browser.
      if (aTab.selected) {
        updateUserContextUIIndicator();
      }

      // Only fire this event if the tab is already in the DOM
      // and will be handled by a listener.
      if (aTab.isConnected) {
        var evt = new CustomEvent("TabBrowserInserted", {
          bubbles: true,
          detail: { insertedOnTabCreation: aInsertedOnTabCreation },
        });
        aTab.dispatchEvent(evt);
      }
    },

    _mayDiscardBrowser(aTab, aForceDiscard) {
      let browser = aTab.linkedBrowser;
      let action = aForceDiscard ? "unload" : "dontUnload";

      if (
        !aTab ||
        aTab.selected ||
        aTab.closing ||
        this._windowIsClosing ||
        !browser.isConnected ||
        !browser.isRemoteBrowser ||
        !browser.permitUnload(action).permitUnload
      ) {
        return false;
      }

      return true;
    },

    discardBrowser(aTab, aForceDiscard) {
      "use strict";
      let browser = aTab.linkedBrowser;

      if (!this._mayDiscardBrowser(aTab, aForceDiscard)) {
        return false;
      }

      // Reset sharing state.
      if (aTab._sharingState) {
        this.resetBrowserSharing(browser);
      }
      webrtcUI.forgetStreamsFromBrowserContext(browser.browsingContext);

      // Set browser parameters for when browser is restored.  Also remove
      // listeners and set up lazy restore data in SessionStore. This must
      // be done before browser is destroyed and removed from the document.
      aTab._browserParams = {
        uriIsAboutBlank: browser.currentURI.spec == "about:blank",
        remoteType: browser.remoteType,
        usingPreloadedContent: false,
      };

      SessionStore.resetBrowserToLazyState(aTab);

      // Remove the tab's filter and progress listener.
      let filter = this._tabFilters.get(aTab);
      let listener = this._tabListeners.get(aTab);
      browser.webProgress.removeProgressListener(filter);
      filter.removeProgressListener(listener);
      listener.destroy();

      this._tabListeners.delete(aTab);
      this._tabFilters.delete(aTab);

      // Reset the findbar and remove it if it is attached to the tab.
      if (aTab._findBar) {
        aTab._findBar.close(true);
        aTab._findBar.remove();
        delete aTab._findBar;
      }

      // Remove potentially stale attributes.
      let attributesToRemove = [
        "activemedia-blocked",
        "busy",
        "pendingicon",
        "progress",
        "soundplaying",
      ];
      let removedAttributes = [];
      for (let attr of attributesToRemove) {
        if (aTab.hasAttribute(attr)) {
          removedAttributes.push(attr);
          aTab.removeAttribute(attr);
        }
      }
      if (removedAttributes.length) {
        this._tabAttrModified(aTab, removedAttributes);
      }

      browser.destroy();
      this.getPanel(browser).remove();
      aTab.removeAttribute("linkedpanel");

      this._createLazyBrowser(aTab);

      let evt = new CustomEvent("TabBrowserDiscarded", { bubbles: true });
      aTab.dispatchEvent(evt);
      return true;
    },

    /**
     * Loads a tab with a default null principal unless specified
     */
    addWebTab(aURI, params = {}) {
      if (!params.triggeringPrincipal) {
        params.triggeringPrincipal =
          Services.scriptSecurityManager.createNullPrincipal({
            userContextId: params.userContextId,
          });
      }
      if (params.triggeringPrincipal.isSystemPrincipal) {
        throw new Error(
          "System principal should never be passed into addWebTab()"
        );
      }
      return this.addTab(aURI, params);
    },

    addAdjacentNewTab(tab) {
      Services.obs.notifyObservers(
        {
          wrappedJSObject: new Promise(resolve => {
            this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
              index: tab._tPos + 1,
              userContextId: tab.userContextId,
            });
            resolve(this.selectedBrowser);
          }),
        },
        "browser-open-newtab-start"
      );
    },

    /**
     * Must only be used sparingly for content that came from Chrome context
     * If in doubt use addWebTab
     */
    addTrustedTab(aURI, params = {}) {
      params.triggeringPrincipal =
        Services.scriptSecurityManager.getSystemPrincipal();
      return this.addTab(aURI, params);
    },

    /**
     * @returns {object}
     *    The new tab. The return value will be null if the tab couldn't be
     *    created; this shouldn't normally happen, and an error will be logged
     *    to the console if it does.
     */
    addTab(
      uriString,
      {
        allowInheritPrincipal,
        allowThirdPartyFixup,
        bulkOrderedOpen,
        charset,
        createLazyBrowser,
        disableTRR,
        eventDetail,
        focusUrlBar,
        forceNotRemote,
        forceAllowDataURI,
        fromExternal,
        inBackground = true,
        index,
        lazyTabTitle,
        name,
        noInitialLabel,
        openWindowInfo,
        openerBrowser,
        originPrincipal,
        originStoragePrincipal,
        ownerTab,
        pinned,
        postData,
        preferredRemoteType,
        referrerInfo,
        relatedToCurrent,
        initialBrowsingContextGroupId,
        skipAnimation,
        skipBackgroundNotify,
        triggeringPrincipal,
        userContextId,
        csp,
        skipLoad = createLazyBrowser,
        insertTab = true,
        globalHistoryOptions,
        triggeringRemoteType,
      } = {}
    ) {
      // all callers of addTab that pass a params object need to pass
      // a valid triggeringPrincipal.
      if (!triggeringPrincipal) {
        throw new Error(
          "Required argument triggeringPrincipal missing within addTab"
        );
      }

      if (!UserInteraction.running("browser.tabs.opening", window)) {
        UserInteraction.start("browser.tabs.opening", "initting", window);
      }

      // If we're opening a foreground tab, set the owner by default.
      ownerTab ??= inBackground ? null : this.selectedTab;

      // Don't use document.l10n.setAttributes because the FTL file is loaded
      // lazily and we won't be able to resolve the string.
      document
        .getElementById("History:UndoCloseTab")
        .setAttribute("data-l10n-args", JSON.stringify({ tabCount: 1 }));

      // if we're adding tabs, we're past interrupt mode, ditch the owner
      if (this.selectedTab.owner) {
        this.selectedTab.owner = null;
      }

      // Find the tab that opened this one, if any. This is used for
      // determining positioning, and inherited attributes such as the
      // user context ID.
      //
      // If we have a browser opener (which is usually the browser
      // element from a remote window.open() call), use that.
      //
      // Otherwise, if the tab is related to the current tab (e.g.,
      // because it was opened by a link click), use the selected tab as
      // the owner. If referrerInfo is set, and we don't have an
      // explicit relatedToCurrent arg, we assume that the tab is
      // related to the current tab, since referrerURI is null or
      // undefined if the tab is opened from an external application or
      // bookmark (i.e. somewhere other than an existing tab).
      if (relatedToCurrent == null) {
        relatedToCurrent = !!(referrerInfo && referrerInfo.originalReferrer);
      }
      let openerTab =
        (openerBrowser && this.getTabForBrowser(openerBrowser)) ||
        (relatedToCurrent && this.selectedTab);

      // When overflowing, new tabs are scrolled into view smoothly, which
      // doesn't go well together with the width transition. So we skip the
      // transition in that case.
      let animate =
        !skipAnimation &&
        !pinned &&
        this.tabContainer.getAttribute("overflow") != "true" &&
        !gReduceMotion;

      let uriInfo = this._determineURIToLoad(uriString, createLazyBrowser);
      let { uri, uriIsAboutBlank, lazyBrowserURI } = uriInfo;
      // Have to overwrite this if we're lazy-loading. Should go away
      // with bug 1818777.
      ({ uriString } = uriInfo);

      let usingPreloadedContent = false;
      let b, t;

      try {
        t = this._createTab({
          uriString,
          animate,
          userContextId,
          openerTab,
          createLazyBrowser,
          skipAnimation,
          pinned,
          noInitialLabel,
          skipBackgroundNotify,
        });
        if (insertTab) {
          // insert the tab into the tab container in the correct position
          this._insertTabAtIndex(t, {
            index,
            ownerTab,
            openerTab,
            pinned,
            bulkOrderedOpen,
          });
        }

        ({ browser: b, usingPreloadedContent } = this._createBrowserForTab(t, {
          uriString,
          uri,
          preferredRemoteType,
          openerBrowser,
          uriIsAboutBlank,
          referrerInfo,
          forceNotRemote,
          name,
          initialBrowsingContextGroupId,
          openWindowInfo,
          skipLoad,
        }));

        if (focusUrlBar) {
          b._urlbarFocused = true;
        }

        // If the caller opts in, create a lazy browser.
        if (createLazyBrowser) {
          this._createLazyBrowser(t);

          if (lazyBrowserURI) {
            // Lazy browser must be explicitly registered so tab will appear as
            // a switch-to-tab candidate in autocomplete.
            this.UrlbarProviderOpenTabs.registerOpenTab(
              lazyBrowserURI.spec,
              t.userContextId,
              PrivateBrowsingUtils.isWindowPrivate(window)
            );
            b.registeredOpenURI = lazyBrowserURI;
          }
          // If we're not inserting the tab into the DOM, we can't set the tab
          // state meaningfully. Session restore (the only caller who does this)
          // will have to do this work itself later, when the tabs have been
          // inserted.
          if (insertTab) {
            SessionStore.setTabState(t, {
              entries: [
                {
                  url: lazyBrowserURI?.spec || "about:blank",
                  title: lazyTabTitle,
                  triggeringPrincipal_base64:
                    E10SUtils.serializePrincipal(triggeringPrincipal),
                },
              ],
              // Make sure to store the userContextId associated to the lazy tab
              // otherwise it would be created as a default tab when recreated on a
              // session restore (See Bug 1819794).
              userContextId,
            });
          }
        } else {
          this._insertBrowser(t, true);
          // If we were called by frontend and don't have openWindowInfo,
          // but we were opened from another browser, set the cross group
          // opener ID:
          if (openerBrowser && !openWindowInfo) {
            b.browsingContext.setCrossGroupOpener(
              openerBrowser.browsingContext
            );
          }
        }
      } catch (e) {
        console.error("Failed to create tab");
        console.error(e);
        t?.remove();
        if (t?.linkedBrowser) {
          this._tabFilters.delete(t);
          this._tabListeners.delete(t);
          this.getPanel(t.linkedBrowser).remove();
        }
        return null;
      }

      if (insertTab) {
        // Fire a TabOpen event
        this._fireTabOpen(t, eventDetail);

        this._kickOffBrowserLoad(b, {
          uri,
          uriString,
          usingPreloadedContent,
          triggeringPrincipal,
          originPrincipal,
          originStoragePrincipal,
          uriIsAboutBlank,
          allowInheritPrincipal,
          allowThirdPartyFixup,
          fromExternal,
          disableTRR,
          forceAllowDataURI,
          skipLoad,
          referrerInfo,
          charset,
          postData,
          csp,
          globalHistoryOptions,
          triggeringRemoteType,
        });
      }

      // This field is updated regardless if we actually animate
      // since it's important that we keep this count correct in all cases.
      this.tabAnimationsInProgress++;

      if (animate) {
        requestAnimationFrame(function () {
          // kick the animation off
          t.setAttribute("fadein", "true");
        });
      }

      // Additionally send pinned tab events
      if (pinned) {
        this._notifyPinnedStatus(t);
      }

      gSharedTabWarning.tabAdded(t);

      if (!inBackground) {
        this.selectedTab = t;
      }
      return t;
    },

    _determineURIToLoad(uriString, createLazyBrowser) {
      uriString = uriString || "about:blank";
      let aURIObject = null;
      try {
        aURIObject = Services.io.newURI(uriString);
      } catch (ex) {
        /* we'll try to fix up this URL later */
      }

      let lazyBrowserURI;
      if (createLazyBrowser && uriString != "about:blank") {
        lazyBrowserURI = aURIObject;
        uriString = "about:blank";
      }

      let uriIsAboutBlank = uriString == "about:blank";
      return { uri: aURIObject, uriIsAboutBlank, lazyBrowserURI, uriString };
    },

    _createTab({
      uriString,
      userContextId,
      openerTab,
      createLazyBrowser,
      skipAnimation,
      pinned,
      noInitialLabel,
      skipBackgroundNotify,
      animate,
    }) {
      var t = document.createXULElement("tab", { is: "tabbrowser-tab" });
      // Tag the tab as being created so extension code can ignore events
      // prior to TabOpen.
      t.initializingTab = true;
      t.openerTab = openerTab;

      // Related tab inherits current tab's user context unless a different
      // usercontextid is specified
      if (userContextId == null && openerTab) {
        userContextId = openerTab.getAttribute("usercontextid") || 0;
      }

      if (!noInitialLabel) {
        if (isBlankPageURL(uriString)) {
          t.setAttribute("label", this.tabContainer.emptyTabTitle);
        } else {
          // Set URL as label so that the tab isn't empty initially.
          this.setInitialTabTitle(t, uriString, {
            beforeTabOpen: true,
            isURL: true,
          });
        }
      }

      if (userContextId) {
        t.setAttribute("usercontextid", userContextId);
        ContextualIdentityService.setTabStyle(t);
      }

      if (skipBackgroundNotify) {
        t.setAttribute("skipbackgroundnotify", true);
      }

      if (pinned) {
        t.setAttribute("pinned", "true");
      }

      t.classList.add("tabbrowser-tab");

      this.tabContainer._unlockTabSizing();

      if (!animate) {
        UserInteraction.update("browser.tabs.opening", "not-animated", window);
        t.setAttribute("fadein", "true");

        // Call _handleNewTab asynchronously as it needs to know if the
        // new tab is selected.
        setTimeout(
          function (tabContainer) {
            tabContainer._handleNewTab(t);
          },
          0,
          this.tabContainer
        );
      } else {
        UserInteraction.update("browser.tabs.opening", "animated", window);
      }

      return t;
    },

    _createBrowserForTab(
      tab,
      {
        uriString,
        uri,
        name,
        preferredRemoteType,
        openerBrowser,
        uriIsAboutBlank,
        referrerInfo,
        forceNotRemote,
        initialBrowsingContextGroupId,
        openWindowInfo,
        skipLoad,
      }
    ) {
      // If we don't have a preferred remote type, and we have a remote
      // opener, use the opener's remote type.
      if (!preferredRemoteType && openerBrowser) {
        preferredRemoteType = openerBrowser.remoteType;
      }

      let { userContextId } = tab;

      var oa = E10SUtils.predictOriginAttributes({ window, userContextId });

      // If URI is about:blank and we don't have a preferred remote type,
      // then we need to use the referrer, if we have one, to get the
      // correct remote type for the new tab.
      if (
        uriIsAboutBlank &&
        !preferredRemoteType &&
        referrerInfo &&
        referrerInfo.originalReferrer
      ) {
        preferredRemoteType = E10SUtils.getRemoteTypeForURI(
          referrerInfo.originalReferrer.spec,
          gMultiProcessBrowser,
          gFissionBrowser,
          E10SUtils.DEFAULT_REMOTE_TYPE,
          null,
          oa
        );
      }

      let remoteType = forceNotRemote
        ? E10SUtils.NOT_REMOTE
        : E10SUtils.getRemoteTypeForURI(
            uriString,
            gMultiProcessBrowser,
            gFissionBrowser,
            preferredRemoteType,
            null,
            oa
          );

      let b,
        usingPreloadedContent = false;
      // If we open a new tab with the newtab URL in the default
      // userContext, check if there is a preloaded browser ready.
      if (uriString == BROWSER_NEW_TAB_URL && !userContextId) {
        b = NewTabPagePreloading.getPreloadedBrowser(window);
        if (b) {
          usingPreloadedContent = true;
        }
      }

      if (!b) {
        // No preloaded browser found, create one.
        b = this.createBrowser({
          remoteType,
          uriIsAboutBlank,
          userContextId,
          initialBrowsingContextGroupId,
          openWindowInfo,
          name,
          skipLoad,
        });
      }

      tab.linkedBrowser = b;

      this._tabForBrowser.set(b, tab);
      tab.permanentKey = b.permanentKey;
      tab._browserParams = {
        uriIsAboutBlank,
        remoteType,
        usingPreloadedContent,
      };

      // Hack to ensure that the about:newtab, and about:welcome favicon is loaded
      // instantaneously, to avoid flickering and improve perceived performance.
      this.setDefaultIcon(tab, uri);

      return { browser: b, usingPreloadedContent };
    },

    _kickOffBrowserLoad(
      browser,
      {
        uri,
        uriString,
        usingPreloadedContent,
        triggeringPrincipal,
        originPrincipal,
        originStoragePrincipal,
        uriIsAboutBlank,
        allowInheritPrincipal,
        allowThirdPartyFixup,
        fromExternal,
        disableTRR,
        forceAllowDataURI,
        skipLoad,
        referrerInfo,
        charset,
        postData,
        csp,
        globalHistoryOptions,
        triggeringRemoteType,
      }
    ) {
      if (
        !usingPreloadedContent &&
        originPrincipal &&
        originStoragePrincipal &&
        uriString
      ) {
        let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
        // Unless we know for sure we're not inheriting principals,
        // force the about:blank viewer to have the right principal:
        if (!uri || doGetProtocolFlags(uri) & URI_INHERITS_SECURITY_CONTEXT) {
          browser.createAboutBlankContentViewer(
            originPrincipal,
            originStoragePrincipal
          );
        }
      }

      // If we didn't swap docShells with a preloaded browser
      // then let's just continue loading the page normally.
      if (
        !usingPreloadedContent &&
        (!uriIsAboutBlank || !allowInheritPrincipal) &&
        !skipLoad
      ) {
        // pretend the user typed this so it'll be available till
        // the document successfully loads
        if (uriString && !gInitialPages.includes(uriString)) {
          browser.userTypedValue = uriString;
        }

        let flags = LOAD_FLAGS_NONE;
        if (allowThirdPartyFixup) {
          flags |=
            LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
        }
        if (fromExternal) {
          flags |= LOAD_FLAGS_FROM_EXTERNAL;
        } else if (!triggeringPrincipal.isSystemPrincipal) {
          // XXX this code must be reviewed and changed when bug 1616353
          // lands.
          flags |= LOAD_FLAGS_FIRST_LOAD;
        }
        if (!allowInheritPrincipal) {
          flags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
        }
        if (disableTRR) {
          flags |= LOAD_FLAGS_DISABLE_TRR;
        }
        if (forceAllowDataURI) {
          flags |= LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
        }
        try {
          browser.fixupAndLoadURIString(uriString, {
            flags,
            triggeringPrincipal,
            referrerInfo,
            charset,
            postData,
            csp,
            globalHistoryOptions,
            triggeringRemoteType,
          });
        } catch (ex) {
          console.error(ex);
        }
      }
    },

    createTabsForSessionRestore(restoreTabsLazily, selectTab, tabDataList) {
      let tabs = [];
      let tabsFragment = document.createDocumentFragment();
      let tabToSelect = null;
      let hiddenTabs = new Map();
      let shouldUpdateForPinnedTabs = false;

      // We create each tab and browser, but only insert them
      // into a document fragment so that we can insert them all
      // together. This prevents synch reflow for each tab
      // insertion.
      for (var i = 0; i < tabDataList.length; i++) {
        let tabData = tabDataList[i];

        let userContextId = tabData.userContextId;
        let select = i == selectTab - 1;
        let tab;
        let tabWasReused = false;

        // Re-use existing selected tab if possible to avoid the overhead of
        // selecting a new tab.
        if (
          select &&
          this.selectedTab.userContextId == userContextId &&
          !SessionStore.isTabRestoring(this.selectedTab)
        ) {
          tabWasReused = true;
          tab = this.selectedTab;
          if (!tabData.pinned) {
            this.unpinTab(tab);
          } else {
            this.pinTab(tab);
          }
        }

        // Add a new tab if needed.
        if (!tab) {
          let createLazyBrowser =
            restoreTabsLazily && !select && !tabData.pinned;

          let url = "about:blank";
          if (tabData.entries?.length) {
            let activeIndex = (tabData.index || tabData.entries.length) - 1;
            // Ensure the index is in bounds.
            activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
            activeIndex = Math.max(activeIndex, 0);
            url = tabData.entries[activeIndex].url;
          }

          let preferredRemoteType = E10SUtils.getRemoteTypeForURI(
            url,
            gMultiProcessBrowser,
            gFissionBrowser,
            E10SUtils.DEFAULT_REMOTE_TYPE,
            null,
            E10SUtils.predictOriginAttributes({ window, userContextId })
          );

          // If we're creating a lazy browser, let tabbrowser know the future
          // URI because progress listeners won't get onLocationChange
          // notification before the browser is inserted.
          //
          // Setting noInitialLabel is a perf optimization. Rendering tab labels
          // would make resizing the tabs more expensive as we're adding them.
          // Each tab will get its initial label set in restoreTab.
          tab = this.addTrustedTab(createLazyBrowser ? url : "about:blank", {
            createLazyBrowser,
            skipAnimation: true,
            noInitialLabel: true,
            userContextId,
            skipBackgroundNotify: true,
            bulkOrderedOpen: true,
            insertTab: false,
            skipLoad: true,
            preferredRemoteType,
          });

          if (select) {
            tabToSelect = tab;
          }
        }

        tabs.push(tab);

        if (tabData.pinned) {
          // Calling `pinTab` calls `moveTabTo`, which assumes the tab is
          // inserted in the DOM. If the tab is not yet in the DOM,
          // just insert it in the right place from the start.
          if (!tab.parentNode) {
            tab._tPos = this._numPinnedTabs;
            this.tabContainer.insertBefore(tab, this.tabs[this._numPinnedTabs]);
            tab.setAttribute("pinned", "true");
            this.tabContainer._invalidateCachedTabs();
            // Then ensure all the tab open/pinning information is sent.
            this._fireTabOpen(tab, {});
            this._notifyPinnedStatus(tab);
            // Once we're done adding all tabs, _updateTabBarForPinnedTabs
            // needs calling:
            shouldUpdateForPinnedTabs = true;
          }
        } else {
          if (tab.hidden) {
            tab.hidden = true;
            hiddenTabs.set(tab, tabData.extData && tabData.extData.hiddenBy);
          }

          tabsFragment.appendChild(tab);
          if (tabWasReused) {
            this.tabContainer._invalidateCachedTabs();
          }
        }

        tab.initialize();
      }

      // inject the new DOM nodes
      this.tabContainer.appendChild(tabsFragment);

      for (let [tab, hiddenBy] of hiddenTabs) {
        let event = document.createEvent("Events");
        event.initEvent("TabHide", true, false);
        tab.dispatchEvent(event);
        if (hiddenBy) {
          SessionStore.setCustomTabValue(tab, "hiddenBy", hiddenBy);
        }
      }

      this.tabContainer._invalidateCachedTabs();
      if (shouldUpdateForPinnedTabs) {
        this._updateTabBarForPinnedTabs();
      }

      // We need to wait until after all tabs have been appended to the DOM
      // to remove the old selected tab.
      if (tabToSelect) {
        let leftoverTab = this.selectedTab;
        this.selectedTab = tabToSelect;
        this.removeTab(leftoverTab);
      }

      if (tabs.length > 1 || !tabs[0].selected) {
        this._updateTabsAfterInsert();
        TabBarVisibility.update();

        for (let tab of tabs) {
          // If tabToSelect is a tab, we didn't reuse the selected tab.
          if (tabToSelect || !tab.selected) {
            // Fire a TabOpen event for all unpinned tabs, except reused selected
            // tabs.
            if (!tab.pinned) {
              this._fireTabOpen(tab, {});
            }

            // Fire a TabBrowserInserted event on all tabs that have a connected,
            // real browser, except for reused selected tabs.
            if (tab.linkedPanel) {
              var evt = new CustomEvent("TabBrowserInserted", {
                bubbles: true,
                detail: { insertedOnTabCreation: true },
              });
              tab.dispatchEvent(evt);
            }
          }
        }
      }

      return tabs;
    },

    moveTabsToStart(contextTab) {
      let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab];
      // Walk the array in reverse order so the tabs are kept in order.
      for (let i = tabs.length - 1; i >= 0; i--) {
        let tab = tabs[i];
        if (tab._tPos > 0) {
          this.moveTabTo(tab, 0);
        }
      }
    },

    moveTabsToEnd(contextTab) {
      let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab];
      for (let tab of tabs) {
        if (tab._tPos < this.tabs.length - 1) {
          this.moveTabTo(tab, this.tabs.length - 1);
        }
      }
    },

    warnAboutClosingTabs(tabsToClose, aCloseTabs, aSource) {
      if (tabsToClose <= 1) {
        return true;
      }

      const pref =
        aCloseTabs == this.closingTabsEnum.ALL
          ? "browser.tabs.warnOnClose"
          : "browser.tabs.warnOnCloseOtherTabs";
      var shouldPrompt = Services.prefs.getBoolPref(pref);
      if (!shouldPrompt) {
        return true;
      }

      const maxTabsUndo = Services.prefs.getIntPref(
        "browser.sessionstore.max_tabs_undo"
      );
      if (
        aCloseTabs != this.closingTabsEnum.ALL &&
        tabsToClose <= maxTabsUndo
      ) {
        return true;
      }

      // Our prompt to close this window is most important, so replace others.
      gDialogBox.replaceDialogIfOpen();

      var ps = Services.prompt;

      // default to true: if it were false, we wouldn't get this far
      var warnOnClose = { value: true };

      // focus the window before prompting.
      // this will raise any minimized window, which will
      // make it obvious which window the prompt is for and will
      // solve the problem of windows "obscuring" the prompt.
      // see bug #350299 for more details
      window.focus();
      const [title, button, checkbox] = this.tabLocalization.formatValuesSync([
        {
          id: "tabbrowser-confirm-close-tabs-title",
          args: { tabCount: tabsToClose },
        },
        { id: "tabbrowser-confirm-close-tabs-button" },
        { id: "tabbrowser-confirm-close-tabs-checkbox" },
      ]);
      let flags =
        ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0 +
        ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1;
      let checkboxLabel =
        aCloseTabs == this.closingTabsEnum.ALL ? checkbox : null;
      var buttonPressed = ps.confirmEx(
        window,
        title,
        null,
        flags,
        button,
        null,
        null,
        checkboxLabel,
        warnOnClose
      );

      Services.telemetry.setEventRecordingEnabled("close_tab_warning", true);
      let closeTabEnumKey =
        Object.entries(this.closingTabsEnum)
          .find(([k, v]) => v == aCloseTabs)?.[0]
          ?.toLowerCase() || "some";

      let warnCheckbox = warnOnClose.value ? "checked" : "unchecked";
      if (!checkboxLabel) {
        warnCheckbox = "not-present";
      }
      let sessionWillBeRestored =
        Services.prefs.getIntPref("browser.startup.page") == 3 ||
        Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
      let closesWindow = aCloseTabs == this.closingTabsEnum.ALL;
      Services.telemetry.recordEvent(
        "close_tab_warning",
        "shown",
        closesWindow ? "window" : "tabs",
        null,
        {
          source: aSource || `close-${closeTabEnumKey}-tabs`,
          button: buttonPressed == 0 ? "close" : "cancel",
          warn_checkbox: warnCheckbox,
          closing_tabs: "" + tabsToClose,
          closing_wins: "" + +closesWindow, // ("1" or "0", depending on the value)
          // This value doesn't really apply to whether this warning
          // gets shown, but having pings be consistent (and perhaps
          // being able to see trends for users with/without sessionrestore)
          // seems useful:
          will_restore: sessionWillBeRestored ? "yes" : "no",
        }
      );

      var reallyClose = buttonPressed == 0;

      // don't set the pref unless they press OK and it's false
      if (
        aCloseTabs == this.closingTabsEnum.ALL &&
        reallyClose &&
        !warnOnClose.value
      ) {
        Services.prefs.setBoolPref(pref, false);
      }

      return reallyClose;
    },

    /**
     * This determines where the tab should be inserted within the tabContainer
     */
    _insertTabAtIndex(
      tab,
      { index, ownerTab, openerTab, pinned, bulkOrderedOpen } = {}
    ) {
      // If this new tab is owned by another, assert that relationship
      if (ownerTab) {
        tab.owner = ownerTab;
      }

      // Ensure we have an index if one was not provided.
      if (typeof index != "number") {
        // Move the new tab after another tab if needed, to the end otherwise.
        index = Infinity;
        if (
          !bulkOrderedOpen &&
          ((openerTab &&
            Services.prefs.getBoolPref(
              "browser.tabs.insertRelatedAfterCurrent"
            )) ||
            Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent"))
        ) {
          let lastRelatedTab =
            openerTab && this._lastRelatedTabMap.get(openerTab);
          let previousTab = lastRelatedTab || openerTab || this.selectedTab;
          if (!previousTab.hidden) {
            index = previousTab._tPos + 1;
          } else if (previousTab == FirefoxViewHandler.tab) {
            index = 0;
          }

          if (lastRelatedTab) {
            lastRelatedTab.owner = null;
          } else if (openerTab) {
            tab.owner = openerTab;
          }
          // Always set related map if opener exists.
          if (openerTab) {
            this._lastRelatedTabMap.set(openerTab, tab);
          }
        }
      }
      // Ensure index is within bounds.
      if (pinned) {
        index = Math.max(index, 0);
        index = Math.min(index, this._numPinnedTabs);
      } else {
        index = Math.max(index, this._numPinnedTabs);
        index = Math.min(index, this.tabs.length);
      }

      let tabAfter = this.tabs[index] || null;
      this.tabContainer._invalidateCachedTabs();
      // Prevent a flash of unstyled content by setting up the tab content
      // and inherited attributes before appending it (see Bug 1592054):
      tab.initialize();
      this.tabContainer.insertBefore(tab, tabAfter);
      if (tabAfter) {
        this._updateTabsAfterInsert();
      } else {
        tab._tPos = index;
      }

      if (pinned) {
        this._updateTabBarForPinnedTabs();
      }

      TabBarVisibility.update();
    },

    /**
     * Dispatch a new tab event. This should be called when things are in a
     * consistent state, such that listeners of this event can again open
     * or close tabs.
     */
    _fireTabOpen(tab, eventDetail) {
      delete tab.initializingTab;
      let evt = new CustomEvent("TabOpen", {
        bubbles: true,
        detail: eventDetail || {},
      });
      tab.dispatchEvent(evt);
    },

    getTabsToTheStartFrom(aTab) {
      let tabsToStart = [];
      if (aTab.hidden) {
        return tabsToStart;
      }
      let tabs = this.visibleTabs;
      for (let i = 0; i < tabs.length; ++i) {
        if (tabs[i] == aTab) {
          break;
        }
        // Ignore pinned tabs.
        if (tabs[i].pinned) {
          continue;
        }
        // In a multi-select context, select all unselected tabs
        // starting from the context tab.
        if (aTab.multiselected && tabs[i].multiselected) {
          continue;
        }
        tabsToStart.push(tabs[i]);
      }
      return tabsToStart;
    },

    getTabsToTheEndFrom(aTab) {
      let tabsToEnd = [];
      if (aTab.hidden) {
        return tabsToEnd;
      }
      let tabs = this.visibleTabs;
      for (let i = tabs.length - 1; i >= 0; --i) {
        if (tabs[i] == aTab) {
          break;
        }
        // Ignore pinned tabs.
        if (tabs[i].pinned) {
          continue;
        }
        // In a multi-select context, select all unselected tabs
        // starting from the context tab.
        if (aTab.multiselected && tabs[i].multiselected) {
          continue;
        }
        tabsToEnd.push(tabs[i]);
      }
      return tabsToEnd;
    },

    /**
     * In a multi-select context, the tabs (except pinned tabs) that are located to the
     * left of the leftmost selected tab will be removed.
     */
    removeTabsToTheStartFrom(aTab) {
      let tabs = this.getTabsToTheStartFrom(aTab);
      if (
        !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_START)
      ) {
        return;
      }

      this.removeTabs(tabs);
    },

    /**
     * In a multi-select context, the tabs (except pinned tabs) that are located to the
     * right of the rightmost selected tab will be removed.
     */
    removeTabsToTheEndFrom(aTab) {
      let tabs = this.getTabsToTheEndFrom(aTab);
      if (
        !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END)
      ) {
        return;
      }

      this.removeTabs(tabs);
    },

    /**
     * In a multi-select context, all unpinned and unselected tabs are removed.
     * Otherwise all unpinned tabs except aTab are removed.
     *
     * @param   aTab
     *          The tab we will skip removing
     * @param   aParams
     *          An optional set of parameters that will be passed to the
     *          removeTabs function.
     */
    removeAllTabsBut(aTab, aParams) {
      let tabsToRemove = [];
      if (aTab && aTab.multiselected) {
        tabsToRemove = this.visibleTabs.filter(
          tab => !tab.multiselected && !tab.pinned
        );
      } else {
        tabsToRemove = this.visibleTabs.filter(
          tab => tab != aTab && !tab.pinned
        );
      }

      if (
        !this.warnAboutClosingTabs(
          tabsToRemove.length,
          this.closingTabsEnum.OTHER
        )
      ) {
        return;
      }

      this.removeTabs(tabsToRemove, aParams);
    },

    removeMultiSelectedTabs() {
      let selectedTabs = this.selectedTabs;
      if (
        !this.warnAboutClosingTabs(
          selectedTabs.length,
          this.closingTabsEnum.MULTI_SELECTED
        )
      ) {
        return;
      }

      this.removeTabs(selectedTabs);
    },

    /**
     * @typedef {object} _startRemoveTabsReturnValue
     * @property {Promise} beforeUnloadComplete
     *   A promise that is resolved once all the beforeunload handlers have been
     *   called.
     * @property {object[]} tabsWithBeforeUnloadPrompt
     *   An array of tabs with unload prompts that need to be handled.
     * @property {object} [lastToClose]
     *   The last tab to be closed, if appropriate.
     */

    /**
     * Starts to remove tabs from the UI: checking for beforeunload handlers,
     * closing tabs where possible and triggering running of the unload handlers.
     *
     * @param {object[]} tabs
     *   The set of tabs to remove.
     * @param {object} options
     * @param {boolean} options.animate
     *   Whether or not to animate closing.
     * @param {boolean} options.suppressWarnAboutClosingWindow
     *   This will supress the warning about closing a window with the last tab.
     * @param {boolean} options.skipPermitUnload
     *   Skips the before unload checks for the tabs. Only set this to true when
     *   using it in tandem with `runBeforeUnloadForTabs`.
     * @param {boolean} options.skipRemoves
     *   Skips actually removing the tabs. The beforeunload handlers still run.
     * @returns {_startRemoveTabsReturnValue}
     */
    _startRemoveTabs(
      tabs,
      { animate, suppressWarnAboutClosingWindow, skipPermitUnload, skipRemoves }
    ) {
      // Note: if you change any of the unload algorithm, consider also
      // changing `runBeforeUnloadForTabs` above.
      let tabsWithBeforeUnloadPrompt = [];
      let tabsWithoutBeforeUnload = [];
      let beforeUnloadPromises = [];
      let lastToClose;

      for (let tab of tabs) {
        if (!skipRemoves) {
          tab._closedInGroup = true;
        }
        if (!skipRemoves && tab.selected) {
          lastToClose = tab;
          let toBlurTo = this._findTabToBlurTo(lastToClose, tabs);
          if (toBlurTo) {
            this._getSwitcher().warmupTab(toBlurTo);
          }
        } else if (!skipPermitUnload && this._hasBeforeUnload(tab)) {
          TelemetryStopwatch.start("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", tab);
          // We need to block while calling permitUnload() because it
          // processes the event queue and may lead to another removeTab()
          // call before permitUnload() returns.
          tab._pendingPermitUnload = true;
          beforeUnloadPromises.push(
            // To save time, we first run the beforeunload event listeners in all
            // content processes in parallel. Tabs that would have shown a prompt
            // will be handled again later.
            tab.linkedBrowser.asyncPermitUnload("dontUnload").then(
              ({ permitUnload }) => {
                tab._pendingPermitUnload = false;
                TelemetryStopwatch.finish(
                  "FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS",
                  tab
                );
                if (tab.closing) {
                  // The tab was closed by the user while we were in permitUnload, don't
                  // attempt to close it a second time.
                } else if (permitUnload) {
                  if (!skipRemoves) {
                    // OK to close without prompting, do it immediately.
                    this.removeTab(tab, {
                      animate,
                      prewarmed: true,
                      skipPermitUnload: true,
                    });
                  }
                } else {
                  // We will need to prompt, queue it so it happens sequentially.
                  tabsWithBeforeUnloadPrompt.push(tab);
                }
              },
              err => {
                console.error("error while calling asyncPermitUnload", err);
                tab._pendingPermitUnload = false;
                TelemetryStopwatch.finish(
                  "FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS",
                  tab
                );
              }
            )
          );
        } else {
          tabsWithoutBeforeUnload.push(tab);
        }
      }

      // Now that all the beforeunload IPCs have been sent to content processes,
      // we can queue unload messages for all the tabs without beforeunload listeners.
      // Doing this first would cause content process main threads to be busy and delay
      // beforeunload responses, which would be user-visible.
      if (!skipRemoves) {
        for (let tab of tabsWithoutBeforeUnload) {
          this.removeTab(tab, {
            animate,
            prewarmed: true,
            skipPermitUnload,
          });
        }
      }

      return {
        beforeUnloadComplete: Promise.all(beforeUnloadPromises),
        tabsWithBeforeUnloadPrompt,
        lastToClose,
      };
    },

    /**
     * Runs the before unload handler for the provided tabs, waiting for them
     * to complete.
     *
     * This can be used in tandem with removeTabs to allow any before unload
     * prompts to happen before any tab closures. This should only be used
     * in the case where any prompts need to happen before other items before
     * the actual tabs are closed.
     *
     * When using this function alongside removeTabs, specify the `skipUnload`
     * option to removeTabs.
     *
     * @param {object[]} tabs
     *   An array of tabs to remove.
     * @returns {Promise<boolean>}
     *   Returns true if the unload has been blocked by the user. False if tabs
     *   may be subsequently closed.
     */
    async runBeforeUnloadForTabs(tabs) {
      try {
        let { beforeUnloadComplete, tabsWithBeforeUnloadPrompt } =
          this._startRemoveTabs(tabs, {
            animate: false,
            suppressWarnAboutClosingWindow: false,
            skipPermitUnload: false,
            skipRemoves: true,
          });

        await beforeUnloadComplete;

        // Now run again sequentially the beforeunload listeners that will result in a prompt.
        for (let tab of tabsWithBeforeUnloadPrompt) {
          tab._pendingPermitUnload = true;
          let { permitUnload } = this.getBrowserForTab(tab).permitUnload();
          tab._pendingPermitUnload = false;
          if (!permitUnload) {
            return true;
          }
        }
      } catch (e) {
        console.error(e);
      }
      return false;
    },

    /**
     * Removes multiple tabs from the tab browser.
     *
     * @param {object[]} tabs
     *   The set of tabs to remove.
     * @param {object} [options]
     * @param {boolean} [options.animate]
     *   Whether or not to animate closing, defaults to true.
     * @param {boolean} [options.suppressWarnAboutClosingWindow]
     *   This will supress the warning about closing a window with the last tab.
     * @param {boolean} [options.skipPermitUnload]
     *   Skips the before unload checks for the tabs. Only set this to true when
     *   using it in tandem with `runBeforeUnloadForTabs`.
     */
    removeTabs(
      tabs,
      {
        animate = true,
        suppressWarnAboutClosingWindow = false,
        skipPermitUnload = false,
      } = {}
    ) {
      // When 'closeWindowWithLastTab' pref is enabled, closing all tabs
      // can be considered equivalent to closing the window.
      if (
        this.tabs.length == tabs.length &&
        Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab")
      ) {
        window.closeWindow(
          true,
          suppressWarnAboutClosingWindow ? null : window.warnAboutClosingWindow,
          "close-last-tab"
        );
        return;
      }

      SessionStore.resetLastClosedTabCount(window);
      this._clearMultiSelectionLocked = true;

      // Guarantee that _clearMultiSelectionLocked lock gets released.
      try {
        let { beforeUnloadComplete, tabsWithBeforeUnloadPrompt, lastToClose } =
          this._startRemoveTabs(tabs, {
            animate,
            suppressWarnAboutClosingWindow,
            skipPermitUnload,
            skipRemoves: false,
          });

        // Wait for all the beforeunload events to have been processed by content processes.
        // The permitUnload() promise will, alas, not call its resolution
        // callbacks after the browser window the promise lives in has closed,
        // so we have to check for that case explicitly.
        let done = false;
        beforeUnloadComplete.then(() => {
          done = true;
        });
        Services.tm.spinEventLoopUntilOrQuit(
          "tabbrowser.js:removeTabs",
          () => done || window.closed
        );
        if (!done) {
          return;
        }

        let aParams = {
          animate,
          prewarmed: true,
          skipPermitUnload,
        };

        // Now run again sequentially the beforeunload listeners that will result in a prompt.
        for (let tab of tabsWithBeforeUnloadPrompt) {
          this.removeTab(tab, aParams);
          if (!tab.closing) {
            // If we abort the closing of the tab.
            tab._closedInGroup = false;
          }
        }

        // Avoid changing the selected browser several times by removing it,
        // if appropriate, lastly.
        if (lastToClose) {
          this.removeTab(lastToClose, aParams);
        }
      } catch (e) {
        console.error(e);
      }

      this._clearMultiSelectionLocked = false;
      this._avoidSingleSelectedTab();
      // Don't use document.l10n.setAttributes because the FTL file is loaded
      // lazily and we won't be able to resolve the string.
      document.getElementById("History:UndoCloseTab").setAttribute(
        "data-l10n-args",
        JSON.stringify({
          tabCount: SessionStore.getLastClosedTabCount(window),
        })
      );
    },

    removeCurrentTab(aParams) {
      this.removeTab(this.selectedTab, aParams);
    },

    removeTab(
      aTab,
      {
        animate,
        triggeringEvent,
        skipPermitUnload,
        closeWindowWithLastTab,
        prewarmed,
      } = {}
    ) {
      if (UserInteraction.running("browser.tabs.opening", window)) {
        UserInteraction.finish("browser.tabs.opening", window);
      }

      // Telemetry stopwatches may already be running if removeTab gets
      // called again for an already closing tab.
      if (
        !TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_ANIM_MS", aTab) &&
        !TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab)
      ) {
        // Speculatevely start both stopwatches now. We'll cancel one of
        // the two later depending on whether we're animating.
        TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
        TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
      }

      // Handle requests for synchronously removing an already
      // asynchronously closing tab.
      if (!animate && aTab.closing) {
        this._endRemoveTab(aTab);
        return;
      }

      let isLastTab = !aTab.hidden && this.visibleTabs.length == 1;
      // We have to sample the tab width now, since _beginRemoveTab might
      // end up modifying the DOM in such a way that aTab gets a new
      // frame created for it (for example, by updating the visually selected
      // state).
      let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;

      if (
        !this._beginRemoveTab(aTab, {
          closeWindowFastpath: true,
          skipPermitUnload,
          closeWindowWithLastTab,
          prewarmed,
        })
      ) {
        TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
        TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
        return;
      }

      let lockTabSizing =
        !aTab.pinned &&
        !aTab.hidden &&
        aTab._fullyOpen &&
        triggeringEvent?.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE &&
        triggeringEvent?.target.closest(".tabbrowser-tab");
      if (lockTabSizing) {
        this.tabContainer._lockTabSizing(aTab, tabWidth);
      } else {
        this.tabContainer._unlockTabSizing();
      }

      if (
        !animate /* the caller didn't opt in */ ||
        gReduceMotion ||
        isLastTab ||
        aTab.pinned ||
        aTab.hidden ||
        this._removingTabs.size >
          3 /* don't want lots of concurrent animations */ ||
        aTab.getAttribute("fadein") !=
          "true" /* fade-in transition hasn't been triggered yet */ ||
        tabWidth == 0 /* fade-in transition hasn't moved yet */
      ) {
        // We're not animating, so we can cancel the animation stopwatch.
        TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
        this._endRemoveTab(aTab);
        return;
      }

      // We're animating, so we can cancel the non-animation stopwatch.
      TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);

      aTab.style.maxWidth = ""; // ensure that fade-out transition happens
      aTab.removeAttribute("fadein");
      aTab.removeAttribute("bursting");

      setTimeout(
        function (tab, tabbrowser) {
          if (
            tab.container &&
            window.getComputedStyle(tab).maxWidth == "0.1px"
          ) {
            console.assert(
              false,
              "Giving up waiting for the tab closing animation to finish (bug 608589)"
            );
            tabbrowser._endRemoveTab(tab);
          }
        },
        3000,
        aTab,
        this
      );
    },

    _hasBeforeUnload(aTab) {
      let browser = aTab.linkedBrowser;
      if (browser.isRemoteBrowser && browser.frameLoader) {
        return browser.hasBeforeUnload;
      }
      return false;
    },

    _beginRemoveTab(
      aTab,
      {
        adoptedByTab,
        closeWindowWithLastTab,
        closeWindowFastpath,
        skipPermitUnload,
        prewarmed,
      } = {}
    ) {
      if (aTab.closing || this._windowIsClosing) {
        return false;
      }

      var browser = this.getBrowserForTab(aTab);
      if (
        !skipPermitUnload &&
        !adoptedByTab &&
        aTab.linkedPanel &&
        !aTab._pendingPermitUnload &&
        (!browser.isRemoteBrowser || this._hasBeforeUnload(aTab))
      ) {
        if (!prewarmed) {
          let blurTab = this._findTabToBlurTo(aTab);
          if (blurTab) {
            this.warmupTab(blurTab);
          }
        }

        TelemetryStopwatch.start("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);

        // We need to block while calling permitUnload() because it
        // processes the event queue and may lead to another removeTab()
        // call before permitUnload() returns.
        aTab._pendingPermitUnload = true;
        let { permitUnload } = browser.permitUnload();
        aTab._pendingPermitUnload = false;

        TelemetryStopwatch.finish("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);

        // If we were closed during onbeforeunload, we return false now
        // so we don't (try to) close the same tab again. Of course, we
        // also stop if the unload was cancelled by the user:
        if (aTab.closing || !permitUnload) {
          return false;
        }
      }

      // this._switcher would normally cover removing a tab from this
      // cache, but we may not have one at this time.
      let tabCacheIndex = this._tabLayerCache.indexOf(aTab);
      if (tabCacheIndex != -1) {
        this._tabLayerCache.splice(tabCacheIndex, 1);
      }

      // Delay hiding the the active tab if we're screen sharing.
      // See Bug 1642747.
      let screenShareInActiveTab =
        aTab == this.selectedTab && aTab._sharingState?.webRTC?.screen;

      if (!screenShareInActiveTab) {
        this._blurTab(aTab);
      }

      var closeWindow = false;
      var newTab = false;
      if (!aTab.hidden && this.visibleTabs.length == 1) {
        closeWindow =
          closeWindowWithLastTab != null
            ? closeWindowWithLastTab
            : !window.toolbar.visible ||
              Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");

        if (closeWindow) {
          // We've already called beforeunload on all the relevant tabs if we get here,
          // so avoid calling it again:
          window.skipNextCanClose = true;
        }

        // Closing the tab and replacing it with a blank one is notably slower
        // than closing the window right away. If the caller opts in, take
        // the fast path.
        if (closeWindow && closeWindowFastpath && !this._removingTabs.size) {
          // This call actually closes the window, unless the user
          // cancels the operation.  We are finished here in both cases.
          this._windowIsClosing = window.closeWindow(
            true,
            window.warnAboutClosingWindow,
            "close-last-tab"
          );
          return false;
        }

        newTab = true;
      }
      aTab._endRemoveArgs = [closeWindow, newTab];

      // swapBrowsersAndCloseOther will take care of closing the window without animation.
      if (closeWindow && adoptedByTab) {
        // Remove the tab's filter and progress listener to avoid leaking.
        if (aTab.linkedPanel) {
          const filter = this._tabFilters.get(aTab);
          browser.webProgress.removeProgressListener(filter);
          const listener = this._tabListeners.get(aTab);
          filter.removeProgressListener(listener);
          listener.destroy();
          this._tabListeners.delete(aTab);
          this._tabFilters.delete(aTab);
        }
        return true;
      }

      if (!aTab._fullyOpen) {
        // If the opening tab animation hasn't finished before we start closing the
        // tab, decrement the animation count since _handleNewTab will not get called.
        this.tabAnimationsInProgress--;
      }

      this.tabAnimationsInProgress++;

      // Mute audio immediately to improve perceived speed of tab closure.
      if (!adoptedByTab && aTab.hasAttribute("soundplaying")) {
        // Don't persist the muted state as this wasn't a user action.
        // This lets undo-close-tab return it to an unmuted state.
        aTab.linkedBrowser.mute(true);
      }

      aTab.closing = true;
      this._removingTabs.add(aTab);
      this.tabContainer._invalidateCachedTabs();

      // Invalidate hovered tab state tracking for this closing tab.
      aTab._mouseleave();

      if (newTab) {
        this.addTrustedTab(BROWSER_NEW_TAB_URL, {
          skipAnimation: true,
        });
      } else {
        TabBarVisibility.update();
      }

      // Splice this tab out of any lines of succession before any events are
      // dispatched.
      this.replaceInSuccession(aTab, aTab.successor);
      this.setSuccessor(aTab, null);

      // We're committed to closing the tab now.
      // Dispatch a notification.
      // We dispatch it before any teardown so that event listeners can
      // inspect the tab that's about to close.
      let evt = new CustomEvent("TabClose", {
        bubbles: true,
        detail: { adoptedBy: adoptedByTab },
      });
      aTab.dispatchEvent(evt);

      if (this.tabs.length == 2) {
        // We're closing one of our two open tabs, inform the other tab that its
        // sibling is going away.
        for (let tab of this.tabs) {
          let bc = tab.linkedBrowser.browsingContext;
          if (bc) {
            bc.hasSiblings = false;
          }
        }
      }

      let notificationBox = this.readNotificationBox(browser);
      notificationBox?._stack?.remove();

      if (aTab.linkedPanel) {
        if (!adoptedByTab && !gMultiProcessBrowser) {
          // Prevent this tab from showing further dialogs, since we're closing it
          browser.contentWindow.windowUtils.disableDialogs();
        }

        // Remove the tab's filter and progress listener.
        const filter = this._tabFilters.get(aTab);

        browser.webProgress.removeProgressListener(filter);

        const listener = this._tabListeners.get(aTab);
        filter.removeProgressListener(listener);
        listener.destroy();
      }

      if (browser.registeredOpenURI && !adoptedByTab) {
        let userContextId = browser.getAttribute("usercontextid") || 0;
        this.UrlbarProviderOpenTabs.unregisterOpenTab(
          browser.registeredOpenURI.spec,
          userContextId,
          PrivateBrowsingUtils.isWindowPrivate(window)
        );
        delete browser.registeredOpenURI;
      }

      // We are no longer the primary content area.
      browser.removeAttribute("primary");

      return true;
    },

    _endRemoveTab(aTab) {
      if (!aTab || !aTab._endRemoveArgs) {
        return;
      }

      var [aCloseWindow, aNewTab] = aTab._endRemoveArgs;
      aTab._endRemoveArgs = null;

      if (this._windowIsClosing) {
        aCloseWindow = false;
        aNewTab = false;
      }

      this.tabAnimationsInProgress--;

      this._lastRelatedTabMap = new WeakMap();

      // update the UI early for responsiveness
      aTab.collapsed = true;
      this._blurTab(aTab);

      this._removingTabs.delete(aTab);

      if (aCloseWindow) {
        this._windowIsClosing = true;
        for (let tab of this._removingTabs) {
          this._endRemoveTab(tab);
        }
      } else if (!this._windowIsClosing) {
        if (aNewTab) {
          gURLBar.select();
        }

        // workaround for bug 345399
        this.tabContainer.arrowScrollbox._updateScrollButtonsDisabledState();
      }

      // We're going to remove the tab and the browser now.
      this._tabFilters.delete(aTab);
      this._tabListeners.delete(aTab);

      var browser = this.getBrowserForTab(aTab);

      if (aTab.linkedPanel) {
        // Because of the fact that we are setting JS properties on
        // the browser elements, and we have code in place
        // to preserve the JS objects for any elements that have
        // JS properties set on them, the browser element won't be
        // destroyed until the document goes away.  So we force a
        // cleanup ourselves.
        // This has to happen before we remove the child since functions
        // like `getBrowserContainer` expect the browser to be parented.
        browser.destroy();
      }

      var wasPinned = aTab.pinned;

      // Remove the tab ...
      aTab.remove();
      this.tabContainer._invalidateCachedTabs();

      // Update hashiddentabs if this tab was hidden.
      if (aTab.hidden) {
        this.tabContainer._updateHiddenTabsStatus();
      }

      // ... and fix up the _tPos properties immediately.
      for (let i = aTab._tPos; i < this.tabs.length; i++) {
        this.tabs[i]._tPos = i;
      }

      if (!this._windowIsClosing) {
        if (wasPinned) {
          this.tabContainer._positionPinnedTabs();
        }

        // update tab close buttons state
        this.tabContainer._updateCloseButtons();

        setTimeout(
          function (tabs) {
            tabs._lastTabClosedByMouse = false;
          },
          0,
          this.tabContainer
        );
      }

      // update tab positional properties and attributes
      this.selectedTab._selected = true;

      // Removing the panel requires fixing up selectedPanel immediately
      // (see below), which would be hindered by the potentially expensive
      // browser removal. So we remove the browser and the panel in two
      // steps.

      var panel = this.getPanel(browser);

      // In the multi-process case, it's possible an asynchronous tab switch
      // is still underway. If so, then it's possible that the last visible
      // browser is the one we're in the process of removing. There's the
      // risk of displaying preloaded browsers that are at the end of the
      // deck if we remove the browser before the switch is complete, so
      // we alert the switcher in order to show a spinner instead.
      if (this._switcher) {
        this._switcher.onTabRemoved(aTab);
      }

      // This will unload the document. An unload handler could remove
      // dependant tabs, so it's important that the tabbrowser is now in
      // a consistent state (tab removed, tab positions updated, etc.).
      browser.remove();

      // Release the browser in case something is erroneously holding a
      // reference to the tab after its removal.
      this._tabForBrowser.delete(aTab.linkedBrowser);
      aTab.linkedBrowser = null;

      panel.remove();

      // closeWindow might wait an arbitrary length of time if we're supposed
      // to warn about closing the window, so we'll just stop the tab close
      // stopwatches here instead.
      TelemetryStopwatch.finish(
        "FX_TAB_CLOSE_TIME_ANIM_MS",
        aTab,
        true /* aCanceledOkay */
      );
      TelemetryStopwatch.finish(
        "FX_TAB_CLOSE_TIME_NO_ANIM_MS",
        aTab,
        true /* aCanceledOkay */
      );

      if (aCloseWindow) {
        this._windowIsClosing = closeWindow(
          true,
          window.warnAboutClosingWindow,
          "close-last-tab"
        );
      }
    },

    /**
     * Handles opening a new tab with mouse middleclick.
     * @param node
     * @param event
     *        The click event
     */
    handleNewTabMiddleClick(node, event) {
      // We should be using the disabled property here instead of the attribute,
      // but some elements that this function is used with don't support it (e.g.
      // menuitem).
      if (node.getAttribute("disabled") == "true") {
        return;
      } // Do nothing

      if (event.button == 1) {
        BrowserOpenTab({ event });
        // Stop the propagation of the click event, to prevent the event from being
        // handled more than once.
        // E.g. see https://bugzilla.mozilla.org/show_bug.cgi?id=1657992#c4
        event.stopPropagation();
        event.preventDefault();
      }
    },

    /**
     * Finds the tab that we will blur to if we blur aTab.
     * @param   aTab
     *          The tab we would blur
     * @param   aExcludeTabs
     *          Tabs to exclude from our search (i.e., because they are being
     *          closed along with aTab)
     */
    _findTabToBlurTo(aTab, aExcludeTabs = []) {
      if (!aTab.selected) {
        return null;
      }
      if (FirefoxViewHandler.tab) {
        aExcludeTabs.push(FirefoxViewHandler.tab);
      }

      let excludeTabs = new Set(aExcludeTabs);

      // If this tab has a successor, it should be selectable, since
      // hiding or closing a tab removes that tab as a successor.
      if (aTab.successor && !excludeTabs.has(aTab.successor)) {
        return aTab.successor;
      }

      if (
        aTab.owner &&
        !aTab.owner.hidden &&
        !aTab.owner.closing &&
        !excludeTabs.has(aTab.owner) &&
        Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
      ) {
        return aTab.owner;
      }

      // Switch to a visible tab unless there aren't any others remaining
      let remainingTabs = this.visibleTabs;
      let numTabs = remainingTabs.length;
      if (numTabs == 0 || (numTabs == 1 && remainingTabs[0] == aTab)) {
        remainingTabs = Array.prototype.filter.call(
          this.tabs,
          tab => !tab.closing && !excludeTabs.has(tab)
        );
      }

      // Try to find a remaining tab that comes after the given tab
      let tab = this.tabContainer.findNextTab(aTab, {
        direction: 1,
        filter: _tab => remainingTabs.includes(_tab),
      });

      if (!tab) {
        tab = this.tabContainer.findNextTab(aTab, {
          direction: -1,
          filter: _tab => remainingTabs.includes(_tab),
        });
      }

      return tab;
    },

    _blurTab(aTab) {
      this.selectedTab = this._findTabToBlurTo(aTab);
    },

    /**
     * @returns {boolean}
     *   False if swapping isn't permitted, true otherwise.
     */
    swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
      // Do not allow transfering a private tab to a non-private window
      // and vice versa.
      if (
        PrivateBrowsingUtils.isWindowPrivate(window) !=
        PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerGlobal)
      ) {
        return false;
      }

      // Do not allow transfering a useRemoteSubframes tab to a
      // non-useRemoteSubframes window and vice versa.
      if (gFissionBrowser != aOtherTab.ownerGlobal.gFissionBrowser) {
        return false;
      }

      let ourBrowser = this.getBrowserForTab(aOurTab);
      let otherBrowser = aOtherTab.linkedBrowser;

      // Can't swap between chrome and content processes.
      if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser) {
        return false;
      }

      // Keep the userContextId if set on other browser
      if (otherBrowser.hasAttribute("usercontextid")) {
        ourBrowser.setAttribute(
          "usercontextid",
          otherBrowser.getAttribute("usercontextid")
        );
      }

      // That's gBrowser for the other window, not the tab's browser!
      var remoteBrowser = aOtherTab.ownerGlobal.gBrowser;
      var isPending = aOtherTab.hasAttribute("pending");

      let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab);
      let stateFlags = 0;
      if (otherTabListener) {
        stateFlags = otherTabListener.mStateFlags;
      }

      // Expedite the removal of the icon if it was already scheduled.
      if (aOtherTab._soundPlayingAttrRemovalTimer) {
        clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer);
        aOtherTab._soundPlayingAttrRemovalTimer = 0;
        aOtherTab.removeAttribute("soundplaying");
        remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]);
      }

      // First, start teardown of the other browser.  Make sure to not
      // fire the beforeunload event in the process.  Close the other
      // window if this was its last tab.
      if (
        !remoteBrowser._beginRemoveTab(aOtherTab, {
          adoptedByTab: aOurTab,
          closeWindowWithLastTab: true,
        })
      ) {
        return false;
      }

      // If this is the last tab of the window, hide the window
      // immediately without animation before the docshell swap, to avoid
      // about:blank being painted.
      let [closeWindow] = aOtherTab._endRemoveArgs;
      if (closeWindow) {
        let win = aOtherTab.ownerGlobal;
        win.windowUtils.suppressAnimation(true);
        // Only suppressing window animations isn't enough to avoid
        // an empty content area being painted.
        let baseWin = win.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
        baseWin.visibility = false;
      }

      let modifiedAttrs = [];
      if (aOtherTab.hasAttribute("muted")) {
        aOurTab.setAttribute("muted", "true");
        aOurTab.muteReason = aOtherTab.muteReason;
        // For non-lazy tabs, mute() must be called.
        if (aOurTab.linkedPanel) {
          ourBrowser.mute();
        }
        modifiedAttrs.push("muted");
      }
      if (aOtherTab.hasAttribute("undiscardable")) {
        aOurTab.setAttribute("undiscardable", "true");
        modifiedAttrs.push("undiscardable");
      }
      if (aOtherTab.hasAttribute("soundplaying")) {
        aOurTab.setAttribute("soundplaying", "true");
        modifiedAttrs.push("soundplaying");
      }
      if (aOtherTab.hasAttribute("usercontextid")) {
        aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid"));
        modifiedAttrs.push("usercontextid");
      }
      if (aOtherTab.hasAttribute("sharing")) {
        aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
        modifiedAttrs.push("sharing");
        aOurTab._sharingState = aOtherTab._sharingState;
        webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
      }
      if (aOtherTab.hasAttribute("pictureinpicture")) {
        aOurTab.setAttribute("pictureinpicture", true);
        modifiedAttrs.push("pictureinpicture");

        let event = new CustomEvent("TabSwapPictureInPicture", {
          detail: aOurTab,
        });
        aOtherTab.dispatchEvent(event);
      }

      SitePermissions.copyTemporaryPermissions(otherBrowser, ourBrowser);

      // If the other tab is pending (i.e. has not been restored, yet)
      // then do not switch docShells but retrieve the other tab's state
      // and apply it to our tab.
      if (isPending) {
        // Tag tab so that the extension framework can ignore tab events that
        // are triggered amidst the tab/browser restoration process
        // (TabHide, TabPinned, TabUnpinned, "muted" attribute changes, etc.).
        aOurTab.initializingTab = true;
        delete ourBrowser._cachedCurrentURI;
        SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab));
        delete aOurTab.initializingTab;

        // Make sure to unregister any open URIs.
        this._swapRegisteredOpenURIs(ourBrowser, otherBrowser);
      } else {
        // Workarounds for bug 458697
        // Icon might have been set on DOMLinkAdded, don't override that.
        if (!ourBrowser.mIconURL && otherBrowser.mIconURL) {
          this.setIcon(aOurTab, otherBrowser.mIconURL);
        }
        var isBusy = aOtherTab.hasAttribute("busy");
        if (isBusy) {
          aOurTab.setAttribute("busy", "true");
          modifiedAttrs.push("busy");
          if (aOurTab.selected) {
            this._isBusy = true;
          }
        }

        this._swapBrowserDocShells(aOurTab, otherBrowser, stateFlags);
      }

      // Unregister the previously opened URI
      if (otherBrowser.registeredOpenURI) {
        let userContextId = otherBrowser.getAttribute("usercontextid") || 0;
        this.UrlbarProviderOpenTabs.unregisterOpenTab(
          otherBrowser.registeredOpenURI.spec,
          userContextId,
          PrivateBrowsingUtils.isWindowPrivate(window)
        );
        delete otherBrowser.registeredOpenURI;
      }

      // Handle findbar data (if any)
      let otherFindBar = aOtherTab._findBar;
      if (otherFindBar && otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
        let oldValue = otherFindBar._findField.value;
        let wasHidden = otherFindBar.hidden;
        let ourFindBarPromise = this.getFindBar(aOurTab);
        ourFindBarPromise.then(ourFindBar => {
          if (!ourFindBar) {
            return;
          }
          ourFindBar._findField.value = oldValue;
          if (!wasHidden) {
            ourFindBar.onFindCommand();
          }
        });
      }

      // Finish tearing down the tab that's going away.
      if (closeWindow) {
        aOtherTab.ownerGlobal.close();
      } else {
        remoteBrowser._endRemoveTab(aOtherTab);
      }

      this.setTabTitle(aOurTab);

      // If the tab was already selected (this happens in the scenario
      // of replaceTabWithWindow), notify onLocationChange, etc.
      if (aOurTab.selected) {
        this.updateCurrentBrowser(true);
      }

      if (modifiedAttrs.length) {
        this._tabAttrModified(aOurTab, modifiedAttrs);
      }

      return true;
    },

    swapBrowsers(aOurTab, aOtherTab) {
      let otherBrowser = aOtherTab.linkedBrowser;
      let otherTabBrowser = otherBrowser.getTabBrowser();

      // We aren't closing the other tab so, we also need to swap its tablisteners.
      let filter = otherTabBrowser._tabFilters.get(aOtherTab);
      let tabListener = otherTabBrowser._tabListeners.get(aOtherTab);
      otherBrowser.webProgress.removeProgressListener(filter);
      filter.removeProgressListener(tabListener);

      // Perform the docshell swap through the common mechanism.
      this._swapBrowserDocShells(aOurTab, otherBrowser);

      // Restore the listeners for the swapped in tab.
      tabListener = new otherTabBrowser.ownerGlobal.TabProgressListener(
        aOtherTab,
        otherBrowser,
        false,
        false
      );
      otherTabBrowser._tabListeners.set(aOtherTab, tabListener);

      const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
      filter.addProgressListener(tabListener, notifyAll);
      otherBrowser.webProgress.addProgressListener(filter, notifyAll);
    },

    _swapBrowserDocShells(aOurTab, aOtherBrowser, aStateFlags) {
      // aOurTab's browser needs to be inserted now if it hasn't already.
      this._insertBrowser(aOurTab);

      // Unhook our progress listener
      const filter = this._tabFilters.get(aOurTab);
      let tabListener = this._tabListeners.get(aOurTab);
      let ourBrowser = this.getBrowserForTab(aOurTab);
      ourBrowser.webProgress.removeProgressListener(filter);
      filter.removeProgressListener(tabListener);

      // Make sure to unregister any open URIs.
      this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);

      let remoteBrowser = aOtherBrowser.ownerGlobal.gBrowser;

      // If switcher is active, it will intercept swap events and
      // react as needed.
      if (!this._switcher) {
        aOtherBrowser.docShellIsActive =
          this.shouldActivateDocShell(ourBrowser);
      }

      // Swap the docshells
      ourBrowser.swapDocShells(aOtherBrowser);

      // Swap permanentKey properties.
      let ourPermanentKey = ourBrowser.permanentKey;
      ourBrowser.permanentKey = aOtherBrowser.permanentKey;
      aOtherBrowser.permanentKey = ourPermanentKey;
      aOurTab.permanentKey = ourBrowser.permanentKey;
      if (remoteBrowser) {
        let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser);
        if (otherTab) {
          otherTab.permanentKey = aOtherBrowser.permanentKey;
        }
      }

      // Restore the progress listener
      tabListener = new TabProgressListener(
        aOurTab,
        ourBrowser,
        false,
        false,
        aStateFlags
      );
      this._tabListeners.set(aOurTab, tabListener);

      const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
      filter.addProgressListener(tabListener, notifyAll);
      ourBrowser.webProgress.addProgressListener(filter, notifyAll);
    },

    _swapRegisteredOpenURIs(aOurBrowser, aOtherBrowser) {
      // Swap the registeredOpenURI properties of the two browsers
      let tmp = aOurBrowser.registeredOpenURI;
      delete aOurBrowser.registeredOpenURI;
      if (aOtherBrowser.registeredOpenURI) {
        aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI;
        delete aOtherBrowser.registeredOpenURI;
      }
      if (tmp) {
        aOtherBrowser.registeredOpenURI = tmp;
      }
    },

    reloadMultiSelectedTabs() {
      this.reloadTabs(this.selectedTabs);
    },

    reloadTabs(tabs) {
      for (let tab of tabs) {
        try {
          this.getBrowserForTab(tab).reload();
        } catch (e) {
          // ignore failure to reload so others will be reloaded
        }
      }
    },

    reloadTab(aTab) {
      let browser = this.getBrowserForTab(aTab);
      // Reset temporary permissions on the current tab. This is done here
      // because we only want to reset permissions on user reload.
      SitePermissions.clearTemporaryBlockPermissions(browser);
      // Also reset DOS mitigations for the basic auth prompt on reload.
      delete browser.authPromptAbuseCounter;
      gIdentityHandler.hidePopup();
      gPermissionPanel.hidePopup();
      browser.reload();
    },

    addProgressListener(aListener) {
      if (arguments.length != 1) {
        console.error(
          "gBrowser.addProgressListener was " +
            "called with a second argument, " +
            "which is not supported. See bug " +
            "608628. Call stack: ",
          new Error().stack
        );
      }

      this.mProgressListeners.push(aListener);
    },

    removeProgressListener(aListener) {
      this.mProgressListeners = this.mProgressListeners.filter(
        l => l != aListener
      );
    },

    addTabsProgressListener(aListener) {
      this.mTabsProgressListeners.push(aListener);
    },

    removeTabsProgressListener(aListener) {
      this.mTabsProgressListeners = this.mTabsProgressListeners.filter(
        l => l != aListener
      );
    },

    getBrowserForTab(aTab) {
      return aTab.linkedBrowser;
    },

    showOnlyTheseTabs(aTabs) {
      for (let tab of this.tabs) {
        if (!aTabs.includes(tab)) {
          this.hideTab(tab);
        } else {
          this.showTab(tab);
        }
      }

      this.tabContainer._updateHiddenTabsStatus();
      this.tabContainer._handleTabSelect(true);
    },

    showTab(aTab) {
      if (!aTab.hidden || aTab == FirefoxViewHandler.tab) {
        return;
      }
      aTab.removeAttribute("hidden");
      this.tabContainer._invalidateCachedVisibleTabs();

      this.tabContainer._updateCloseButtons();
      this.tabContainer._updateHiddenTabsStatus();

      let event = document.createEvent("Events");
      event.initEvent("TabShow", true, false);
      aTab.dispatchEvent(event);
      SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
    },

    hideTab(aTab, aSource) {
      if (
        aTab.hidden ||
        aTab.pinned ||
        aTab.selected ||
        aTab.closing ||
        // Tabs that are sharing the screen, microphone or camera cannot be hidden.
        aTab._sharingState?.webRTC?.sharing
      ) {
        return;
      }
      aTab.setAttribute("hidden", "true");
      this.tabContainer._invalidateCachedVisibleTabs();

      this.tabContainer._updateCloseButtons();
      this.tabContainer._updateHiddenTabsStatus();

      // Splice this tab out of any lines of succession before any events are
      // dispatched.
      this.replaceInSuccession(aTab, aTab.successor);
      this.setSuccessor(aTab, null);

      let event = document.createEvent("Events");
      event.initEvent("TabHide", true, false);
      aTab.dispatchEvent(event);
      if (aSource) {
        SessionStore.setCustomTabValue(aTab, "hiddenBy", aSource);
      }
    },

    selectTabAtIndex(aIndex, aEvent) {
      let tabs = this.visibleTabs;

      // count backwards for aIndex < 0
      if (aIndex < 0) {
        aIndex += tabs.length;
        // clamp at index 0 if still negative.
        if (aIndex < 0) {
          aIndex = 0;
        }
      } else if (aIndex >= tabs.length) {
        // clamp at right-most tab if out of range.
        aIndex = tabs.length - 1;
      }

      this.selectedTab = tabs[aIndex];

      if (aEvent) {
        aEvent.preventDefault();
        aEvent.stopPropagation();
      }
    },

    /**
     * Moves a tab to a new browser window, unless it's already the only tab
     * in the current window, in which case this will do nothing.
     */
    replaceTabWithWindow(aTab, aOptions) {
      if (this.tabs.length == 1) {
        return null;
      }

      var options = "chrome,dialog=no,all";
      for (var name in aOptions) {
        options += "," + name + "=" + aOptions[name];
      }

      if (PrivateBrowsingUtils.isWindowPrivate(window)) {
        options += ",private=1";
      }

      // Play the tab closing animation to give immediate feedback while
      // waiting for the new window to appear.
      if (!gReduceMotion) {
        aTab.style.maxWidth = ""; // ensure that fade-out transition happens
        aTab.removeAttribute("fadein");
      }

      // tell a new window to take the "dropped" tab
      return window.openDialog(
        AppConstants.BROWSER_CHROME_URL,
        "_blank",
        options,
        aTab
      );
    },

    /**
     * Move contextTab (or selected tabs in a mutli-select context)
     * to a new browser window, unless it is (they are) already the only tab(s)
     * in the current window, in which case this will do nothing.
     */
    replaceTabsWithWindow(contextTab, aOptions = {}) {
      let tabs;
      if (contextTab.multiselected) {
        tabs = this.selectedTabs;
      } else {
        tabs = [contextTab];
      }

      if (this.tabs.length == tabs.length) {
        return null;
      }

      if (tabs.length == 1) {
        return this.replaceTabWithWindow(tabs[0], aOptions);
      }

      // Play the closing animation for all selected tabs to give
      // immediate feedback while waiting for the new window to appear.
      if (!gReduceMotion) {
        for (let tab of tabs) {
          tab.style.maxWidth = ""; // ensure that fade-out transition happens
          tab.removeAttribute("fadein");
        }
      }

      // Create a new window and make it adopt the tabs, preserving their relative order.
      // The initial tab of the new window will be selected, so it should adopt the
      // selected tab of the original window, if applicable, or else the first moving tab.
      // This avoids tab-switches in the new window, preserving tab laziness.
      // However, to avoid multiple tab-switches in the original window, the other tabs
      // should be adopted before the selected one.
      let { selectedTab } = gBrowser;
      if (!tabs.includes(selectedTab)) {
        selectedTab = tabs[0];
      }
      let win = this.replaceTabWithWindow(selectedTab, aOptions);
      win.addEventListener(
        "before-initial-tab-adopted",
        () => {
          let index = 0;
          for (let tab of tabs) {
            if (tab !== selectedTab) {
              const newTab = win.gBrowser.adoptTab(tab, index);
              if (!newTab) {
                // The adoption failed. Restore "fadein" and don't increase the index.
                tab.setAttribute("fadein", "true");
                continue;
              }
            }
            ++index;
          }
          // Restore tab selection
          let winVisibleTabs = win.gBrowser.visibleTabs;
          let winTabLength = winVisibleTabs.length;
          win.gBrowser.addRangeToMultiSelectedTabs(
            winVisibleTabs[0],
            winVisibleTabs[winTabLength - 1]
          );
          win.gBrowser.lockClearMultiSelectionOnce();
        },
        { once: true }
      );
      return win;
    },

    _updateTabsAfterInsert() {
      for (let i = 0; i < this.tabs.length; i++) {
        this.tabs[i]._tPos = i;
        this.tabs[i]._selected = false;
      }

      // If we're in the midst of an async tab switch while calling
      // moveTabTo, we can get into a case where _visuallySelected
      // is set to true on two different tabs.
      //
      // What we want to do in moveTabTo is to remove logical selection
      // from all tabs, and then re-add logical selection to selectedTab
      // (and visual selection as well if we're not running with e10s, which
      // setting _selected will do automatically).
      //
      // If we're running with e10s, then the visual selection will not
      // be changed, which is fine, since if we weren't in the midst of a
      // tab switch, the previously visually selected tab should still be
      // correct, and if we are in the midst of a tab switch, then the async
      // tab switcher will set the visually selected tab once the tab switch
      // has completed.
      this.selectedTab._selected = true;
    },

    moveTabTo(aTab, aIndex, aKeepRelatedTabs) {
      var oldPosition = aTab._tPos;
      if (oldPosition == aIndex) {
        return;
      }

      // Don't allow mixing pinned and unpinned tabs.
      if (aTab.pinned) {
        aIndex = Math.min(aIndex, this._numPinnedTabs - 1);
      } else {
        aIndex = Math.max(aIndex, this._numPinnedTabs);
      }
      if (oldPosition == aIndex) {
        return;
      }

      if (!aKeepRelatedTabs) {
        this._lastRelatedTabMap = new WeakMap();
      }

      let wasFocused = document.activeElement == this.selectedTab;

      aIndex = aIndex < aTab._tPos ? aIndex : aIndex + 1;

      let neighbor = this.tabs[aIndex] || null;
      this.tabContainer._invalidateCachedTabs();
      this.tabContainer.insertBefore(aTab, neighbor);
      this._updateTabsAfterInsert();

      if (wasFocused) {
        this.selectedTab.focus();
      }

      this.tabContainer._handleTabSelect(true);

      if (aTab.pinned) {
        this.tabContainer._positionPinnedTabs();
      }

      var evt = document.createEvent("UIEvents");
      evt.initUIEvent("TabMove", true, false, window, oldPosition);
      aTab.dispatchEvent(evt);
    },

    moveTabForward() {
      let nextTab = this.tabContainer.findNextTab(this.selectedTab, {
        direction: 1,
        filter: tab => !tab.hidden,
      });

      if (nextTab) {
        this.moveTabTo(this.selectedTab, nextTab._tPos);
      } else if (this.arrowKeysShouldWrap) {
        this.moveTabToStart();
      }
    },

    /**
     * Adopts a tab from another browser window, and inserts it at aIndex
     *
     * @returns {object}
     *    The new tab in the current window, null if the tab couldn't be adopted.
     */
    adoptTab(aTab, aIndex, aSelectTab) {
      // Swap the dropped tab with a new one we create and then close
      // it in the other window (making it seem to have moved between
      // windows). We also ensure that the tab we create to swap into has
      // the same remote type and process as the one we're swapping in.
      // This makes sure we don't get a short-lived process for the new tab.
      let linkedBrowser = aTab.linkedBrowser;
      let createLazyBrowser = !aTab.linkedPanel;
      let params = {
        eventDetail: { adoptedTab: aTab },
        preferredRemoteType: linkedBrowser.remoteType,
        initialBrowsingContextGroupId: linkedBrowser.browsingContext?.group.id,
        skipAnimation: true,
        index: aIndex,
        createLazyBrowser,
      };

      let numPinned = this._numPinnedTabs;
      if (aIndex < numPinned || (aTab.pinned && aIndex == numPinned)) {
        params.pinned = true;
      }

      if (aTab.hasAttribute("usercontextid")) {
        // new tab must have the same usercontextid as the old one
        params.userContextId = aTab.getAttribute("usercontextid");
      }
      let newTab = this.addWebTab("about:blank", params);
      let newBrowser = this.getBrowserForTab(newTab);

      aTab.container._finishAnimateTabMove();

      if (!createLazyBrowser) {
        // Stop the about:blank load.
        newBrowser.stop();
        // Make sure it has a docshell.
        newBrowser.docShell;
      }

      if (!this.swapBrowsersAndCloseOther(newTab, aTab)) {
        // Swapping wasn't permitted. Bail out.
        this.removeTab(newTab);
        return null;
      }

      if (aSelectTab) {
        this.selectedTab = newTab;
      }

      return newTab;
    },

    moveTabBackward() {
      let previousTab = this.tabContainer.findNextTab(this.selectedTab, {
        direction: -1,
        filter: tab => !tab.hidden,
      });

      if (previousTab) {
        this.moveTabTo(this.selectedTab, previousTab._tPos);
      } else if (this.arrowKeysShouldWrap) {
        this.moveTabToEnd();
      }
    },

    moveTabToStart() {
      let tabPos = this.selectedTab._tPos;
      if (tabPos > 0) {
        this.moveTabTo(this.selectedTab, 0);
      }
    },

    moveTabToEnd() {
      let tabPos = this.selectedTab._tPos;
      if (tabPos < this.browsers.length - 1) {
        this.moveTabTo(this.selectedTab, this.browsers.length - 1);
      }
    },

    moveTabOver(aEvent) {
      if (
        (!RTL_UI && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) ||
        (RTL_UI && aEvent.keyCode == KeyEvent.DOM_VK_LEFT)
      ) {
        this.moveTabForward();
      } else {
        this.moveTabBackward();
      }
    },

    /**
     * @param   aTab
     *          Can be from a different window as well
     * @param   aRestoreTabImmediately
     *          Can defer loading of the tab contents
     * @param   aOptions
     *          The new index of the tab
     */
    duplicateTab(aTab, aRestoreTabImmediately, aOptions) {
      return SessionStore.duplicateTab(
        window,
        aTab,
        0,
        aRestoreTabImmediately,
        aOptions
      );
    },

    addToMultiSelectedTabs(aTab) {
      if (aTab.multiselected) {
        return;
      }

      aTab.setAttribute("multiselected", "true");
      aTab.setAttribute("aria-selected", "true");
      this._multiSelectedTabsSet.add(aTab);
      this._startMultiSelectChange();
      if (this._multiSelectChangeRemovals.has(aTab)) {
        this._multiSelectChangeRemovals.delete(aTab);
      } else {
        this._multiSelectChangeAdditions.add(aTab);
      }
    },

    /**
     * Adds two given tabs and all tabs between them into the (multi) selected tabs collection
     */
    addRangeToMultiSelectedTabs(aTab1, aTab2) {
      if (aTab1 == aTab2) {
        return;
      }

      const tabs = this.visibleTabs;
      const indexOfTab1 = tabs.indexOf(aTab1);
      const indexOfTab2 = tabs.indexOf(aTab2);

      const [lowerIndex, higherIndex] =
        indexOfTab1 < indexOfTab2
          ? [Math.max(0, indexOfTab1), indexOfTab2]
          : [Math.max(0, indexOfTab2), indexOfTab1];

      for (let i = lowerIndex; i <= higherIndex; i++) {
        this.addToMultiSelectedTabs(tabs[i]);
      }
    },

    removeFromMultiSelectedTabs(aTab) {
      if (!aTab.multiselected) {
        return;
      }
      aTab.removeAttribute("multiselected");
      aTab.removeAttribute("aria-selected");
      this._multiSelectedTabsSet.delete(aTab);
      this._startMultiSelectChange();
      if (this._multiSelectChangeAdditions.has(aTab)) {
        this._multiSelectChangeAdditions.delete(aTab);
      } else {
        this._multiSelectChangeRemovals.add(aTab);
      }
    },

    clearMultiSelectedTabs() {
      if (this._clearMultiSelectionLocked) {
        if (this._clearMultiSelectionLockedOnce) {
          this._clearMultiSelectionLockedOnce = false;
          this._clearMultiSelectionLocked = false;
        }
        return;
      }

      if (this.multiSelectedTabsCount < 1) {
        return;
      }

      for (let tab of this.selectedTabs) {
        this.removeFromMultiSelectedTabs(tab);
      }
      this._lastMultiSelectedTabRef = null;
    },

    selectAllTabs() {
      let visibleTabs = this.visibleTabs;
      gBrowser.addRangeToMultiSelectedTabs(
        visibleTabs[0],
        visibleTabs[visibleTabs.length - 1]
      );
    },

    allTabsSelected() {
      return (
        this.visibleTabs.length == 1 ||
        this.visibleTabs.every(t => t.multiselected)
      );
    },

    lockClearMultiSelectionOnce() {
      this._clearMultiSelectionLockedOnce = true;
      this._clearMultiSelectionLocked = true;
    },

    unlockClearMultiSelection() {
      this._clearMultiSelectionLockedOnce = false;
      this._clearMultiSelectionLocked = false;
    },

    /**
     * Remove a tab from the multiselection if it's the only one left there.
     *
     * In fact, some scenario may lead to only one single tab multi-selected,
     * this is something to avoid (Chrome does the same)
     * Consider 4 tabs A,B,C,D with A having the focus
     * 1. select C with Ctrl
     * 2. Right-click on B and "Close Tabs to The Right"
     *
     * Expected result
     * C and D closing
     * A being the only multi-selected tab, selection should be cleared
     *
     *
     * Single selected tab could even happen with a none-focused tab.
     * For exemple with the menu "Close other tabs", it could happen
     * with a multi-selected pinned tab.
     * For illustration, consider 4 tabs A,B,C,D with B active
     * 1. pin A and Ctrl-select it
     * 2. Ctrl-select C
     * 3. right-click on D and click "Close Other Tabs"
     *
     * Expected result
     * B and C closing
     * A[pinned] being the only multi-selected tab, selection should be cleared.
     */
    _avoidSingleSelectedTab() {
      if (this.multiSelectedTabsCount == 1) {
        this.clearMultiSelectedTabs();
      }
    },

    _switchToNextMultiSelectedTab() {
      this._clearMultiSelectionLocked = true;

      // Guarantee that _clearMultiSelectionLocked lock gets released.
      try {
        let lastMultiSelectedTab = this.lastMultiSelectedTab;
        if (!lastMultiSelectedTab.selected) {
          this.selectedTab = lastMultiSelectedTab;
        } else {
          let selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(
            this._multiSelectedTabsSet
          ).filter(this._mayTabBeMultiselected);
          this.selectedTab = selectedTabs.at(-1);
        }
      } catch (e) {
        console.error(e);
      }

      this._clearMultiSelectionLocked = false;
    },

    set selectedTabs(tabs) {
      this.clearMultiSelectedTabs();
      this.selectedTab = tabs[0];
      if (tabs.length > 1) {
        for (let tab of tabs) {
          this.addToMultiSelectedTabs(tab);
        }
      }
    },

    get selectedTabs() {
      let { selectedTab, _multiSelectedTabsSet } = this;
      let tabs = ChromeUtils.nondeterministicGetWeakSetKeys(
        _multiSelectedTabsSet
      ).filter(this._mayTabBeMultiselected);
      if (
        (!_multiSelectedTabsSet.has(selectedTab) &&
          this._mayTabBeMultiselected(selectedTab)) ||
        !tabs.length
      ) {
        tabs.push(selectedTab);
      }
      return tabs.sort((a, b) => a._tPos > b._tPos);
    },

    get multiSelectedTabsCount() {
      return ChromeUtils.nondeterministicGetWeakSetKeys(
        this._multiSelectedTabsSet
      ).filter(this._mayTabBeMultiselected).length;
    },

    get lastMultiSelectedTab() {
      let tab = this._lastMultiSelectedTabRef
        ? this._lastMultiSelectedTabRef.get()
        : null;
      if (tab && tab.isConnected && this._multiSelectedTabsSet.has(tab)) {
        return tab;
      }
      let selectedTab = this.selectedTab;
      this.lastMultiSelectedTab = selectedTab;
      return selectedTab;
    },

    set lastMultiSelectedTab(aTab) {
      this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab);
    },

    _mayTabBeMultiselected(aTab) {
      return aTab.isConnected && !aTab.closing && !aTab.hidden;
    },

    _startMultiSelectChange() {
      if (!this._multiSelectChangeStarted) {
        this._multiSelectChangeStarted = true;
        Promise.resolve().then(() => this._endMultiSelectChange());
      }
    },

    _endMultiSelectChange() {
      let noticeable = false;
      let { selectedTab } = this;
      if (this._multiSelectChangeAdditions.size) {
        if (!selectedTab.multiselected) {
          this.addToMultiSelectedTabs(selectedTab);
        }
        noticeable = true;
      }
      if (this._multiSelectChangeRemovals.size) {
        if (this._multiSelectChangeRemovals.has(selectedTab)) {
          this._switchToNextMultiSelectedTab();
        }
        this._avoidSingleSelectedTab();
        noticeable = true;
      }
      this._multiSelectChangeStarted = false;
      if (noticeable || this._multiSelectChangeSelected) {
        this._multiSelectChangeSelected = false;
        this._multiSelectChangeAdditions.clear();
        this._multiSelectChangeRemovals.clear();
        this.dispatchEvent(
          new CustomEvent("TabMultiSelect", { bubbles: true })
        );
      }
    },

    toggleMuteAudioOnMultiSelectedTabs(aTab) {
      let tabMuted = aTab.linkedBrowser.audioMuted;
      let tabsToToggle = this.selectedTabs.filter(
        tab => tab.linkedBrowser.audioMuted == tabMuted
      );
      for (let tab of tabsToToggle) {
        tab.toggleMuteAudio();
      }
    },

    resumeDelayedMediaOnMultiSelectedTabs() {
      for (let tab of this.selectedTabs) {
        tab.resumeDelayedMedia();
      }
    },

    pinMultiSelectedTabs() {
      for (let tab of this.selectedTabs) {
        this.pinTab(tab);
      }
    },

    unpinMultiSelectedTabs() {
      // The selectedTabs getter returns the tabs
      // in visual order. We need to unpin in reverse
      // order to maintain visual order.
      let selectedTabs = this.selectedTabs;
      for (let i = selectedTabs.length - 1; i >= 0; i--) {
        let tab = selectedTabs[i];
        this.unpinTab(tab);
      }
    },

    activateBrowserForPrintPreview(aBrowser) {
      this._printPreviewBrowsers.add(aBrowser);
      if (this._switcher) {
        this._switcher.activateBrowserForPrintPreview(aBrowser);
      }
      aBrowser.docShellIsActive = true;
    },

    deactivatePrintPreviewBrowsers() {
      let browsers = this._printPreviewBrowsers;
      this._printPreviewBrowsers = new Set();
      for (let browser of browsers) {
        browser.docShellIsActive = this.shouldActivateDocShell(browser);
      }
    },

    /**
     * Returns true if a given browser's docshell should be active.
     */
    shouldActivateDocShell(aBrowser) {
      if (this._switcher) {
        return this._switcher.shouldActivateDocShell(aBrowser);
      }
      return (
        (aBrowser == this.selectedBrowser && !document.hidden) ||
        this._printPreviewBrowsers.has(aBrowser) ||
        this.PictureInPicture.isOriginatingBrowser(aBrowser)
      );
    },

    _getSwitcher() {
      if (!this._switcher) {
        this._switcher = new this.AsyncTabSwitcher(this);
      }
      return this._switcher;
    },

    warmupTab(aTab) {
      if (gMultiProcessBrowser) {
        this._getSwitcher().warmupTab(aTab);
      }
    },

    /**
     * _maybeRequestReplyFromRemoteContent may call
     * aEvent.requestReplyFromRemoteContent if necessary.
     *
     * @param aEvent    The handling event.
     * @return          true if the handler should wait a reply event.
     *                  false if the handle can handle the immediately.
     */
    _maybeRequestReplyFromRemoteContent(aEvent) {
      if (aEvent.defaultPrevented) {
        return false;
      }
      // If the event target is a remote browser, and the event has not been
      // handled by the remote content yet, we should wait a reply event
      // from the content.
      if (aEvent.isWaitingReplyFromRemoteContent) {
        return true; // Somebody called requestReplyFromRemoteContent already.
      }
      if (
        !aEvent.isReplyEventFromRemoteContent &&
        aEvent.target?.isRemoteBrowser === true
      ) {
        aEvent.requestReplyFromRemoteContent();
        return true;
      }
      return false;
    },

    _handleKeyDownEvent(aEvent) {
      if (!aEvent.isTrusted) {
        // Don't let untrusted events mess with tabs.
        return;
      }

      // Skip this only if something has explicitly cancelled it.
      if (aEvent.defaultCancelled) {
        return;
      }

      // Skip if chrome code has cancelled this:
      if (aEvent.defaultPreventedByChrome) {
        return;
      }

      // Don't check if the event was already consumed because tab
      // navigation should always work for better user experience.

      switch (ShortcutUtils.getSystemActionForEvent(aEvent)) {
        case ShortcutUtils.TOGGLE_CARET_BROWSING:
          this._maybeRequestReplyFromRemoteContent(aEvent);
          return;
        case ShortcutUtils.MOVE_TAB_BACKWARD:
          this.moveTabBackward();
          aEvent.preventDefault();
          return;
        case ShortcutUtils.MOVE_TAB_FORWARD:
          this.moveTabForward();
          aEvent.preventDefault();
          return;
        case ShortcutUtils.CLOSE_TAB:
          if (gBrowser.multiSelectedTabsCount) {
            gBrowser.removeMultiSelectedTabs();
          } else if (!this.selectedTab.pinned) {
            this.removeCurrentTab({ animate: true });
          }
          aEvent.preventDefault();
      }
    },

    toggleCaretBrowsing() {
      const kPrefShortcutEnabled =
        "accessibility.browsewithcaret_shortcut.enabled";
      const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret";
      const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";

      var isEnabled = Services.prefs.getBoolPref(kPrefShortcutEnabled);
      if (!isEnabled || this._awaitingToggleCaretBrowsingPrompt) {
        return;
      }

      // Toggle browse with caret mode
      var browseWithCaretOn = Services.prefs.getBoolPref(
        kPrefCaretBrowsingOn,
        false
      );
      var warn = Services.prefs.getBoolPref(kPrefWarnOnEnable, true);
      if (warn && !browseWithCaretOn) {
        var checkValue = { value: false };
        var promptService = Services.prompt;

        try {
          this._awaitingToggleCaretBrowsingPrompt = true;
          const [title, message, checkbox] =
            this.tabLocalization.formatValuesSync([
              "tabbrowser-confirm-caretbrowsing-title",
              "tabbrowser-confirm-caretbrowsing-message",
              "tabbrowser-confirm-caretbrowsing-checkbox",
            ]);
          var buttonPressed = promptService.confirmEx(
            window,
            title,
            message,
            // Make "No" the default:
            promptService.STD_YES_NO_BUTTONS |
              promptService.BUTTON_POS_1_DEFAULT,
            null,
            null,
            null,
            checkbox,
            checkValue
          );
        } catch (ex) {
          return;
        } finally {
          this._awaitingToggleCaretBrowsingPrompt = false;
        }
        if (buttonPressed != 0) {
          if (checkValue.value) {
            try {
              Services.prefs.setBoolPref(kPrefShortcutEnabled, false);
            } catch (ex) {}
          }
          return;
        }
        if (checkValue.value) {
          try {
            Services.prefs.setBoolPref(kPrefWarnOnEnable, false);
          } catch (ex) {}
        }
      }

      // Toggle the pref
      try {
        Services.prefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn);
      } catch (ex) {}
    },

    _handleKeyPressEvent(aEvent) {
      if (!aEvent.isTrusted) {
        // Don't let untrusted events mess with tabs.
        return;
      }

      // Skip this only if something has explicitly cancelled it.
      if (aEvent.defaultCancelled) {
        return;
      }

      // Skip if chrome code has cancelled this:
      if (aEvent.defaultPreventedByChrome) {
        return;
      }

      switch (ShortcutUtils.getSystemActionForEvent(aEvent, { rtl: RTL_UI })) {
        case ShortcutUtils.TOGGLE_CARET_BROWSING:
          if (
            aEvent.defaultPrevented ||
            this._maybeRequestReplyFromRemoteContent(aEvent)
          ) {
            break;
          }
          this.toggleCaretBrowsing();
          break;

        case ShortcutUtils.NEXT_TAB:
          if (AppConstants.platform == "macosx") {
            this.tabContainer.advanceSelectedTab(1, true);
            aEvent.preventDefault();
          }
          break;
        case ShortcutUtils.PREVIOUS_TAB:
          if (AppConstants.platform == "macosx") {
            this.tabContainer.advanceSelectedTab(-1, true);
            aEvent.preventDefault();
          }
          break;
      }
    },

    getTabTooltip(tab, includeLabel = true) {
      let labelArray = [];
      if (includeLabel) {
        labelArray.push(tab._fullLabel || tab.getAttribute("label"));
      }
      if (
        Services.prefs.getBoolPref(
          "browser.tabs.tooltipsShowPidAndActiveness",
          false
        )
      ) {
        if (tab.linkedBrowser) {
          // Show the PIDs of the content process and remote subframe processes.
          let [contentPid, ...framePids] = E10SUtils.getBrowserPids(
            tab.linkedBrowser,
            gFissionBrowser
          );
          if (contentPid) {
            if (framePids && framePids.length) {
              labelArray.push(
                `(pids ${contentPid}, ${framePids.sort().join(", ")})`
              );
            } else {
              labelArray.push(`(pid ${contentPid})`);
            }
          }
          if (tab.linkedBrowser.docShellIsActive) {
            labelArray.push("[A]");
          }
        }
      }

      let label = labelArray.join(" ");
      if (tab.userContextId) {
        const containerName = ContextualIdentityService.getUserContextLabel(
          tab.userContextId
        );
        label = this.tabLocalization.formatValueSync(
          "tabbrowser-container-tab-title",
          { title: label, containerName }
        );
      }

      labelArray = [label];
      if (tab.soundPlaying) {
        let audioPlayingString = this.tabLocalization.formatValueSync(
          "tabbrowser-tab-audio-playing-description"
        );
        labelArray.push(audioPlayingString);
      }
      return labelArray.join("\n");
    },

    createTooltip(event) {
      event.stopPropagation();
      let tab = event.target.triggerNode?.closest("tab");
      if (!tab) {
        event.preventDefault();
        return;
      }

      const tooltip = event.target;
      tooltip.removeAttribute("data-l10n-id");

      const tabCount = this.selectedTabs.includes(tab)
        ? this.selectedTabs.length
        : 1;
      if (tab.mOverCloseButton) {
        tooltip.label = "";
        document.l10n.setAttributes(tooltip, "tabbrowser-close-tabs-tooltip", {
          tabCount,
        });
      } else if (tab._overPlayingIcon) {
        let l10nId;
        const l10nArgs = { tabCount };
        if (tab.selected) {
          l10nId = tab.linkedBrowser.audioMuted
            ? "tabbrowser-unmute-tab-audio-tooltip"
            : "tabbrowser-mute-tab-audio-tooltip";
          const keyElem = document.getElementById("key_toggleMute");
          l10nArgs.shortcut = ShortcutUtils.prettifyShortcut(keyElem);
        } else if (tab.hasAttribute("activemedia-blocked")) {
          l10nId = "tabbrowser-unblock-tab-audio-tooltip";
        } else {
          l10nId = tab.linkedBrowser.audioMuted
            ? "tabbrowser-unmute-tab-audio-background-tooltip"
            : "tabbrowser-mute-tab-audio-background-tooltip";
        }
        tooltip.label = "";
        document.l10n.setAttributes(tooltip, l10nId, l10nArgs);
      } else {
        tooltip.label = this.getTabTooltip(tab, true);
      }
    },

    handleEvent(aEvent) {
      switch (aEvent.type) {
        case "keydown":
          this._handleKeyDownEvent(aEvent);
          break;
        case "keypress":
          this._handleKeyPressEvent(aEvent);
          break;
        case "framefocusrequested": {
          let tab = this.getTabForBrowser(aEvent.target);
          if (!tab || tab == this.selectedTab) {
            // Let the focus manager try to do its thing by not calling
            // preventDefault(). It will still raise the window if appropriate.
            break;
          }
          this.selectedTab = tab;
          window.focus();
          aEvent.preventDefault();
          break;
        }
        case "visibilitychange":
          const inactive = document.hidden;
          if (!this._switcher) {
            this.selectedBrowser.preserveLayers(inactive);
            this.selectedBrowser.docShellIsActive = !inactive;
          }
          break;
      }
    },

    observe(aSubject, aTopic, aData) {
      switch (aTopic) {
        case "contextual-identity-updated": {
          let identity = aSubject.wrappedJSObject;
          for (let tab of this.tabs) {
            if (tab.getAttribute("usercontextid") == identity.userContextId) {
              ContextualIdentityService.setTabStyle(tab);
            }
          }
          break;
        }
      }
    },

    refreshBlocked(actor, browser, data) {
      // The data object is expected to contain the following properties:
      //  - URI (string)
      //     The URI that a page is attempting to refresh or redirect to.
      //  - delay (int)
      //     The delay (in milliseconds) before the page was going to
      //     reload or redirect.
      //  - sameURI (bool)
      //     true if we're refreshing the page. false if we're redirecting.

      let notificationBox = this.getNotificationBox(browser);
      let notification =
        notificationBox.getNotificationWithValue("refresh-blocked");

      let l10nId = data.sameURI
        ? "refresh-blocked-refresh-label"
        : "refresh-blocked-redirect-label";
      if (notification) {
        notification.label = { "l10n-id": l10nId };
      } else {
        const buttons = [
          {
            "l10n-id": "refresh-blocked-allow",
            callback() {
              actor.sendAsyncMessage("RefreshBlocker:Refresh", data);
            },
          },
        ];

        notificationBox.appendNotification(
          "refresh-blocked",
          {
            label: { "l10n-id": l10nId },
            image: "chrome://browser/skin/notification-icons/popup.svg",
            priority: notificationBox.PRIORITY_INFO_MEDIUM,
          },
          buttons
        );
      }
    },

    _generateUniquePanelID() {
      if (!this._uniquePanelIDCounter) {
        this._uniquePanelIDCounter = 0;
      }

      let outerID = window.docShell.outerWindowID;

      // We want panel IDs to be globally unique, that's why we include the
      // window ID. We switched to a monotonic counter as Date.now() lead
      // to random failures because of colliding IDs.
      return "panel-" + outerID + "-" + ++this._uniquePanelIDCounter;
    },

    destroy() {
      this.tabContainer.destroy();
      Services.obs.removeObserver(this, "contextual-identity-updated");

      for (let tab of this.tabs) {
        let browser = tab.linkedBrowser;
        if (browser.registeredOpenURI) {
          let userContextId = browser.getAttribute("usercontextid") || 0;
          this.UrlbarProviderOpenTabs.unregisterOpenTab(
            browser.registeredOpenURI.spec,
            userContextId,
            PrivateBrowsingUtils.isWindowPrivate(window)
          );
          delete browser.registeredOpenURI;
        }

        let filter = this._tabFilters.get(tab);
        if (filter) {
          browser.webProgress.removeProgressListener(filter);

          let listener = this._tabListeners.get(tab);
          if (listener) {
            filter.removeProgressListener(listener);
            listener.destroy();
          }

          this._tabFilters.delete(tab);
          this._tabListeners.delete(tab);
        }
      }

      Services.els.removeSystemEventListener(document, "keydown", this, false);
      if (AppConstants.platform == "macosx") {
        Services.els.removeSystemEventListener(
          document,
          "keypress",
          this,
          false
        );
      }
      document.removeEventListener("visibilitychange", this);
      window.removeEventListener("framefocusrequested", this);

      if (gMultiProcessBrowser) {
        if (this._switcher) {
          this._switcher.destroy();
        }
      }
    },

    _setupEventListeners() {
      this.tabpanels.addEventListener("select", event => {
        if (event.target == this.tabpanels) {
          this.updateCurrentBrowser();
        }
      });

      this.addEventListener("DOMWindowClose", event => {
        let browser = event.target;
        if (!browser.isRemoteBrowser) {
          if (!event.isTrusted) {
            // If the browser is not remote, then we expect the event to be trusted.
            // In the remote case, the DOMWindowClose event is captured in content,
            // a message is sent to the parent, and another DOMWindowClose event
            // is re-dispatched on the actual browser node. In that case, the event
            // won't  be marked as trusted, since it's synthesized by JavaScript.
            return;
          }
          // In the parent-process browser case, it's possible that the browser
          // that fired DOMWindowClose is actually a child of another browser. We
          // want to find the top-most browser to determine whether or not this is
          // for a tab or not. The chromeEventHandler will be the top-most browser.
          browser = event.target.docShell.chromeEventHandler;
        }

        if (this.tabs.length == 1) {
          // We already did PermitUnload in the content process
          // for this tab (the only one in the window). So we don't
          // need to do it again for any tabs.
          window.skipNextCanClose = true;
          // In the parent-process browser case, the nsCloseEvent will actually take
          // care of tearing down the window, but we need to do this ourselves in the
          // content-process browser case. Doing so in both cases doesn't appear to
          // hurt.
          window.close();
          return;
        }

        let tab = this.getTabForBrowser(browser);
        if (tab) {
          // Skip running PermitUnload since it already happened in
          // the content process.
          this.removeTab(tab, { skipPermitUnload: true });
          // If we don't preventDefault on the DOMWindowClose event, then
          // in the parent-process browser case, we're telling the platform
          // to close the entire window. Calling preventDefault is our way of
          // saying we took care of this close request by closing the tab.
          event.preventDefault();
        }
      });

      this.addEventListener("pagetitlechanged", event => {
        let browser = event.target;
        let tab = this.getTabForBrowser(browser);
        if (!tab || tab.hasAttribute("pending")) {
          return;
        }

        // Ignore empty title changes on internal pages. This prevents the title
        // from changing while Fluent is populating the (initially-empty) title
        // element.
        if (
          !browser.contentTitle &&
          browser.contentPrincipal.isSystemPrincipal
        ) {
          return;
        }

        let titleChanged = this.setTabTitle(tab);
        if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) {
          tab.setAttribute("titlechanged", "true");
        }
      });

      this.addEventListener(
        "DOMWillOpenModalDialog",
        event => {
          if (!event.isTrusted) {
            return;
          }

          let targetIsWindow = Window.isInstance(event.target);

          // We're about to open a modal dialog, so figure out for which tab:
          // If this is a same-process modal dialog, then we're given its DOM
          // window as the event's target. For remote dialogs, we're given the
          // browser, but that's in the originalTarget and not the target,
          // because it's across the tabbrowser's XBL boundary.
          let tabForEvent = targetIsWindow
            ? this.getTabForBrowser(event.target.docShell.chromeEventHandler)
            : this.getTabForBrowser(event.originalTarget);

          // Focus window for beforeunload dialog so it is seen but don't
          // steal focus from other applications.
          if (
            event.detail &&
            event.detail.tabPrompt &&
            event.detail.inPermitUnload &&
            Services.focus.activeWindow
          ) {
            window.focus();
          }

          // Don't need to act if the tab is already selected or if there isn't
          // a tab for the event (e.g. for the webextensions options_ui remote
          // browsers embedded in the "about:addons" page):
          if (!tabForEvent || tabForEvent.selected) {
            return;
          }

          // We always switch tabs for beforeunload tab-modal prompts.
          if (
            event.detail &&
            event.detail.tabPrompt &&
            !event.detail.inPermitUnload
          ) {
            let docPrincipal = targetIsWindow
              ? event.target.document.nodePrincipal
              : null;
            // At least one of these should/will be non-null:
            let promptPrincipal =
              event.detail.promptPrincipal ||
              docPrincipal ||
              tabForEvent.linkedBrowser.contentPrincipal;

            // For null principals, we bail immediately and don't show the checkbox:
            if (!promptPrincipal || promptPrincipal.isNullPrincipal) {
              tabForEvent.attention = true;
              return;
            }

            // For non-system/expanded principals without permission, we bail and show the checkbox.
            if (promptPrincipal.URI && !promptPrincipal.isSystemPrincipal) {
              let permission = Services.perms.testPermissionFromPrincipal(
                promptPrincipal,
                "focus-tab-by-prompt"
              );
              if (permission != Services.perms.ALLOW_ACTION) {
                // Tell the prompt box we want to show the user a checkbox:
                let tabPrompt = Services.prefs.getBoolPref(
                  "prompts.contentPromptSubDialog"
                )
                  ? this.getTabDialogBox(tabForEvent.linkedBrowser)
                  : this.getTabModalPromptBox(tabForEvent.linkedBrowser);

                tabPrompt.onNextPromptShowAllowFocusCheckboxFor(
                  promptPrincipal
                );
                tabForEvent.attention = true;
                return;
              }
            }
            // ... so system and expanded principals, as well as permitted "normal"
            // URI-based principals, always get to steal focus for the tab when prompting.
          }

          // If permissions/origins dictate so, bring tab to the front.
          this.selectedTab = tabForEvent;
        },
        true
      );

      // When cancelling beforeunload tabmodal dialogs, reset the URL bar to
      // avoid spoofing risks.
      this.addEventListener(
        "DOMModalDialogClosed",
        event => {
          if (
            !event.detail?.wasPermitUnload ||
            event.detail.areLeaving ||
            event.target.nodeName != "browser"
          ) {
            return;
          }
          event.target.userTypedValue = null;
          if (event.target == this.selectedBrowser) {
            gURLBar.setURI();
          }
        },
        true
      );

      let onTabCrashed = event => {
        if (!event.isTrusted) {
          return;
        }

        let browser = event.originalTarget;

        if (!event.isTopFrame) {
          TabCrashHandler.onSubFrameCrash(browser, event.childID);
          return;
        }

        // Preloaded browsers do not actually have any tabs. If one crashes,
        // it should be released and removed.
        if (browser === this.preloadedBrowser) {
          NewTabPagePreloading.removePreloadedBrowser(window);
          return;
        }

        let isRestartRequiredCrash =
          event.type == "oop-browser-buildid-mismatch";

        let icon = browser.mIconURL;
        let tab = this.getTabForBrowser(browser);

        if (this.selectedBrowser == browser) {
          TabCrashHandler.onSelectedBrowserCrash(
            browser,
            isRestartRequiredCrash
          );
        } else {
          TabCrashHandler.onBackgroundBrowserCrash(
            browser,
            isRestartRequiredCrash
          );
        }

        tab.removeAttribute("soundplaying");
        this.setIcon(tab, icon);
      };

      this.addEventListener("oop-browser-crashed", onTabCrashed);
      this.addEventListener("oop-browser-buildid-mismatch", onTabCrashed);

      this.addEventListener("DOMAudioPlaybackStarted", event => {
        var tab = this.getTabFromAudioEvent(event);
        if (!tab) {
          return;
        }

        clearTimeout(tab._soundPlayingAttrRemovalTimer);
        tab._soundPlayingAttrRemovalTimer = 0;

        let modifiedAttrs = [];
        if (tab.hasAttribute("soundplaying-scheduledremoval")) {
          tab.removeAttribute("soundplaying-scheduledremoval");
          modifiedAttrs.push("soundplaying-scheduledremoval");
        }

        if (!tab.hasAttribute("soundplaying")) {
          tab.setAttribute("soundplaying", true);
          modifiedAttrs.push("soundplaying");
        }

        if (modifiedAttrs.length) {
          // Flush style so that the opacity takes effect immediately, in
          // case the media is stopped before the style flushes naturally.
          getComputedStyle(tab).opacity;
        }

        this._tabAttrModified(tab, modifiedAttrs);
      });

      this.addEventListener("DOMAudioPlaybackStopped", event => {
        var tab = this.getTabFromAudioEvent(event);
        if (!tab) {
          return;
        }

        if (tab.hasAttribute("soundplaying")) {
          let removalDelay = Services.prefs.getIntPref(
            "browser.tabs.delayHidingAudioPlayingIconMS"
          );

          tab.style.setProperty(
            "--soundplaying-removal-delay",
            `${removalDelay - 300}ms`
          );
          tab.setAttribute("soundplaying-scheduledremoval", "true");
          this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]);

          tab._soundPlayingAttrRemovalTimer = setTimeout(() => {
            tab.removeAttribute("soundplaying-scheduledremoval");
            tab.removeAttribute("soundplaying");
            this._tabAttrModified(tab, [
              "soundplaying",
              "soundplaying-scheduledremoval",
            ]);
          }, removalDelay);
        }
      });

      this.addEventListener("DOMAudioPlaybackBlockStarted", event => {
        var tab = this.getTabFromAudioEvent(event);
        if (!tab) {
          return;
        }

        if (!tab.hasAttribute("activemedia-blocked")) {
          tab.setAttribute("activemedia-blocked", true);
          this._tabAttrModified(tab, ["activemedia-blocked"]);
        }
      });

      this.addEventListener("DOMAudioPlaybackBlockStopped", event => {
        var tab = this.getTabFromAudioEvent(event);
        if (!tab) {
          return;
        }

        if (tab.hasAttribute("activemedia-blocked")) {
          tab.removeAttribute("activemedia-blocked");
          this._tabAttrModified(tab, ["activemedia-blocked"]);
          let hist = Services.telemetry.getHistogramById(
            "TAB_AUDIO_INDICATOR_USED"
          );
          hist.add(2 /* unblockByVisitingTab */);
        }
      });

      this.addEventListener("GloballyAutoplayBlocked", event => {
        let browser = event.originalTarget;
        let tab = this.getTabForBrowser(browser);
        if (!tab) {
          return;
        }

        SitePermissions.setForPrincipal(
          browser.contentPrincipal,
          "autoplay-media",
          SitePermissions.BLOCK,
          SitePermissions.SCOPE_GLOBAL,
          browser
        );
      });

      let tabContextFTLInserter = () => {
        this.translateTabContextMenu();
        this.tabContainer.removeEventListener(
          "contextmenu",
          tabContextFTLInserter,
          true
        );
        this.tabContainer.removeEventListener(
          "mouseover",
          tabContextFTLInserter
        );
        this.tabContainer.removeEventListener(
          "focus",
          tabContextFTLInserter,
          true
        );
      };
      this.tabContainer.addEventListener(
        "contextmenu",
        tabContextFTLInserter,
        true
      );
      this.tabContainer.addEventListener("mouseover", tabContextFTLInserter);
      this.tabContainer.addEventListener("focus", tabContextFTLInserter, true);

      // Fired when Gecko has decided a <browser> element will change
      // remoteness. This allows persisting some state on this element across
      // process switches.
      this.addEventListener("WillChangeBrowserRemoteness", event => {
        let browser = event.originalTarget;
        let tab = this.getTabForBrowser(browser);
        if (!tab) {
          return;
        }

        // Dispatch the `BeforeTabRemotenessChange` event, allowing other code
        // to react to this tab's process switch.
        let evt = document.createEvent("Events");
        evt.initEvent("BeforeTabRemotenessChange", true, false);
        tab.dispatchEvent(evt);

        let wasActive = document.activeElement == browser;

        // Unhook our progress listener.
        let filter = this._tabFilters.get(tab);
        let oldListener = this._tabListeners.get(tab);
        browser.webProgress.removeProgressListener(filter);
        filter.removeProgressListener(oldListener);
        let stateFlags = oldListener.mStateFlags;
        let requestCount = oldListener.mRequestCount;

        // We'll be creating a new listener, so destroy the old one.
        oldListener.destroy();

        let oldDroppedLinkHandler = browser.droppedLinkHandler;
        let oldUserTypedValue = browser.userTypedValue;
        let hadStartedLoad = browser.didStartLoadSinceLastUserTyping();

        let didChange = didChangeEvent => {
          browser.userTypedValue = oldUserTypedValue;
          if (hadStartedLoad) {
            browser.urlbarChangeTracker.startedLoad();
          }

          browser.droppedLinkHandler = oldDroppedLinkHandler;

          // This shouldn't really be necessary, however, this has the side effect
          // of sending MozLayerTreeReady / MozLayerTreeCleared events for remote
          // frames, which the tab switcher depends on.
          //
          // eslint-disable-next-line no-self-assign
          browser.docShellIsActive = browser.docShellIsActive;

          // Create a new tab progress listener for the new browser we just
          // injected, since tab progress listeners have logic for handling the
          // initial about:blank load
          let listener = new TabProgressListener(
            tab,
            browser,
            false,
            false,
            stateFlags,
            requestCount
          );
          this._tabListeners.set(tab, listener);
          filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);

          // Restore the progress listener.
          browser.webProgress.addProgressListener(
            filter,
            Ci.nsIWebProgress.NOTIFY_ALL
          );

          let cbEvent = browser.getContentBlockingEvents();
          // Include the true final argument to indicate that this event is
          // simulated (instead of being observed by the webProgressListener).
          this._callProgressListeners(
            browser,
            "onContentBlockingEvent",
            [browser.webProgress, null, cbEvent, true],
            true,
            false
          );

          if (browser.isRemoteBrowser) {
            // Switching the browser to be remote will connect to a new child
            // process so the browser can no longer be considered to be
            // crashed.
            tab.removeAttribute("crashed");
            gBrowser.tabContainer.updateTabIndicatorAttr(tab);
          }

          if (wasActive) {
            browser.focus();
          }

          if (this.isFindBarInitialized(tab)) {
            this.getCachedFindBar(tab).browser = browser;
          }

          evt = document.createEvent("Events");
          evt.initEvent("TabRemotenessChange", true, false);
          tab.dispatchEvent(evt);
        };
        browser.addEventListener("DidChangeBrowserRemoteness", didChange, {
          once: true,
        });
      });

      this.addEventListener("pageinfo", event => {
        let browser = event.originalTarget;
        let tab = this.getTabForBrowser(browser);
        if (!tab) {
          return;
        }

        const { url, description, previewImageURL } = event.detail;
        this.setPageInfo(url, description, previewImageURL);
      });
    },

    translateTabContextMenu() {
      if (this._tabContextMenuTranslated) {
        return;
      }
      MozXULElement.insertFTLIfNeeded("browser/tabContextMenu.ftl");
      // Un-lazify the l10n-ids now that the FTL file has been inserted.
      document
        .getElementById("tabContextMenu")
        .querySelectorAll("[data-lazy-l10n-id]")
        .forEach(el => {
          el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
          el.removeAttribute("data-lazy-l10n-id");
        });
      this._tabContextMenuTranslated = true;
    },

    setSuccessor(aTab, successorTab) {
      if (aTab.ownerGlobal != window) {
        throw new Error("Cannot set the successor of another window's tab");
      }
      if (successorTab == aTab) {
        successorTab = null;
      }
      if (successorTab && successorTab.ownerGlobal != window) {
        throw new Error("Cannot set the successor to another window's tab");
      }
      if (aTab.successor) {
        aTab.successor.predecessors.delete(aTab);
      }
      aTab.successor = successorTab;
      if (successorTab) {
        if (!successorTab.predecessors) {
          successorTab.predecessors = new Set();
        }
        successorTab.predecessors.add(aTab);
      }
    },

    /**
     * For all tabs with aTab as a successor, set the successor to aOtherTab
     * instead.
     */
    replaceInSuccession(aTab, aOtherTab) {
      if (aTab.predecessors) {
        for (const predecessor of Array.from(aTab.predecessors)) {
          this.setSuccessor(predecessor, aOtherTab);
        }
      }
    },

    /**
     * Get the triggering principal for the last navigation in the session history.
     */
    _getTriggeringPrincipalFromHistory(aBrowser) {
      let sessionHistory = aBrowser?.browsingContext?.sessionHistory;
      if (
        !sessionHistory ||
        !sessionHistory.index ||
        sessionHistory.count == 0
      ) {
        return undefined;
      }
      let currentEntry = sessionHistory.getEntryAtIndex(sessionHistory.index);
      let triggeringPrincipal = currentEntry?.triggeringPrincipal;
      return triggeringPrincipal;
    },

    clearRelatedTabs() {
      this._lastRelatedTabMap = new WeakMap();
    },
  };

  /**
   * A web progress listener object definition for a given tab.
   */
  class TabProgressListener {
    constructor(
      aTab,
      aBrowser,
      aStartsBlank,
      aWasPreloadedBrowser,
      aOrigStateFlags,
      aOrigRequestCount
    ) {
      let stateFlags = aOrigStateFlags || 0;
      // Initialize mStateFlags to non-zero e.g. when creating a progress
      // listener for preloaded browsers as there was no progress listener
      // around when the content started loading. If the content didn't
      // quite finish loading yet, mStateFlags will very soon be overridden
      // with the correct value and end up at STATE_STOP again.
      if (aWasPreloadedBrowser) {
        stateFlags =
          Ci.nsIWebProgressListener.STATE_STOP |
          Ci.nsIWebProgressListener.STATE_IS_REQUEST;
      }

      this.mTab = aTab;
      this.mBrowser = aBrowser;
      this.mBlank = aStartsBlank;

      // cache flags for correct status UI update after tab switching
      this.mStateFlags = stateFlags;
      this.mStatus = 0;
      this.mMessage = "";
      this.mTotalProgress = 0;

      // count of open requests (should always be 0 or 1)
      this.mRequestCount = aOrigRequestCount || 0;
    }

    destroy() {
      delete this.mTab;
      delete this.mBrowser;
    }

    _callProgressListeners(...args) {
      args.unshift(this.mBrowser);
      return gBrowser._callProgressListeners.apply(gBrowser, args);
    }

    _shouldShowProgress(aRequest) {
      if (this.mBlank) {
        return false;
      }

      // Don't show progress indicators in tabs for about: URIs
      // pointing to local resources.
      if (
        aRequest instanceof Ci.nsIChannel &&
        aRequest.originalURI.schemeIs("about")
      ) {
        return false;
      }

      return true;
    }

    _isForInitialAboutBlank(aWebProgress, aStateFlags, aLocation) {
      if (!this.mBlank || !aWebProgress.isTopLevel) {
        return false;
      }

      // If the state has STATE_STOP, and no requests were in flight, then this
      // must be the initial "stop" for the initial about:blank document.
      if (
        aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
        this.mRequestCount == 0 &&
        !aLocation
      ) {
        return true;
      }

      let location = aLocation ? aLocation.spec : "";
      return location == "about:blank";
    }

    onProgressChange(
      aWebProgress,
      aRequest,
      aCurSelfProgress,
      aMaxSelfProgress,
      aCurTotalProgress,
      aMaxTotalProgress
    ) {
      this.mTotalProgress = aMaxTotalProgress
        ? aCurTotalProgress / aMaxTotalProgress
        : 0;

      if (!this._shouldShowProgress(aRequest)) {
        return;
      }

      if (this.mTotalProgress && this.mTab.hasAttribute("busy")) {
        this.mTab.setAttribute("progress", "true");
        gBrowser._tabAttrModified(this.mTab, ["progress"]);
      }

      this._callProgressListeners("onProgressChange", [
        aWebProgress,
        aRequest,
        aCurSelfProgress,
        aMaxSelfProgress,
        aCurTotalProgress,
        aMaxTotalProgress,
      ]);
    }

    onProgressChange64(
      aWebProgress,
      aRequest,
      aCurSelfProgress,
      aMaxSelfProgress,
      aCurTotalProgress,
      aMaxTotalProgress
    ) {
      return this.onProgressChange(
        aWebProgress,
        aRequest,
        aCurSelfProgress,
        aMaxSelfProgress,
        aCurTotalProgress,
        aMaxTotalProgress
      );
    }

    /* eslint-disable complexity */
    onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
      if (!aRequest) {
        return;
      }

      let location, originalLocation;
      try {
        aRequest.QueryInterface(Ci.nsIChannel);
        location = aRequest.URI;
        originalLocation = aRequest.originalURI;
      } catch (ex) {}

      let ignoreBlank = this._isForInitialAboutBlank(
        aWebProgress,
        aStateFlags,
        location
      );

      const { STATE_START, STATE_STOP, STATE_IS_NETWORK } =
        Ci.nsIWebProgressListener;

      // If we were ignoring some messages about the initial about:blank, and we
      // got the STATE_STOP for it, we'll want to pay attention to those messages
      // from here forward. Similarly, if we conclude that this state change
      // is one that we shouldn't be ignoring, then stop ignoring.
      if (
        (ignoreBlank &&
          aStateFlags & STATE_STOP &&
          aStateFlags & STATE_IS_NETWORK) ||
        (!ignoreBlank && this.mBlank)
      ) {
        this.mBlank = false;
      }

      if (aStateFlags & STATE_START && aStateFlags & STATE_IS_NETWORK) {
        this.mRequestCount++;

        if (aWebProgress.isTopLevel) {
          // Need to use originalLocation rather than location because things
          // like about:home and about:privatebrowsing arrive with nsIRequest
          // pointing to their resolved jar: or file: URIs.
          if (
            !(
              originalLocation &&
              gInitialPages.includes(originalLocation.spec) &&
              originalLocation != "about:blank" &&
              this.mBrowser.initialPageLoadedFromUserAction !=
                originalLocation.spec &&
              this.mBrowser.currentURI &&
              this.mBrowser.currentURI.spec == "about:blank"
            )
          ) {
            // Indicating that we started a load will allow the location
            // bar to be cleared when the load finishes.
            // In order to not overwrite user-typed content, we avoid it
            // (see if condition above) in a very specific case:
            // If the load is of an 'initial' page (e.g. about:privatebrowsing,
            // about:newtab, etc.), was not explicitly typed in the location
            // bar by the user, is not about:blank (because about:blank can be
            // loaded by websites under their principal), and the current
            // page in the browser is about:blank (indicating it is a newly
            // created or re-created browser, e.g. because it just switched
            // remoteness or is a new tab/window).
            this.mBrowser.urlbarChangeTracker.startedLoad();

            // To improve the user experience and perceived performance when
            // opening links in new tabs, we show the url and tab title sooner,
            // but only if it's safe (from a phishing point of view) to do so,
            // thus there's no session history and the load starts from a
            // non-web-controlled blank page.
            if (
              this.mBrowser.browsingContext.sessionHistory?.count === 0 &&
              BrowserUIUtils.checkEmptyPageOrigin(
                this.mBrowser,
                originalLocation
              )
            ) {
              gBrowser.setInitialTabTitle(this.mTab, originalLocation.spec, {
                isURL: true,
              });

              this.mBrowser.browsingContext.nonWebControlledBlankURI =
                originalLocation;
              if (this.mTab.selected && !gBrowser.userTypedValue) {
                gURLBar.setURI();
              }
            }
          }
          delete this.mBrowser.initialPageLoadedFromUserAction;
          // If the browser is loading it must not be crashed anymore
          this.mTab.removeAttribute("crashed");
          gBrowser.tabContainer.updateTabIndicatorAttr(this.mTab);
        }

        if (this._shouldShowProgress(aRequest)) {
          if (
            !(aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) &&
            aWebProgress &&
            aWebProgress.isTopLevel
          ) {
            this.mTab.setAttribute("busy", "true");
            gBrowser._tabAttrModified(this.mTab, ["busy"]);
            this.mTab._notselectedsinceload = !this.mTab.selected;
            gBrowser.syncThrobberAnimations(this.mTab);
          }

          if (this.mTab.selected) {
            gBrowser._isBusy = true;
          }
        }
      } else if (aStateFlags & STATE_STOP && aStateFlags & STATE_IS_NETWORK) {
        // since we (try to) only handle STATE_STOP of the last request,
        // the count of open requests should now be 0
        this.mRequestCount = 0;

        let modifiedAttrs = [];
        if (this.mTab.hasAttribute("busy")) {
          this.mTab.removeAttribute("busy");
          modifiedAttrs.push("busy");

          // Only animate the "burst" indicating the page has loaded if
          // the top-level page is the one that finished loading.
          if (
            aWebProgress.isTopLevel &&
            !aWebProgress.isLoadingDocument &&
            Components.isSuccessCode(aStatus) &&
            !gBrowser.tabAnimationsInProgress &&
            !gReduceMotion
          ) {
            if (this.mTab._notselectedsinceload) {
              this.mTab.setAttribute("notselectedsinceload", "true");
            } else {
              this.mTab.removeAttribute("notselectedsinceload");
            }

            this.mTab.setAttribute("bursting", "true");
          }
        }

        if (this.mTab.hasAttribute("progress")) {
          this.mTab.removeAttribute("progress");
          modifiedAttrs.push("progress");
        }

        if (modifiedAttrs.length) {
          gBrowser._tabAttrModified(this.mTab, modifiedAttrs);
        }

        if (aWebProgress.isTopLevel) {
          let isSuccessful = Components.isSuccessCode(aStatus);
          if (!isSuccessful && !this.mTab.isEmpty) {
            // Restore the current document's location in case the
            // request was stopped (possibly from a content script)
            // before the location changed.

            this.mBrowser.userTypedValue = null;
            // When browser.tabs.documentchannel.parent-controlled pref and SHIP
            // are enabled and a load gets cancelled due to another one
            // starting, the error is NS_BINDING_CANCELLED_OLD_LOAD.
            // When these prefs are not enabled, the error is different and
            // that's why we still want to look at the isNavigating flag.
            // We could add a workaround and make sure that in the alternative
            // codepaths we would also omit the same error, but considering
            // how we will be enabling fission by default soon, we can keep
            // using isNavigating for now, and remove it when the
            // parent-controlled pref and SHIP are enabled by default.
            // Bug 1725716 has been filed to consider removing isNavigating
            // field alltogether.
            let isNavigating = this.mBrowser.isNavigating;
            if (
              this.mTab.selected &&
              aStatus != Cr.NS_BINDING_CANCELLED_OLD_LOAD &&
              !isNavigating
            ) {
              gURLBar.setURI();
            }
          } else if (isSuccessful) {
            this.mBrowser.urlbarChangeTracker.finishedLoad();
          }
        }

        // If we don't already have an icon for this tab then clear the tab's
        // icon. Don't do this on the initial about:blank load to prevent
        // flickering. Don't clear the icon if we already set it from one of the
        // known defaults. Note we use the original URL since about:newtab
        // redirects to a prerendered page.
        if (
          !this.mBrowser.mIconURL &&
          !ignoreBlank &&
          !(originalLocation.spec in FAVICON_DEFAULTS)
        ) {
          this.mTab.removeAttribute("image");
        }

        // For keyword URIs clear the user typed value since they will be changed into real URIs
        if (location.scheme == "keyword") {
          this.mBrowser.userTypedValue = null;
        }

        if (this.mTab.selected) {
          gBrowser._isBusy = false;
        }
      }

      if (ignoreBlank) {
        this._callProgressListeners(
          "onUpdateCurrentBrowser",
          [aStateFlags, aStatus, "", 0],
          true,
          false
        );
      } else {
        this._callProgressListeners(
          "onStateChange",
          [aWebProgress, aRequest, aStateFlags, aStatus],
          true,
          false
        );
      }

      this._callProgressListeners(
        "onStateChange",
        [aWebProgress, aRequest, aStateFlags, aStatus],
        false
      );

      if (aStateFlags & (STATE_START | STATE_STOP)) {
        // reset cached temporary values at beginning and end
        this.mMessage = "";
        this.mTotalProgress = 0;
      }
      this.mStateFlags = aStateFlags;
      this.mStatus = aStatus;
    }
    /* eslint-enable complexity */

    onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
      // OnLocationChange is called for both the top-level content
      // and the subframes.
      let topLevel = aWebProgress.isTopLevel;

      let isSameDocument = !!(
        aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
      );
      if (topLevel) {
        let isReload = !!(
          aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD
        );
        let isErrorPage = !!(
          aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE
        );

        // We need to clear the typed value
        // if the document failed to load, to make sure the urlbar reflects the
        // failed URI (particularly for SSL errors). However, don't clear the value
        // if the error page's URI is about:blank, because that causes complete
        // loss of urlbar contents for invalid URI errors (see bug 867957).
        // Another reason to clear the userTypedValue is if this was an anchor
        // navigation initiated by the user.
        // Finally, we do insert the URL if this is a same-document navigation
        // and the user cleared the URL manually.
        if (
          this.mBrowser.didStartLoadSinceLastUserTyping() ||
          (isErrorPage && aLocation.spec != "about:blank") ||
          (isSameDocument && this.mBrowser.isNavigating) ||
          (isSameDocument && !this.mBrowser.userTypedValue)
        ) {
          this.mBrowser.userTypedValue = null;
        }

        // If the tab has been set to "busy" outside the stateChange
        // handler below (e.g. by sessionStore.navigateAndRestore), and
        // the load results in an error page, it's possible that there
        // isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy
        // attribute being removed. In this case we should remove the
        // attribute here.
        if (isErrorPage && this.mTab.hasAttribute("busy")) {
          this.mTab.removeAttribute("busy");
          gBrowser._tabAttrModified(this.mTab, ["busy"]);
        }

        if (!isSameDocument) {
          // If the browser was playing audio, we should remove the playing state.
          if (this.mTab.hasAttribute("soundplaying")) {
            clearTimeout(this.mTab._soundPlayingAttrRemovalTimer);
            this.mTab._soundPlayingAttrRemovalTimer = 0;
            this.mTab.removeAttribute("soundplaying");
            gBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
          }

          // If the browser was previously muted, we should restore the muted state.
          if (this.mTab.hasAttribute("muted")) {
            this.mTab.linkedBrowser.mute();
          }

          if (gBrowser.isFindBarInitialized(this.mTab)) {
            let findBar = gBrowser.getCachedFindBar(this.mTab);

            // Close the Find toolbar if we're in old-style TAF mode
            if (findBar.findMode != findBar.FIND_NORMAL) {
              findBar.close();
            }
          }

          // Note that we're not updating for same-document loads, despite
          // the `title` argument to `history.pushState/replaceState`. For
          // context, see https://bugzilla.mozilla.org/show_bug.cgi?id=585653
          // and https://github.com/whatwg/html/issues/2174
          if (!isReload) {
            gBrowser.setTabTitle(this.mTab);
          }

          // Don't clear the favicon if this tab is in the pending
          // state, as SessionStore will have set the icon for us even
          // though we're pointed at an about:blank. Also don't clear it
          // if the tab is in customize mode, to keep the one set by
          // gCustomizeMode.setTab (bug 1551239). Also don't clear it
          // if onLocationChange was triggered by a pushState or a
          // replaceState (bug 550565) or a hash change (bug 408415).
          if (
            !this.mTab.hasAttribute("pending") &&
            !this.mTab.hasAttribute("customizemode") &&
            aWebProgress.isLoadingDocument
          ) {
            // Removing the tab's image here causes flickering, wait until the
            // load is complete.
            this.mBrowser.mIconURL = null;
          }

          if (!isReload && aWebProgress.isLoadingDocument) {
            let triggerer = gBrowser._getTriggeringPrincipalFromHistory(
              this.mBrowser
            );
            // Typing a url, searching or clicking a bookmark will load a new
            // document that is no longer tied to a navigation from the previous
            // content and will have a system principal as the triggerer.
            if (triggerer && triggerer.isSystemPrincipal) {
              // Reset the related tab map so that the next tab opened will be related
              // to this new document and not to tabs opened by the previous one.
              gBrowser.clearRelatedTabs();
            }
          }

          if (
            aRequest instanceof Ci.nsIChannel &&
            !isBlankPageURL(aRequest.originalURI.spec)
          ) {
            this.mBrowser.originalURI = aRequest.originalURI;
          }
        }

        let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
        if (this.mBrowser.registeredOpenURI) {
          let uri = this.mBrowser.registeredOpenURI;
          gBrowser.UrlbarProviderOpenTabs.unregisterOpenTab(
            uri.spec,
            userContextId,
            PrivateBrowsingUtils.isWindowPrivate(window)
          );
          delete this.mBrowser.registeredOpenURI;
        }
        if (!isBlankPageURL(aLocation.spec)) {
          gBrowser.UrlbarProviderOpenTabs.registerOpenTab(
            aLocation.spec,
            userContextId,
            PrivateBrowsingUtils.isWindowPrivate(window)
          );
          this.mBrowser.registeredOpenURI = aLocation;
        }

        if (this.mTab != gBrowser.selectedTab) {
          let tabCacheIndex = gBrowser._tabLayerCache.indexOf(this.mTab);
          if (tabCacheIndex != -1) {
            gBrowser._tabLayerCache.splice(tabCacheIndex, 1);
            gBrowser._getSwitcher().cleanUpTabAfterEviction(this.mTab);
          }
        } else {
          if (
            gBrowser.featureCallout &&
            (gBrowser.featureCalloutPanelId !==
              gBrowser.selectedTab.linkedPanel ||
              gBrowser.contentPrincipal.originNoSuffix !== "resource://pdf.js")
          ) {
            gBrowser.featureCallout.endTour(true);
            gBrowser.featureCallout = null;
          }

          // For now, only check for Feature Callout messages
          // when viewing PDFs. Later, we can expand this to check
          // for callout messages on every change of tab location.
          if (
            !gBrowser.featureCallout &&
            gBrowser.contentPrincipal.originNoSuffix === "resource://pdf.js"
          ) {
            gBrowser.instantiateFeatureCalloutTour(
              gBrowser.selectedBrowser,
              gBrowser.selectedTab.linkedPanel
            );
            gBrowser.featureCallout.showFeatureCallout();
          }
        }
      }

      if (!this.mBlank || this.mBrowser.hasContentOpener) {
        this._callProgressListeners("onLocationChange", [
          aWebProgress,
          aRequest,
          aLocation,
          aFlags,
        ]);
        if (topLevel && !isSameDocument) {
          // Include the true final argument to indicate that this event is
          // simulated (instead of being observed by the webProgressListener).
          this._callProgressListeners("onContentBlockingEvent", [
            aWebProgress,
            null,
            0,
            true,
          ]);
        }
      }

      if (topLevel) {
        this.mBrowser.lastURI = aLocation;
        this.mBrowser.lastLocationChange = Date.now();
      }
    }

    onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
      if (this.mBlank) {
        return;
      }

      this._callProgressListeners("onStatusChange", [
        aWebProgress,
        aRequest,
        aStatus,
        aMessage,
      ]);

      this.mMessage = aMessage;
    }

    onSecurityChange(aWebProgress, aRequest, aState) {
      this._callProgressListeners("onSecurityChange", [
        aWebProgress,
        aRequest,
        aState,
      ]);
    }

    onContentBlockingEvent(aWebProgress, aRequest, aEvent) {
      this._callProgressListeners("onContentBlockingEvent", [
        aWebProgress,
        aRequest,
        aEvent,
      ]);
    }

    onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
      return this._callProgressListeners("onRefreshAttempted", [
        aWebProgress,
        aURI,
        aDelay,
        aSameURI,
      ]);
    }
  }
  TabProgressListener.prototype.QueryInterface = ChromeUtils.generateQI([
    "nsIWebProgressListener",
    "nsIWebProgressListener2",
    "nsISupportsWeakReference",
  ]);

  let URILoadingWrapper = {
    _normalizeLoadURIOptions(browser, loadURIOptions) {
      if (!loadURIOptions.triggeringPrincipal) {
        throw new Error("Must load with a triggering Principal");
      }

      if (
        loadURIOptions.userContextId &&
        loadURIOptions.userContextId != browser.getAttribute("usercontextid")
      ) {
        throw new Error("Cannot load with mismatched userContextId");
      }

      loadURIOptions.loadFlags |= loadURIOptions.flags | LOAD_FLAGS_NONE;
      delete loadURIOptions.flags;
      loadURIOptions.hasValidUserGestureActivation ??=
        document.hasValidTransientUserGestureActivation;
    },

    _loadFlagsToFixupFlags(browser, loadFlags) {
      // Attempt to perform URI fixup to see if we can handle this URI in chrome.
      let fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_NONE;
      if (loadFlags & LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) {
        fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
      }
      if (loadFlags & LOAD_FLAGS_FIXUP_SCHEME_TYPOS) {
        fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
      }
      if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
        fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
      }
      return fixupFlags;
    },

    _fixupURIString(browser, uriString, loadURIOptions) {
      let fixupFlags = this._loadFlagsToFixupFlags(
        browser,
        loadURIOptions.loadFlags
      );

      // XXXgijs: If we switch to loading the URI we return from this method,
      // rather than redoing fixup in docshell (see bug 1815509), we need to
      // ensure that the loadURIOptions have the fixup flag removed here for
      // loads where `uriString` already parses if just passed immediately
      // to `newURI`.
      // Right now this happens in nsDocShellLoadState code.
      try {
        let fixupInfo = Services.uriFixup.getFixupURIInfo(
          uriString,
          fixupFlags
        );
        return fixupInfo.preferredURI;
      } catch (e) {
        // getFixupURIInfo may throw. Just return null, our caller will deal.
      }
      return null;
    },

    /**
     * Handles URIs when we want to deal with them in chrome code rather than pass
     * them down to a content browser. This can avoid unnecessary process switching
     * for the browser.
     * @param aBrowser the browser that is attempting to load the URI
     * @param aUri the nsIURI that is being loaded
     * @returns true if the URI is handled, otherwise false
     */
    _handleUriInChrome(aBrowser, aUri) {
      if (aUri.scheme == "file") {
        try {
          let mimeType = Cc["@mozilla.org/mime;1"]
            .getService(Ci.nsIMIMEService)
            .getTypeFromURI(aUri);
          if (mimeType == "application/x-xpinstall") {
            let systemPrincipal =
              Services.scriptSecurityManager.getSystemPrincipal();
            AddonManager.getInstallForURL(aUri.spec, {
              telemetryInfo: { source: "file-url" },
            }).then(install => {
              AddonManager.installAddonFromWebpage(
                mimeType,
                aBrowser,
                systemPrincipal,
                install
              );
            });
            return true;
          }
        } catch (e) {
          return false;
        }
      }

      return false;
    },

    _updateTriggerMetadataForLoad(
      browser,
      uriString,
      { loadFlags, globalHistoryOptions }
    ) {
      if (globalHistoryOptions?.triggeringSponsoredURL) {
        try {
          // Browser may access URL after fixing it up, then store the URL into DB.
          // To match with it, fix the link up explicitly.
          const triggeringSponsoredURL = Services.uriFixup.getFixupURIInfo(
            globalHistoryOptions.triggeringSponsoredURL,
            this._loadFlagsToFixupFlags(browser, loadFlags)
          ).fixedURI.spec;
          browser.setAttribute(
            "triggeringSponsoredURL",
            triggeringSponsoredURL
          );
          const time =
            globalHistoryOptions.triggeringSponsoredURLVisitTimeMS ||
            Date.now();
          browser.setAttribute("triggeringSponsoredURLVisitTimeMS", time);
        } catch (e) {}
      }

      if (globalHistoryOptions?.triggeringSearchEngine) {
        browser.setAttribute(
          "triggeringSearchEngine",
          globalHistoryOptions.triggeringSearchEngine
        );
        browser.setAttribute("triggeringSearchEngineURL", uriString);
      } else {
        browser.removeAttribute("triggeringSearchEngine");
        browser.removeAttribute("triggeringSearchEngineURL");
      }
    },

    // Both of these are used to override functions on browser-custom-element.
    fixupAndLoadURIString(browser, uriString, loadURIOptions = {}) {
      this._internalMaybeFixupLoadURI(browser, uriString, null, loadURIOptions);
    },
    loadURI(browser, uri, loadURIOptions = {}) {
      this._internalMaybeFixupLoadURI(browser, "", uri, loadURIOptions);
    },

    // A shared function used by both remote and non-remote browsers to
    // load a string URI or redirect it to the correct process.
    _internalMaybeFixupLoadURI(browser, uriString, uri, loadURIOptions) {
      this._normalizeLoadURIOptions(browser, loadURIOptions);
      // Some callers pass undefined/null when calling
      // loadURI/fixupAndLoadURIString. Just load about:blank instead:
      if (!uriString && !uri) {
        uri = Services.io.newURI("about:blank");
      }

      // We need a URI in frontend code for checking various things. Ideally
      // we would then also pass that URI to webnav/browsingcontext code
      // for loading, but we historically haven't. Changing this would alter
      // fixup scenarios in some non-obvious cases.
      let startedWithURI = !!uri;
      if (!uri) {
        // Note: this may return null if we can't make a URI out of the input.
        uri = this._fixupURIString(browser, uriString, loadURIOptions);
      }

      if (uri && this._handleUriInChrome(browser, uri)) {
        // If we've handled the URI in chrome, then just return here.
        return;
      }

      this._updateTriggerMetadataForLoad(
        browser,
        uriString || uri.spec,
        loadURIOptions
      );

      // XXX(nika): Is `browser.isNavigating` necessary anymore?
      // XXX(gijs): Unsure. But it mirrors docShell.isNavigating, but in the parent process
      // (and therefore imperfectly so).
      browser.isNavigating = true;

      try {
        // Should more generally prefer loadURI here - see bug 1815509.
        if (startedWithURI) {
          browser.webNavigation.loadURI(uri, loadURIOptions);
        } else {
          browser.webNavigation.fixupAndLoadURIString(
            uriString,
            loadURIOptions
          );
        }
      } finally {
        browser.isNavigating = false;
      }
    },
  };
} // end private scope for gBrowser

var StatusPanel = {
  // This is useful for debugging (set to `true` in the interesting state for
  // the panel to remain in that state).
  _frozen: false,

  get panel() {
    delete this.panel;
    this.panel = document.getElementById("statuspanel");
    this.panel.addEventListener(
      "transitionend",
      this._onTransitionEnd.bind(this)
    );
    this.panel.addEventListener(
      "transitioncancel",
      this._onTransitionEnd.bind(this)
    );
    return this.panel;
  },

  get isVisible() {
    return !this.panel.hasAttribute("inactive");
  },

  update() {
    if (BrowserHandler.kiosk || this._frozen) {
      return;
    }
    let text;
    let type;
    let types = ["overLink"];
    if (XULBrowserWindow.busyUI) {
      types.push("status");
    }
    types.push("defaultStatus");
    for (type of types) {
      if ((text = XULBrowserWindow[type])) {
        break;
      }
    }

    // If it's a long data: URI that uses base64 encoding, truncate to
    // a reasonable length rather than trying to display the entire thing.
    // We can't shorten arbitrary URIs like this, as bidi etc might mean
    // we need the trailing characters for display. But a base64-encoded
    // data-URI is plain ASCII, so this is OK for status panel display.
    // (See bug 1484071.)
    let textCropped = false;
    if (text.length > 500 && text.match(/^data:[^,]+;base64,/)) {
      text = text.substring(0, 500) + "\u2026";
      textCropped = true;
    }

    if (this._labelElement.value != text || (text && !this.isVisible)) {
      this.panel.setAttribute("previoustype", this.panel.getAttribute("type"));
      this.panel.setAttribute("type", type);

      this._label = text;
      this._labelElement.setAttribute(
        "crop",
        type == "overLink" && !textCropped ? "center" : "end"
      );
    }
  },

  get _labelElement() {
    delete this._labelElement;
    return (this._labelElement = document.getElementById("statuspanel-label"));
  },

  set _label(val) {
    if (!this.isVisible) {
      this.panel.removeAttribute("mirror");
      this.panel.removeAttribute("sizelimit");
    }

    if (
      this.panel.getAttribute("type") == "status" &&
      this.panel.getAttribute("previoustype") == "status"
    ) {
      // Before updating the label, set the panel's current width as its
      // min-width to let the panel grow but not shrink and prevent
      // unnecessary flicker while loading pages. We only care about the
      // panel's width once it has been painted, so we can do this
      // without flushing layout.
      this.panel.style.minWidth =
        window.windowUtils.getBoundsWithoutFlushing(this.panel).width + "px";
    } else {
      this.panel.style.minWidth = "";
    }

    if (val) {
      this._labelElement.value = val;
      if (this.panel.hidden) {
        this.panel.hidden = false;
        // This ensures that the "inactive" attribute removal triggers a
        // transition.
        getComputedStyle(this.panel).display;
      }
      this.panel.removeAttribute("inactive");
      MousePosTracker.addListener(this);
    } else {
      this.panel.setAttribute("inactive", "true");
      MousePosTracker.removeListener(this);
    }
  },

  _onTransitionEnd() {
    if (!this.isVisible) {
      this.panel.hidden = true;
    }
  },

  getMouseTargetRect() {
    let container = this.panel.parentNode;
    let panelRect = window.windowUtils.getBoundsWithoutFlushing(this.panel);
    let containerRect = window.windowUtils.getBoundsWithoutFlushing(container);

    return {
      top: panelRect.top,
      bottom: panelRect.bottom,
      left: RTL_UI ? containerRect.right - panelRect.width : containerRect.left,
      right: RTL_UI
        ? containerRect.right
        : containerRect.left + panelRect.width,
    };
  },

  onMouseEnter() {
    this._mirror();
  },

  onMouseLeave() {
    this._mirror();
  },

  _mirror() {
    if (this._frozen) {
      return;
    }
    if (this.panel.hasAttribute("mirror")) {
      this.panel.removeAttribute("mirror");
    } else {
      this.panel.setAttribute("mirror", "true");
    }

    if (!this.panel.hasAttribute("sizelimit")) {
      this.panel.setAttribute("sizelimit", "true");
    }
  },
};

var TabBarVisibility = {
  _initialUpdateDone: false,

  update() {
    let toolbar = document.getElementById("TabsToolbar");
    let collapse = false;
    if (
      !gBrowser /* gBrowser isn't initialized yet */ ||
      gBrowser.visibleTabs.length == 1
    ) {
      collapse = !window.toolbar.visible;
    }

    if (collapse == toolbar.collapsed && this._initialUpdateDone) {
      return;
    }
    this._initialUpdateDone = true;

    toolbar.collapsed = collapse;
    let navbar = document.getElementById("nav-bar");
    navbar.setAttribute("tabs-hidden", collapse);

    document.getElementById("menu_closeWindow").hidden = collapse;
    document.l10n.setAttributes(
      document.getElementById("menu_close"),
      collapse ? "tabbrowser-menuitem-close" : "tabbrowser-menuitem-close-tab"
    );

    TabsInTitlebar.allowedBy("tabs-visible", !collapse);
  },
};

var TabContextMenu = {
  contextTab: null,
  _updateToggleMuteMenuItems(aTab, aConditionFn) {
    ["muted", "soundplaying"].forEach(attr => {
      if (!aConditionFn || aConditionFn(attr)) {
        if (aTab.hasAttribute(attr)) {
          aTab.toggleMuteMenuItem.setAttribute(attr, "true");
          aTab.toggleMultiSelectMuteMenuItem.setAttribute(attr, "true");
        } else {
          aTab.toggleMuteMenuItem.removeAttribute(attr);
          aTab.toggleMultiSelectMuteMenuItem.removeAttribute(attr);
        }
      }
    });
  },
  updateContextMenu(aPopupMenu) {
    let tab =
      aPopupMenu.triggerNode &&
      (aPopupMenu.triggerNode.tab || aPopupMenu.triggerNode.closest("tab"));

    this.contextTab = tab || gBrowser.selectedTab;
    this.contextTab.addEventListener("TabAttrModified", this);
    aPopupMenu.addEventListener("popuphiding", this);

    let disabled = gBrowser.tabs.length == 1;
    let multiselectionContext = this.contextTab.multiselected;
    let tabCountInfo = JSON.stringify({
      tabCount: (multiselectionContext && gBrowser.multiSelectedTabsCount) || 1,
    });

    var menuItems = aPopupMenu.getElementsByAttribute(
      "tbattr",
      "tabbrowser-multiple"
    );
    for (let menuItem of menuItems) {
      menuItem.disabled = disabled;
    }

    disabled = gBrowser.visibleTabs.length == 1;
    menuItems = aPopupMenu.getElementsByAttribute(
      "tbattr",
      "tabbrowser-multiple-visible"
    );
    for (let menuItem of menuItems) {
      menuItem.disabled = disabled;
    }

    // Session store
    document.getElementById("context_undoCloseTab").disabled =
      SessionStore.getClosedTabCountForWindow(window) == 0;

    // Show/hide fullscreen context menu items and set the
    // autohide item's checked state to mirror the autohide pref.
    showFullScreenViewContextMenuItems(aPopupMenu);

    // Only one of Reload_Tab/Reload_Selected_Tabs should be visible.
    document.getElementById("context_reloadTab").hidden = multiselectionContext;
    document.getElementById("context_reloadSelectedTabs").hidden =
      !multiselectionContext;

    // Show Play Tab menu item if the tab has attribute activemedia-blocked
    document.getElementById("context_playTab").hidden = !(
      this.contextTab.activeMediaBlocked && !multiselectionContext
    );
    document.getElementById("context_playSelectedTabs").hidden = !(
      this.contextTab.activeMediaBlocked && multiselectionContext
    );

    // Only one of pin/unpin/multiselect-pin/multiselect-unpin should be visible
    let contextPinTab = document.getElementById("context_pinTab");
    contextPinTab.hidden = this.contextTab.pinned || multiselectionContext;
    let contextUnpinTab = document.getElementById("context_unpinTab");
    contextUnpinTab.hidden = !this.contextTab.pinned || multiselectionContext;
    let contextPinSelectedTabs = document.getElementById(
      "context_pinSelectedTabs"
    );
    contextPinSelectedTabs.hidden =
      this.contextTab.pinned || !multiselectionContext;
    let contextUnpinSelectedTabs = document.getElementById(
      "context_unpinSelectedTabs"
    );
    contextUnpinSelectedTabs.hidden =
      !this.contextTab.pinned || !multiselectionContext;

    // Move Tab items
    let contextMoveTabOptions = document.getElementById(
      "context_moveTabOptions"
    );
    contextMoveTabOptions.setAttribute("data-l10n-args", tabCountInfo);
    contextMoveTabOptions.disabled =
      this.contextTab.hidden || gBrowser.allTabsSelected();
    let selectedTabs = gBrowser.selectedTabs;
    let contextMoveTabToEnd = document.getElementById("context_moveToEnd");
    let allSelectedTabsAdjacent = selectedTabs.every(
      (element, index, array) => {
        return array.length > index + 1
          ? element._tPos + 1 == array[index + 1]._tPos
          : true;
      }
    );
    let contextTabIsSelected = this.contextTab.multiselected;
    let visibleTabs = gBrowser.visibleTabs;
    let lastVisibleTab = visibleTabs[visibleTabs.length - 1];
    let tabsToMove = contextTabIsSelected ? selectedTabs : [this.contextTab];
    let lastTabToMove = tabsToMove[tabsToMove.length - 1];

    let isLastPinnedTab = false;
    if (lastTabToMove.pinned) {
      let sibling = gBrowser.tabContainer.findNextTab(lastTabToMove);
      isLastPinnedTab = !sibling || !sibling.pinned;
    }
    contextMoveTabToEnd.disabled =
      (lastTabToMove == lastVisibleTab || isLastPinnedTab) &&
      allSelectedTabsAdjacent;
    let contextMoveTabToStart = document.getElementById("context_moveToStart");
    let isFirstTab =
      tabsToMove[0] == visibleTabs[0] ||
      tabsToMove[0] == visibleTabs[gBrowser._numPinnedTabs];
    contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;

    if (this.contextTab.hasAttribute("customizemode")) {
      document.getElementById("context_openTabInWindow").disabled = true;
    }

    // Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
    document.getElementById("context_duplicateTab").hidden =
      multiselectionContext;
    document.getElementById("context_duplicateTabs").hidden =
      !multiselectionContext;

    // Disable "Close Tabs to the Left/Right" if there are no tabs
    // preceding/following it.
    let closeTabsToTheStartItem = document.getElementById(
      "context_closeTabsToTheStart"
    );
    let noTabsToStart = !gBrowser.getTabsToTheStartFrom(this.contextTab).length;
    closeTabsToTheStartItem.disabled = noTabsToStart;
    let closeTabsToTheEndItem = document.getElementById(
      "context_closeTabsToTheEnd"
    );
    let noTabsToEnd = !gBrowser.getTabsToTheEndFrom(this.contextTab).length;
    closeTabsToTheEndItem.disabled = noTabsToEnd;

    // Disable "Close other Tabs" if there are no unpinned tabs.
    let unpinnedTabsToClose = multiselectionContext
      ? gBrowser.visibleTabs.filter(t => !t.multiselected && !t.pinned).length
      : gBrowser.visibleTabs.filter(t => t != this.contextTab && !t.pinned)
          .length;
    let closeOtherTabsItem = document.getElementById("context_closeOtherTabs");
    closeOtherTabsItem.disabled = unpinnedTabsToClose < 1;

    // Update the close item with how many tabs will close.
    document
      .getElementById("context_closeTab")
      .setAttribute("data-l10n-args", tabCountInfo);

    // Disable "Close Multiple Tabs" if all sub menuitems are disabled
    document.getElementById("context_closeTabOptions").disabled =
      closeTabsToTheStartItem.disabled &&
      closeTabsToTheEndItem.disabled &&
      closeOtherTabsItem.disabled;

    // Hide "Bookmark Tab…" for multiselection.
    // Update its state if visible.
    let bookmarkTab = document.getElementById("context_bookmarkTab");
    bookmarkTab.hidden = multiselectionContext;

    // Show "Bookmark Selected Tabs" in a multiselect context and hide it otherwise.
    let bookmarkMultiSelectedTabs = document.getElementById(
      "context_bookmarkSelectedTabs"
    );
    bookmarkMultiSelectedTabs.hidden = !multiselectionContext;

    let toggleMute = document.getElementById("context_toggleMuteTab");
    let toggleMultiSelectMute = document.getElementById(
      "context_toggleMuteSelectedTabs"
    );

    // Only one of mute_unmute_tab/mute_unmute_selected_tabs should be visible
    toggleMute.hidden = multiselectionContext;
    toggleMultiSelectMute.hidden = !multiselectionContext;

    const isMuted = this.contextTab.hasAttribute("muted");
    document.l10n.setAttributes(
      toggleMute,
      isMuted ? "tabbrowser-context-unmute-tab" : "tabbrowser-context-mute-tab"
    );
    document.l10n.setAttributes(
      toggleMultiSelectMute,
      isMuted
        ? "tabbrowser-context-unmute-selected-tabs"
        : "tabbrowser-context-mute-selected-tabs"
    );

    this.contextTab.toggleMuteMenuItem = toggleMute;
    this.contextTab.toggleMultiSelectMuteMenuItem = toggleMultiSelectMute;
    this._updateToggleMuteMenuItems(this.contextTab);

    let selectAllTabs = document.getElementById("context_selectAllTabs");
    selectAllTabs.disabled = gBrowser.allTabsSelected();

    gSync.updateTabContextMenu(aPopupMenu, this.contextTab);

    let reopenInContainer = document.getElementById(
      "context_reopenInContainer"
    );
    reopenInContainer.hidden =
      !Services.prefs.getBoolPref("privacy.userContext.enabled", false) ||
      PrivateBrowsingUtils.isWindowPrivate(window);
    reopenInContainer.disabled = this.contextTab.hidden;

    gShareUtils.updateShareURLMenuItem(
      this.contextTab.linkedBrowser,
      document.getElementById("context_sendTabToDevice")
    );
  },

  handleEvent(aEvent) {
    switch (aEvent.type) {
      case "popuphiding":
        if (aEvent.target.id == "tabContextMenu") {
          this.contextTab.removeEventListener("TabAttrModified", this);
        }
        break;
      case "TabAttrModified":
        let tab = aEvent.target;
        this._updateToggleMuteMenuItems(tab, attr =>
          aEvent.detail.changed.includes(attr)
        );
        break;
    }
  },

  createReopenInContainerMenu(event) {
    createUserContextMenu(event, {
      isContextMenu: true,
      excludeUserContextId: this.contextTab.getAttribute("usercontextid"),
    });
  },
  duplicateSelectedTabs() {
    let tabsToDuplicate = gBrowser.selectedTabs;
    let newIndex = tabsToDuplicate[tabsToDuplicate.length - 1]._tPos + 1;
    for (let tab of tabsToDuplicate) {
      let newTab = SessionStore.duplicateTab(window, tab);
      gBrowser.moveTabTo(newTab, newIndex++);
    }
  },
  reopenInContainer(event) {
    let userContextId = parseInt(
      event.target.getAttribute("data-usercontextid")
    );
    let reopenedTabs = this.contextTab.multiselected
      ? gBrowser.selectedTabs
      : [this.contextTab];

    for (let tab of reopenedTabs) {
      if (tab.getAttribute("usercontextid") == userContextId) {
        continue;
      }

      /* Create a triggering principal that is able to load the new tab
         For content principals that are about: chrome: or resource: we need system to load them.
         Anything other than system principal needs to have the new userContextId.
      */
      let triggeringPrincipal;

      if (tab.linkedPanel) {
        triggeringPrincipal = tab.linkedBrowser.contentPrincipal;
      } else {
        // For lazy tab browsers, get the original principal
        // from SessionStore
        let tabState = JSON.parse(SessionStore.getTabState(tab));
        try {
          triggeringPrincipal = E10SUtils.deserializePrincipal(
            tabState.triggeringPrincipal_base64
          );
        } catch (ex) {
          continue;
        }
      }

      if (!triggeringPrincipal || triggeringPrincipal.isNullPrincipal) {
        // Ensure that we have a null principal if we couldn't
        // deserialize it (for lazy tab browsers) ...
        // This won't always work however is safe to use.
        triggeringPrincipal =
          Services.scriptSecurityManager.createNullPrincipal({ userContextId });
      } else if (triggeringPrincipal.isContentPrincipal) {
        triggeringPrincipal = Services.scriptSecurityManager.principalWithOA(
          triggeringPrincipal,
          {
            userContextId,
          }
        );
      }

      let newTab = gBrowser.addTab(tab.linkedBrowser.currentURI.spec, {
        userContextId,
        pinned: tab.pinned,
        index: tab._tPos + 1,
        triggeringPrincipal,
      });

      if (gBrowser.selectedTab == tab) {
        gBrowser.selectedTab = newTab;
      }
      if (tab.muted && !newTab.muted) {
        newTab.toggleMuteAudio(tab.muteReason);
      }
    }
  },

  closeContextTabs(event) {
    if (this.contextTab.multiselected) {
      gBrowser.removeMultiSelectedTabs();
    } else {
      gBrowser.removeTab(this.contextTab, { animate: true });
    }
  },
};
