Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/browser/components/sidebar/   (Firefox Browser Version 153.0.1©)  Datei vom 27.6.2026 mit Größe 88 kB image not shown  

Quelle  browser-sidebar.js

  Sprache: JAVA
 

/* 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/. */


/**
 * SidebarController handles logic such as toggling sidebar panels,
 * dynamically adding menubar menu items for the View -> Sidebar menu,
 * and provides APIs for sidebar extensions, etc.
 */


const { DeferredTask } = ChromeUtils.importESModule(
  "resource://gre/modules/DeferredTask.sys.mjs"
);

const toolsNameMap = {
  viewGenaiChatSidebar: "aichat",
  viewGenaiPageAssistSidebar: "aipageassist",
  viewTabsSidebar: "syncedtabs",
  viewHistorySidebar: "history",
  viewBookmarksSidebar: "bookmarks",
  viewOpenTabsSidebar: "opentabs",
  viewCPMSidebar: "passwords",
};
const EXPAND_ON_HOVER_DEBOUNCE_TIMEOUT_MS = 1000;
const LAUNCHER_SPLITTER_WIDTH = 4;

var SidebarController = {
  makeSidebar({ elementId, ...rest }, commandID) {
    const sidebar = {
      get sourceL10nEl() {
        return document.getElementById(elementId);
      },
      get title() {
        let element = document.getElementById(elementId);
        return element?.getAttribute("label");
      },
      ...rest,
    };

    const toolID = toolsNameMap[commandID];
    if (toolID) {
      XPCOMUtils.defineLazyPreferenceGetter(
        sidebar,
        "attention",
        `sidebar.notification.badge.${toolID}`,
        false,
        (_pref, _prev) => this.handleToolBadges(toolID)
      );
      sidebar.attention;
    }

    return sidebar;
  },

  registerPrefSidebar(pref, commandID, config) {
    const sidebar = this.makeSidebar(config, commandID);
    this._sidebars.set(commandID, sidebar);

    let switcherMenuitem;
    sidebar._updateMenus = () => {
      const { visible } = sidebar;
      // Hide the sidebar if it is open and should not be visible,
      // and unset the current command and lastOpenedId so they do not
      // re-open the next time the sidebar does.
      if (!visible && this._state.command == commandID) {
        this._state.command = "";
        this.lastOpenedId = null;
        this.hide();
      }

      // Update visibility of View -> Sidebar menu item.
      const viewItem = document.getElementById(sidebar.menuId);
      if (viewItem) {
        viewItem.hidden = !visible;
      }

      let menuItem = document.getElementById(config.elementId);
      // Add/remove switcher menu item.
      if (visible && !menuItem) {
        switcherMenuitem = this.createMenuItem(commandID, sidebar);
        switcherMenuitem.setAttribute("id", config.elementId);
        switcherMenuitem.removeAttribute("type");
        const separator = this._switcherPanel.querySelector("menuseparator");
        separator.parentNode.insertBefore(switcherMenuitem, separator);
      } else {
        switcherMenuitem?.remove();
      }

      window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
    };

    // Detect pref changes and handle initial state.
    XPCOMUtils.defineLazyPreferenceGetter(
      sidebar,
      "_prefVisible",
      pref,
      false,
      () => sidebar._updateMenus?.()
    );
    Object.defineProperty(sidebar, "visible", {
      get: () =>
        sidebar._prefVisible && !(config.hideInAIWindow && this.isAIWindow()),
      configurable: true,
    });
    this.promiseInitialized.then(() => sidebar._updateMenus?.());
  },

  isAIWindow() {
    return this.AIWindow.isAIWindowActive(window);
  },

  get sidebars() {
    if (this._sidebars) {
      return this._sidebars;
    }

    return this.generateSidebarsMap();
  },

  generateSidebarsMap() {
    this._sidebars = new Map([
      [
        "viewHistorySidebar",
        this.makeSidebar({
          name: "history",
          elementId: "sidebar-switcher-history",
          url: this.sidebarRevampEnabled
            ? "chrome://browser/content/sidebar/sidebar-history.html"
            : "chrome://browser/content/places/historySidebar.xhtml",
          menuId: "menu_historySidebar",
          triggerButtonId: "appMenuViewHistorySidebar",
          keyId: "key_gotoHistory",
          menuL10nId: "menu-view-history-button",
          revampL10nId: "sidebar-menu-history-label",
          iconUrl: "chrome://browser/skin/history.svg",
          contextMenuId: this.sidebarRevampEnabled
            ? "sidebar-history-context-menu"
            : undefined,
          gleanEvent: Glean.history.sidebarToggle,
          gleanClickEvent: Glean.sidebar.historyIconClick,
          recordSidebarVersion: true,
        }),
      ],
      [
        "viewTabsSidebar",
        this.makeSidebar({
          name: "syncedtabs",
          elementId: "sidebar-switcher-tabs",
          url: this.sidebarRevampEnabled
            ? "chrome://browser/content/sidebar/sidebar-syncedtabs.html"
            : "chrome://browser/content/syncedtabs/sidebar.xhtml",
          menuId: "menu_tabsSidebar",
          classAttribute: "sync-ui-item",
          menuL10nId: "menu-view-synced-tabs-sidebar",
          revampL10nId: "sidebar-menu-synced-tabs-label",
          iconUrl: "chrome://browser/skin/synced-tabs.svg",
          contextMenuId: this.sidebarRevampEnabled
            ? "sidebar-synced-tabs-context-menu"
            : undefined,
          gleanClickEvent: Glean.sidebar.syncedTabsIconClick,
        }),
      ],
      [
        "viewBookmarksSidebar",
        this.makeSidebar({
          name: "bookmarks",
          elementId: "sidebar-switcher-bookmarks",
          url:
            this.sidebarRevampEnabled && this.updatedBookmarksPanelEnabled
              ? "chrome://browser/content/sidebar/sidebar-bookmarks.html"
              : "chrome://browser/content/places/bookmarksSidebar.xhtml",
          menuId: "menu_bookmarksSidebar",
          keyId: "viewBookmarksSidebarKb",
          menuL10nId: "menu-view-bookmarks",
          revampL10nId: "sidebar-menu-bookmarks-label",
          iconUrl: "chrome://browser/skin/bookmark-hollow.svg",
          contextMenuId: this.sidebarRevampEnabled
            ? "sidebar-bookmarks-context-menu"
            : undefined,
          disabled: true,
          gleanEvent: Glean.bookmarks.sidebarToggle,
          gleanClickEvent: Glean.sidebar.bookmarksIconClick,
          recordSidebarVersion: true,
        }),
      ],
    ]);

    this.registerPrefSidebar(
      "browser.ml.chat.enabled",
      "viewGenaiChatSidebar",
      {
        name: "aichat",
        elementId: "sidebar-switcher-genai-chat",
        url: "chrome://browser/content/genai/chat.html",
        keyId: "viewGenaiChatSidebarKb",
        menuId: "menu_genaiChatSidebar",
        menuL10nId: "menu-view-genai-chat",
        // Bug 1900915 to expose as conditional tool
        revampL10nId: "sidebar-menu-genai-chat-label",
        iconUrl: "chrome://global/skin/icons/highlights.svg",
        gleanClickEvent: Glean.sidebar.chatbotIconClick,
        toolContextMenuId: "aichat",
        permissions: true,
        hideInAIWindow: true,
      }
    );

    this.registerPrefSidebar(
      "browser.ml.pageAssist.enabled",
      "viewGenaiPageAssistSidebar",
      {
        name: "aipageassist",
        elementId: "sidebar-switcher-genai-page-assist",
        url: "chrome://browser/content/genai/pageAssist.html",
        menuId: "menu_genaiPageAssistSidebar",
        menuL10nId: "menu-view-genai-page-assist",
        revampL10nId: "sidebar-menu-genai-page-assist-label",
        iconUrl: "chrome://browser/skin/reader-mode.svg",
      }
    );

    this.registerPrefSidebar(
      "browser.contextual-password-manager.enabled",
      "viewCPMSidebar",
      {
        name: "passwords",
        elementId: "sidebar-switcher-megalist",
        url: "chrome://global/content/megalist/megalist.html",
        menuId: "menu_megalistSidebar",
        menuL10nId: "menu-view-contextual-password-manager",
        revampL10nId: "sidebar-menu-contextual-password-manager-label",
        iconUrl: "chrome://browser/skin/login.svg",
        gleanEvent: Glean.contextualManager.sidebarToggle,
        gleanClickEvent: Glean.sidebar.passwordsIconClick,
        recordSidebarVersion: true,
      }
    );

    if (this.sidebarRevampEnabled) {
      this.registerPrefSidebar(
        "sidebar.openTabsPanel.enabled",
        "viewOpenTabsSidebar",
        {
          name: "opentabs",
          elementId: "sidebar-switcher-opentabs",
          url: "chrome://browser/content/sidebar/sidebar-opentabs.html",
          menuId: "menu_openTabsSidebar",
          keyId: "viewOpenTabsSidebarKb",
          menuL10nId: "menu-view-open-tabs",
          revampL10nId: "sidebar-menu-open-tabs-label",
          iconUrl: "chrome://browser/content/firefoxview/view-opentabs.svg",
        }
      );
    }

    if (this.sidebarRevampEnabled) {
      this._sidebars.set("viewCustomizeSidebar", {
        url: "chrome://browser/content/sidebar/sidebar-customize.html",
        revampL10nId: "sidebar-menu-customize-label",
        iconUrl: "chrome://global/skin/icons/settings.svg",
        gleanEvent: Glean.sidebarCustomize.panelToggle,
      });
    }

    return this._sidebars;
  },

  /**
   * Returns a map of tools and extensions for use in the sidebar
   */

  get toolsAndExtensions() {
    if (this._toolsAndExtensions) {
      return this._toolsAndExtensions;
    }

    this._toolsAndExtensions = new Map();
    this.getTools().forEach(tool => {
      this._toolsAndExtensions.set(tool.commandID, tool);
    });
    this.getExtensions().forEach(extension => {
      this._toolsAndExtensions.set(extension.commandID, extension);
    });
    return this._toolsAndExtensions;
  },

  // Avoid getting the browser element from init() to avoid triggering the
  // <browser> constructor during startup if the sidebar is hidden.
  get browser() {
    if (this._browser) {
      return this._browser;
    }
    return (this._browser = document.getElementById("sidebar"));
  },
  POSITION_START_PREF: "sidebar.position_start",
  DEFAULT_SIDEBAR_ID: "viewBookmarksSidebar",
  VISIBILITY_PREF: "sidebar.visibility",
  TOOLS_PREF: "sidebar.main.tools",
  INSTALLED_EXTENSIONS: "sidebar.installed.extensions",

  // lastOpenedId is set in show() but unlike currentID it's not cleared out on hide
  // and isn't persisted across windows
  lastOpenedId: null,

  _box: null,
  _pinnedTabsContainer: null,
  _pinnedTabsItemsWrapper: null,
  // The constructor of this label accesses the browser element due to the
  // control="sidebar" attribute, so avoid getting this label during startup.
  get _title() {
    if (this.__title) {
      return this.__title;
    }
    return (this.__title = document.getElementById("sidebar-title"));
  },
  _splitter: null,
  _reversePositionButton: null,
  _switcherPanel: null,
  _switcherTarget: null,
  _switcherArrow: null,
  _inited: false,
  _uninitializing: false,
  _switcherListenersAdded: false,
  _verticalNewTabListenerAdded: false,
  _localesObserverAdded: false,
  _mainResizeObserverAdded: false,
  _aiWindowObserverAdded: false,
  _windowRestoredObserverAdded: false,
  _mainResizeObserver: null,
  _ongoingAnimations: [],

  /**
   * @type {MutationObserver | null}
   */

  _observer: null,

  _initDeferred: Promise.withResolvers(),

  _initialUIStateUpdated: false,

  /**
   * Latched true synchronously by SessionStore.restoreSidebar before it
   * kicks off updateUIState. Tells startDelayedLoad that session restore is
   * providing state for this window and it should wait for the per-window
   * sessionstore-single-window-restored notification instead of loading
   * backup state. Once set, stays set for the life of this controller.
   */

  _sessionRestoreStateReceived: false,

  markSessionRestoreStateReceived() {
    this._sessionRestoreStateReceived = true;
  },

  // A promise resolved once the Sidebar has rendered its initial state.
  get promiseInitialized() {
    return this._initDeferred.promise;
  },

  get initialized() {
    return this._inited;
  },

  get uninitializing() {
    return this._uninitializing;
  },

  get inSingleTabWindow() {
    return (
      !window.toolbar.visible ||
      window.document.documentElement.hasAttribute("taskbartab")
    );
  },

  get sidebarContainer() {
    if (!this._sidebarContainer) {
      // This is the *parent* of the `sidebar-main` component. Its ID is "sidebar-container
      this._sidebarContainer = document.getElementById("sidebar-container");
    }
    return this._sidebarContainer;
  },

  get sidebarMain() {
    if (!this._sidebarMain) {
      this._sidebarMain = document.querySelector("sidebar-main");
    }
    return this._sidebarMain;
  },

  get contentArea() {
    if (!this._contentArea) {
      this._contentArea = document.getElementById("tabbrowser-tabbox");
    }
    return this._contentArea;
  },

  get toolbarButton() {
    if (!this._toolbarButton) {
      this._toolbarButton = document.getElementById("sidebar-button");
    }
    return this._toolbarButton;
  },

  get isLauncherDragging() {
    return this._launcherSplitter.getAttribute("state") === "dragging";
  },

  get isPinnedTabsDragging() {
    return this._pinnedTabsSplitter.getAttribute("state") === "dragging";
  },

  get sidebarTools() {
    return this.sidebarRevampTools ? this.sidebarRevampTools.split(",") : [];
  },

  get sidebarExtensions() {
    return this.installedExtensions ? this.installedExtensions.split(",") : [];
  },

  get launcherSplitter() {
    return this._launcherSplitter;
  },

  init() {
    // Initialize global state manager.
    this.SidebarManager;

    // Initialize per-window state manager.
    if (!this._state) {
      this._state = new this.SidebarState(this);
    }

    // Watch for fullscreen transitions to sync the sidebar attribute.
    this._fullscreenObserver = new MutationObserver(() => {
      const inFullscreen =
        document.documentElement.hasAttribute("inDOMFullscreen");
      this._state.fullscreen = inFullscreen;
    });

    this._fullscreenObserver.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["inDOMFullscreen"],
    });
    // Initial check: is the window already in fullscreen when we start?
    this._state.fullscreen =
      document.documentElement.hasAttribute("inDOMFullscreen");
    this._pinnedTabsContainer = document.getElementById(
      "pinned-tabs-container"
    );
    this._pinnedTabsItemsWrapper =
      this._pinnedTabsContainer.shadowRoot.querySelector(
        "[part=items-wrapper]"
      );
    this._box = document.getElementById("sidebar-box");
    this._splitter = document.getElementById("sidebar-splitter");
    this._launcherSplitter = document.getElementById(
      "sidebar-launcher-splitter"
    );
    this._pinnedTabsSplitter = document.getElementById(
      "vertical-pinned-tabs-splitter"
    );
    this._reversePositionButton = document.getElementById(
      "sidebar-reverse-position"
    );
    this._switcherPanel = document.getElementById("sidebarMenu-popup");
    this._switcherTarget = document.getElementById("sidebar-switcher-target");
    this._switcherArrow = document.getElementById("sidebar-switcher-arrow");
    this._hoverBlockerCount = 0;
    this._escapedWhileHovered = false;
    this._mouseLeftSinceEscape = false;
    if (
      Services.prefs.getBoolPref(
        "browser.tabs.allow_transparent_browser",
        false
      )
    ) {
      this.browser.setAttribute("transparent""true");
    }

    const menubar = document.getElementById("viewSidebarMenu");
    const currentMenuItems = new Set(
      Array.from(menubar.childNodes, item => item.id)
    );
    for (const [commandID, sidebar] of this.sidebars.entries()) {
      if (
        !Object.hasOwn(sidebar, "extensionId") &&
        commandID !== "viewCustomizeSidebar" &&
        !currentMenuItems.has(sidebar.menuId)
      ) {
        // registerExtension() already creates menu items for extensions.
        const menuitem = this.createMenuItem(commandID, sidebar);
        menubar.appendChild(menuitem);
      }
    }
    if (this._mainResizeObserver) {
      this._mainResizeObserver.disconnect();
      this._mainResizeObserverAdded = false;
    }
    this._mainResizeObserver = new ResizeObserver(([entry]) =>
      this._handleLauncherResize(entry)
    );

    if (this.sidebarRevampEnabled && !BrowserHandler.kiosk) {
      if (!customElements.get("sidebar-main")) {
        ChromeUtils.importESModule(
          "chrome://browser/content/sidebar/sidebar-main.mjs",
          { global: "current" }
        );
      }
      this._sidebarMainKeydownHandler = e => {
        if (e.key === "Escape") {
          this.collapseOnEscape();
        }
      };
      window.addEventListener("keydown"this._sidebarMainKeydownHandler);
      this.revampComponentsLoaded = true;
      this._state.initializeState(this._showLauncherAfterInit);
      // clear the flag after we've used it
      delete this._showLauncherAfterInit;

      document.getElementById("sidebar-header").hidden = true;
      if (!this._mainResizeObserverAdded) {
        this._mainResizeObserver.observe(this.sidebarMain);
        this._mainResizeObserverAdded = true;
      }
      if (!this._browserResizeObserver) {
        this._browserResizeObserver = () => {
          // Report resize events to Glean.
          const current = this.browser.getBoundingClientRect().width;
          const previous = this._browserWidth;
          const percentage = (current / window.innerWidth) * 100;
          Glean.sidebar.resize.record({
            current: Math.round(current),
            previous: Math.round(previous),
            percentage: Math.round(percentage),
          });
          this._recordBrowserSize();
        };
        this._splitter.addEventListener("command"this._browserResizeObserver);
      }
      this._enablePinnedTabsSplitterDragging();

      // Record Glean metrics.
      this.recordVisibilitySetting();
      this.recordPositionSetting();
      this.recordTabsLayoutSetting();
    } else {
      this._switcherCloseButton = document.getElementById("sidebar-close");
      if (!this._switcherListenersAdded) {
        this._switcherCloseButton.addEventListener("command", () => {
          this.hide();
        });
        this._switcherTarget.addEventListener("command", () => {
          this.toggleSwitcherPanel();
        });
        this._switcherTarget.addEventListener("keydown", event => {
          this.handleKeydown(event);
        });
        this._switcherListenersAdded = true;
      }
      this._disableLauncherDragging();
      this._disablePinnedTabsDragging();
    }
    // We need to update the tab strip for vertical tabs during init
    // as there will be no tabstrip-orientation-change event
    if (CustomizableUI.verticalTabsEnabled) {
      this.toggleTabstrip();
    }

    // sets the sidebar to the left or right, based on a pref
    this.setPosition();

    this._inited = true;

    if (!this._localesObserverAdded) {
      Services.obs.addObserver(this"intl:app-locales-changed");
      this._localesObserverAdded = true;
    }
    if (!this._tabstripOrientationObserverAdded) {
      Services.obs.addObserver(this"tabstrip-orientation-change");
      this._tabstripOrientationObserverAdded = true;
    }
    if (!this._aiWindowObserverAdded) {
      Services.obs.addObserver(this"ai-window-state-changed");
      this._aiWindowObserverAdded = true;
    }
    if (!this._windowRestoredObserverAdded) {
      Services.obs.addObserver(this"sessionstore-single-window-restored");
      this._windowRestoredObserverAdded = true;
    }
  },

  uninit() {
    // Set a flag to allow us to ignore pref changes while the host document is being unloaded.
    this._uninitializing = true;
    if (this._fullscreenObserver) {
      this._fullscreenObserver.disconnect();
      this._fullscreenObserver = null;
    }

    // If this is the last browser window, persist various values that should be
    // remembered for after a restart / reopening a browser window.
    let enumerator = Services.wm.getEnumerator("navigator:browser");
    if (!enumerator.hasMoreElements()) {
      let xulStore = Services.xulStore;
      xulStore.persist(this._title, "value");

      const currentState = this.getUIState();
      this.SidebarManager.setBackupState(currentState);
    }

    if (this._sidebarMainKeydownHandler) {
      window.removeEventListener("keydown"this._sidebarMainKeydownHandler);
      this._sidebarMainKeydownHandler = null;
    }
    this._sidebarMainKeydownHandler = null;
    Services.obs.removeObserver(this"intl:app-locales-changed");
    Services.obs.removeObserver(this"tabstrip-orientation-change");
    Services.obs.removeObserver(this"ai-window-state-changed");
    Services.obs.removeObserver(this"sessionstore-single-window-restored");
    delete this._tabstripOrientationObserverAdded;
    delete this._aiWindowObserverAdded;
    delete this._windowRestoredObserverAdded;

    CustomizableUI.removeListener(this);

    if (this._observer) {
      this._observer.disconnect();
      this._observer = null;
    }

    if (this._mainResizeObserver) {
      this._mainResizeObserver.disconnect();
      this._mainResizeObserver = null;
    }

    if (this.revampComponentsLoaded) {
      // Explicitly disconnect the `sidebar-main` element so that listeners
      // setup by reactive controllers will also be removed.
      this.sidebarMain.remove();
    }
    this._splitter.removeEventListener("command"this._browserResizeObserver);
    this._disableLauncherDragging();
    this._disablePinnedTabsDragging();
  },

  /**
   * Keep track when sidebar.revamp is enabled by the user via about:preferences UI
   *
   * @param {boolean} isEnabled
   */

  enabledViaSettings(isEnabled = false) {
    this._showLauncherAfterInit = isEnabled;
  },

  /**
   * Handle the launcher being resized (either manually or programmatically).
   *
   * @param {ResizeObserverEntry} entry
   */

  _handleLauncherResize(entry) {
    this._state.launcherWidth = entry.contentBoxSize[0].inlineSize;
    if (this.isLauncherDragging) {
      this._state.launcherDragActive = true;
    }
    if (this._state.visibilitySetting === "expand-on-hover") {
      this.setLauncherCollapsedWidth();
    }
  },

  getUIState() {
    if (this.inSingleTabWindow) {
      return null;
    }
    return this._state.getProperties();
  },

  /**
   * Load the UI state information given by session store, backup state, or
   * adopted window.
   *
   * @param {SidebarStateProps} state
   */

  async updateUIState(state = {}) {
    const isValidSidebar = !state.command || this.sidebars.has(state.command);
    if (!isValidSidebar) {
      state.command = "";
    }
    // reset any previously remembered launcher-visibility state
    delete this._launcherStateAtOpen;

    const hasOpenPanel =
      state.panelOpen &&
      state.command &&
      this.sidebars.has(state.command) &&
      this.currentID !== state.command;
    if (hasOpenPanel) {
      // There's a panel to show, so ignore the contradictory hidden property.
      delete state.hidden;
    }
    await this.waitUntilStable(); // Finish currently scheduled tasks.
    await this._state.loadCurrentState(state);
    await this.waitUntilStable(); // Finish newly scheduled tasks.
    this.updateToolbarButton();
    if (this.sidebarRevampVisibility === "expand-on-hover") {
      await this.toggleExpandOnHover(true);
    }
    this._initialUIStateUpdated = true;
  },

  /**
   * Toggle the vertical tabs preference.
   */

  toggleVerticalTabs() {
    Services.prefs.setBoolPref(
      "sidebar.verticalTabs",
      !this.sidebarVerticalTabsEnabled
    );
  },

  /**
   * The handler for Services.obs.addObserver.
   */

  observe(subject, topic, _data) {
    switch (topic) {
      case "intl:app-locales-changed": {
        if (this.isOpen) {
          // The <tree> component used in history and bookmarks, but it does not
          // support live switching the app locale. Reload the entire sidebar to
          // invalidate any old text.
          this.hide({ dismissPanel: false });
          this.showInitially(this.lastOpenedId);
          break;
        }
        if (this.revampComponentsLoaded) {
          this.sidebarMain.requestUpdate();
        }
        break;
      }
      case "sessionstore-single-window-restored": {
        // If Session restore was going to call updateUIState it would have
        // done so by now.
        if (subject == window) {
          this._initDeferred.resolve();
        }
        break;
      }
      case "tabstrip-orientation-change": {
        this.promiseInitialized.then(() => this.toggleTabstrip());
        break;
      }
      case "ai-window-state-changed": {
        if (subject == window) {
          // Update menus and hide sidebars that depend on the type of window
          for (const sidebar of this.sidebars.values()) {
            sidebar._updateMenus?.();
          }
        }
        break;
      }
    }
  },

  /**
   * Ensure the title stays in sync with the source element, which updates for
   * l10n changes.
   *
   * @param {HTMLElement} [element]
   */

  observeTitleChanges(element) {
    if (!element) {
      return;
    }
    let observer = this._observer;
    if (!observer) {
      observer = new MutationObserver(() => {
        // it's possible for lastOpenedId to be null here
        this.title = this.sidebars.get(this.lastOpenedId)?.title;
      });
      // Re-use the observer.
      this._observer = observer;
    }
    observer.disconnect();
    observer.observe(element, {
      attributes: true,
      attributeFilter: ["label"],
    });
  },

  /**
   * Opens the switcher panel if it's closed, or closes it if it's open.
   */

  toggleSwitcherPanel() {
    if (
      this._switcherPanel.state == "open" ||
      this._switcherPanel.state == "showing"
    ) {
      this.hideSwitcherPanel();
    } else if (this._switcherPanel.state == "closed") {
      this.showSwitcherPanel();
    }
  },

  /**
   * Handles keydown on the the switcherTarget button
   *
   * @param  {Event} event
   */

  handleKeydown(event) {
    switch (event.key) {
      case "Enter":
      case " ": {
        this.toggleSwitcherPanel();
        event.stopPropagation();
        event.preventDefault();
        break;
      }
      case "Escape": {
        this.hideSwitcherPanel();
        event.stopPropagation();
        event.preventDefault();
        break;
      }
    }
  },

  hideSwitcherPanel() {
    this._switcherPanel.hidePopup();
  },

  showSwitcherPanel() {
    this._switcherPanel.addEventListener(
      "popuphiding",
      () => {
        this._switcherTarget.classList.remove("active");
        this._switcherTarget.setAttribute("aria-expanded"false);
      },
      { once: true }
    );

    // Combine start/end position with ltr/rtl to set the label in the popup appropriately.
    let label =
      this._positionStart == RTL_UI
        ? gNavigatorBundle.getString("sidebar.moveToLeft")
        : gNavigatorBundle.getString("sidebar.moveToRight");
    this._reversePositionButton.setAttribute("label", label);

    // Open the sidebar switcher popup, anchored off the switcher toggle
    this._switcherPanel.hidden = false;
    this._switcherPanel.openPopup(this._switcherTarget);

    this._switcherTarget.classList.add("active");
    this._switcherTarget.setAttribute("aria-expanded"true);
  },

  updateShortcut({ keyId }) {
    let menuitem = this._switcherPanel?.querySelector(`[key="${keyId}"]`);
    if (!menuitem) {
      // If the menu item doesn't exist yet then the accel text will be set correctly
      // upon creation so there's nothing to do now.
      return;
    }
    menuitem.removeAttribute("acceltext");
  },

  /**
   * Change the pref that will trigger a call to setPosition
   */

  reversePosition() {
    Services.prefs.setBoolPref(this.POSITION_START_PREF, !this._positionStart);
  },

  /**
   * Read the positioning pref and position the sidebar and the splitter
   * appropriately within the browser container.
   */

  setPosition() {
    // First reset all ordinals to match DOM ordering.
    let contentArea = document.getElementById("tabbrowser-tabbox");
    let browser = document.getElementById("browser");
    [...browser.children].forEach((node, i, children) => {
      node.style.order = this._positionStart ? i + 1 : children.length - i;
    });
    let sidebarContainer = document.getElementById("sidebar-container");
    let sidebarMain = document.querySelector("sidebar-main");

    // Indicate we've switched ordering to the box
    this._box.toggleAttribute("sidebar-positionend", !this._positionStart);
    sidebarMain.toggleAttribute("sidebar-positionend", !this._positionStart);
    contentArea.toggleAttribute("sidebar-positionend", !this._positionStart);
    sidebarContainer.toggleAttribute(
      "sidebar-positionend",
      !this._positionStart
    );
    this.toolbarButton &&
      this.toolbarButton.toggleAttribute(
        "sidebar-positionend",
        !this._positionStart
      );

    this.hideSwitcherPanel();

    let content = SidebarController.browser.contentWindow;
    if (content && content.updatePosition) {
      content.updatePosition();
    }
  },

  /**
   * Show/hide new sidebar based on sidebar.revamp pref
   */

  async toggleRevampSidebar() {
    await this.promiseInitialized;
    let wasOpen = this.isOpen;
    if (wasOpen) {
      this.hide({ dismissPanel: false });
    }
    // Reset sidebars map but preserve any existing extensions
    let extensionsArr = [];
    for (const [commandID, sidebar] of this.sidebars.entries()) {
      if (sidebar.hasOwnProperty("extensionId")) {
        extensionsArr.push({ commandID, sidebar });
      }
    }
    this.sidebars = this.generateSidebarsMap();
    for (const extension of extensionsArr) {
      this.sidebars.set(extension.commandID, extension.sidebar);
    }
    if (!this.sidebarRevampEnabled) {
      this._state.launcherVisible = false;
      document.getElementById("sidebar-header").hidden = false;

      // Ensure CPM isn't shown.
      const cpmMenuItem = document.querySelector("#sidebar-switcher-megalist");
      this.lastOpenedId = this.DEFAULT_SIDEBAR_ID;
      cpmMenuItem.hidden = true;
    }
    if (!this._sidebars.get(this.lastOpenedId)) {
      this.lastOpenedId = this.DEFAULT_SIDEBAR_ID;
      wasOpen = false;
    }
    this.updateToolbarButton();
    this._inited = false;
    this.init();

    // Reopen the panel in the new or old sidebar now that we've inited
    if (wasOpen) {
      this.toggle();
    }
  },

  /**
   * Try and adopt the status of the sidebar from another window.
   *
   * @param {Window} sourceWindow - Window to use as a source for sidebar status.
   * @returns {boolean} true if we adopted the state, or false if the caller should
   * initialize the state itself.
   */

  getAdoptedStateFromWindow(sourceWindow) {
    // If the opener had a sidebar, open the same sidebar in our window.
    // The opener can be the hidden window too, if we're coming from the state
    // where no windows are open, and the hidden window has no sidebar box.
    let sourceController = sourceWindow.SidebarController;
    if (!sourceController || !sourceController._box) {
      // no source UI or no _box means we also can't adopt the state.
      return null;
    }

    // Adopt the other window's UI state (it too could be a popup)
    // We get the properties directly forom the SidebarState instance as in this case
    // we need the command property even if no panel is currently open.
    const sourceState = sourceController.inPopup
      ? null
      : sourceController._state?.getProperties();
    return sourceState;
  },

  windowPrivacyMatches(w1, w2) {
    return (
      PrivateBrowsingUtils.isWindowPrivate(w1) ===
      PrivateBrowsingUtils.isWindowPrivate(w2)
    );
  },

  /**
   * If loading a sidebar was delayed on startup, start the load now.
   */

  async startDelayedLoad() {
    if (this.inSingleTabWindow) {
      this._state.launcherVisible = false;
      this._initDeferred.resolve();
      return;
    }

    let sourceWindow = window.opener;
    // No source window means this is the initial window.  If we're being
    // opened from another window, check that it is one we might open a sidebar
    // for.
    if (sourceWindow) {
      if (
        sourceWindow.closed ||
        sourceWindow.location.protocol != "chrome:" ||
        (!this.sidebarRevampEnabled &&
          !this.windowPrivacyMatches(sourceWindow, window))
      ) {
        this._initDeferred.resolve();
        return;
      }
      // Case: opened from a chrome window — adopt state from opener.
      let stateToApply = this.getAdoptedStateFromWindow(sourceWindow);
      if (stateToApply) {
        await this.updateUIState(stateToApply);
        this._initDeferred.resolve();
        return;
      }
    }

    // Case: SessionStore has handed us state via restoreSidebar. The
    // sessionstore-single-window-restored observer will resolve
    // _initDeferred once SessionStore has finished applying it.
    if (this._sessionRestoreStateReceived) {
      return;
    }

    // Case: SessionStore already finished restoring us before startDelayedLoad
    // ran (single-window startup, session restore slot-fill).
    if (this._initialUIStateUpdated) {
      this._initDeferred.resolve();
      return;
    }

    // Case: no upstream source. Load backup state as fallback.
    const backupState = this.SidebarManager.getBackupState();

    // If we're not adopting settings from a parent window, set them now.
    let wasOpen = this._box.getAttribute("checked");
    if (wasOpen) {
      let commandID = this._state.command;

      if (wasOpen && commandID && this.sidebars.has(commandID)) {
        this.showInitially(commandID);
      } else {
        this._box.removeAttribute("checked");
        // Update the state, because the element it
        // refers to no longer exists, so we should assume this sidebar
        // panel has been uninstalled. (249883)
        this._state.command = "";
        // On a startup in which the startup cache was invalidated (e.g. app update)
        // extensions will not be started prior to delayedLoad, thus the
        // sidebarcommand element will not exist yet.  Store the commandID so
        // extensions may reopen if necessary.  A startup cache invalidation
        // can be forced (for testing) by deleting compatibility.ini from the
        // profile.
        this.lastOpenedId = commandID;
      }
    }
    await this.updateUIState(backupState);
    this._initDeferred.resolve();
  },

  /**
   * Fire a "SidebarShown" event on the sidebar to give any interested parties
   * a chance to update the button or whatever.
   */

  _fireShowEvent() {
    let event = new CustomEvent("SidebarShown", { bubbles: true });
    this._switcherTarget.dispatchEvent(event);
  },

  /**
   * Report the current browser width to Glean, and store it internally.
   */

  _recordBrowserSize() {
    this._browserWidth = this.browser.getBoundingClientRect().width;
    Glean.sidebar.width.set(this._browserWidth);
  },

  /**
   * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
   * a chance to adjust focus as needed. An additional event is needed, because
   * we don't want to focus the sidebar when it's opened on startup or in a new
   * window, only when the user opens the sidebar.
   */

  _fireFocusedEvent() {
    let event = new CustomEvent("SidebarFocused", { bubbles: true });
    this.browser.contentWindow.dispatchEvent(event);
  },

  /**
   * True if the sidebar is currently open.
   */

  get isOpen() {
    return this._box ? !this._box.hidden : false;
  },

  /**
   * The ID of the current sidebar.
   */

  get currentID() {
    return this.isOpen ? this._state.command : "";
  },

  /**
   * The context menu of the current sidebar.
   */

  get currentContextMenu() {
    const sidebar = this.sidebars.get(this.currentID);
    if (!sidebar) {
      return null;
    }
    return document.getElementById(sidebar.contextMenuId);
  },

  get launcherVisible() {
    return this._state?.launcherVisible;
  },

  get launcherEverVisible() {
    return this._state?.launcherEverVisible;
  },

  get title() {
    return this._title.value;
  },

  set title(value) {
    this._title.value = value;
  },

  /**
   * Toggle the visibility of the sidebar. If the sidebar is hidden or is open
   * with a different commandID, then the sidebar will be opened using the
   * specified commandID. Otherwise the sidebar will be hidden.
   *
   * @param  {string}  commandID     ID of the sidebar.
   * @param  {DOMNode} [triggerNode] Node, usually a button, that triggered the
   *                                 visibility toggling of the sidebar.
   * @returns {Promise}
   */

  toggle(commandID = this.lastOpenedId, triggerNode) {
    if (
      CustomizationHandler.isCustomizing() ||
      CustomizationHandler.isExitingCustomizeMode
    ) {
      return Promise.resolve();
    }
    // First priority for a default value is this.lastOpenedId which is set during show()
    // and not reset in hide(), unlike currentID. If show() hasn't been called and we don't
    // have a persisted command either, or the command doesn't exist anymore, then
    // fallback to a default sidebar.
    if (!commandID) {
      commandID = this._state.command;
    }
    if (!commandID || !this.sidebars.has(commandID)) {
      if (this.sidebarRevampEnabled && this.sidebars.size) {
        commandID = this.sidebars.keys().next().value;
      } else {
        commandID = this.DEFAULT_SIDEBAR_ID;
      }
    }

    if (this.isOpen && commandID == this.currentID) {
      // Revamp sidebar: this case is a dismissal of the current sidebar panel. The launcher should stay open
      // For legacy sidebar, this is a "sidebar" toggle and the current panel should be remembered
      this.hide({ triggerNode, dismissPanel: this.sidebarRevampEnabled });
      this.updateToolbarButton();
      return Promise.resolve();
    }

    if (!this.sidebarRevampEnabled) {
      const cpmMenuItem = document.querySelector("#sidebar-switcher-megalist");
      this.lastOpenedId = this.DEFAULT_SIDEBAR_ID;
      cpmMenuItem.hidden = true;
    }

    return this.show(commandID, triggerNode);
  },

  _getRects(animatingElements) {
    return animatingElements.map(e => [
      e.hidden,
      e.getBoundingClientRect().toJSON(),
    ]);
  },

  /**
   * Wait for Lit updates and ongoing animations to complete.
   *
   * @returns {Promise}
   */

  async waitUntilStable() {
    if (!this.sidebarRevampEnabled) {
      // Legacy sidebar doesn't have animations, nothing to await.
      return null;
    }
    const tasks = [this.sidebarMain.updateComplete];
    if (this._ongoingAnimations?.length) {
      tasks.push(
        ...this._ongoingAnimations.map(animation => animation.finished)
      );
    }
    return Promise.allSettled(tasks);
  },

  async _animateSidebarContainer() {
    let tabbox = document.getElementById("tabbrowser-tabbox");
    let animatingElements;
    let expandOnHoverEnabled = document.documentElement.hasAttribute(
      "sidebar-expand-on-hover"
    );
    if (expandOnHoverEnabled) {
      animatingElements = [this.sidebarContainer];

      this._addHoverStateBlocker();
    } else {
      animatingElements = [
        this.sidebarContainer,
        this._box,
        this._splitter,
        tabbox,
      ];
    }
    let resetElements = () => {
      for (let el of animatingElements) {
        el.style.minWidth =
          el.style.maxWidth =
          el.style.marginLeft =
          el.style.marginRight =
          el.style.display =
            "";
      }
      this.sidebarContainer.toggleAttribute(
        "sidebar-ongoing-animations",
        false
      );
      this._box.toggleAttribute("sidebar-ongoing-animations"false);
      tabbox.toggleAttribute("sidebar-ongoing-animations"false);
      this.sidebarMain.toggleAttribute("sidebar-ongoing-animations"false);
    };
    if (this._ongoingAnimations.length) {
      this._ongoingAnimations.forEach(a => a.cancel());
      this._ongoingAnimations = [];
      resetElements();
    }

    let fromRects = this._getRects(animatingElements);

    // We need to wait for lit to re-render, and us to get the final width.
    // This is a bit unfortunate but alas...
    await new Promise(resolve => {
      queueMicrotask(() => resolve(this.sidebarMain.updateComplete));
    });
    let toRects = this._getRects(animatingElements);

    const options = {
      duration: document.documentElement.hasAttribute("sidebar-expand-on-hover")
        ? this._animationExpandOnHoverDurationMs
        : this._animationDurationMs,
      easing: "ease-in-out",
    };
    let animations = [];
    let sidebarOnLeft = this._positionStart != RTL_UI;
    let sidebarShift = 0;
    let novaTranslate = 0;
    const novaMode = Services.prefs.getBoolPref("browser.nova.enabled"false);
    for (let i = 0; i < animatingElements.length; ++i) {
      const el = animatingElements[i];
      const [wasHidden, from] = fromRects[i];
      const [isHidden, to] = toRects[i];

      // For the sidebar, we need some special cases to make the animation
      // nicer (keeping the icon positions).
      const isSidebar = el === this.sidebarContainer;

      if (wasHidden != isHidden) {
        if (wasHidden) {
          from.left = from.right = sidebarOnLeft ? to.left : to.right;
        } else {
          to.left = to.right = sidebarOnLeft ? from.left : from.right;
        }
      }
      const widthGrowth = to.width - from.width;
      if (isSidebar) {
        sidebarShift = widthGrowth;
      }

      let fromTranslate = sidebarOnLeft
        ? from.left - to.left
        : from.right - to.right;
      let toTranslate = 0;

      // We fix the element to the larger width during the animation if needed,
      // but keeping the right flex width, and thus our original position, with
      // a negative margin.
      el.style.minWidth =
        el.style.maxWidth =
        el.style.marginLeft =
        el.style.marginRight =
        el.style.display =
          "";
      if (isHidden && !wasHidden) {
        el.style.display = "flex";
      }

      // Before nova, the sidebar would "shrink" by sliding partly out of view,
      // and only after this animation was done would the width actually
      // change. With nova's floating chrome, this trick is visually apparent.
      // In nova mode, we animate the sidebar's apparent width with clip-path
      // which is a performant alternative to actually animating the width.
      if (novaMode) {
        if (isSidebar) {
          novaTranslate = sidebarOnLeft
            ? -(to.width - from.width)
            : to.width - from.width;
          // For collapsing, hold the sidebar at from-width so clip-path has
          // content to clip. Negative margin keeps flex contribution at to-width.
          if (widthGrowth < 0) {
            el.style.minWidth = el.style.maxWidth = from.width + "px";
            el.style["margin-" + (sidebarOnLeft ? "right" : "left")] =
              widthGrowth + "px";
          }
          const clipAmount = Math.abs(widthGrowth);
          const fromClip = sidebarOnLeft
            ? `inset(0 ${widthGrowth > 0 ? clipAmount : 0}px 0 0)`
            : `inset(0 0 0 ${widthGrowth > 0 ? clipAmount : 0}px)`;
          const toClip = sidebarOnLeft
            ? `inset(0 ${widthGrowth < 0 ? clipAmount : 0}px 0 0)`
            : `inset(0 0 0 ${widthGrowth < 0 ? clipAmount : 0}px)`;
          animations.push(
            el.animate([{ clipPath: fromClip }, { clipPath: toClip }], options)
          );

          // When sidebar is on the right, content is left-aligned but the clip
          // moves from the left. Counter-translate the inner element rightward to
          // keep it in the visible area.
          if (!sidebarOnLeft && clipAmount > 0) {
            animations.push(
              this.sidebarMain.animate(
                [
                  { translate: `${widthGrowth > 0 ? clipAmount : 0}px 0 0` },
                  { translate: `${widthGrowth < 0 ? clipAmount : 0}px 0 0` },
                ],
                options
              )
            );
          }
        } else {
          animations.push(
            el.animate(
              [{ translate: `${novaTranslate}px 0 0` }, { translate: "0" }],
              options
            )
          );
        }
        continue;
      }

      if (widthGrowth < 0) {
        el.style.minWidth = el.style.maxWidth = from.width + "px";
        el.style["margin-" + (sidebarOnLeft ? "right" : "left")] =
          widthGrowth + "px";
        if (isSidebar) {
          toTranslate = sidebarOnLeft ? widthGrowth : -widthGrowth;
        } else if (el === this._box) {
          // This is very hacky, but this code doesn't deal well with
          // more than two elements moving, and this is the less invasive change.
          // It would be better to treat "sidebar + sidebar-box" as a unit.
          // We only hit this when completely hiding the box.
          fromTranslate = sidebarOnLeft ? -sidebarShift : sidebarShift;
          toTranslate = sidebarOnLeft
            ? fromTranslate + widthGrowth
            : fromTranslate - widthGrowth;
        }
      } else if (isSidebar) {
        fromTranslate += sidebarOnLeft ? -widthGrowth : widthGrowth;
      }

      animations.push(
        el.animate(
          [
            { translate: `${fromTranslate}px 0 0` },
            { translate: `${toTranslate}px 0 0` },
          ],
          options
        )
      );
      if (!isSidebar || !this._positionStart) {
        continue;
      }
      // We want to keep the buttons in place during the animation, for which
      // we might need to compensate.
      if (!this._state.launcherExpanded) {
        animations.push(
          this.sidebarMain.animate(
            [{ translate: "0" }, { translate: `${-toTranslate}px 0 0` }],
            options
          )
        );
      } else {
        animations.push(
          this.sidebarMain.animate(
            [{ translate: `${-fromTranslate}px 0 0` }, { translate: "0" }],
            options
          )
        );
      }
    }
    this._ongoingAnimations = animations;
    this.sidebarContainer.toggleAttribute("sidebar-ongoing-animations"true);
    this.sidebarMain.toggleAttribute("sidebar-ongoing-animations"true);
    this._box.toggleAttribute("sidebar-ongoing-animations"true);
    tabbox.toggleAttribute("sidebar-ongoing-animations"true);
    await Promise.allSettled(animations.map(a => a.finished));
    if (this._ongoingAnimations === animations) {
      this._ongoingAnimations = [];
      resetElements();
    }

    if (expandOnHoverEnabled) {
      await this._removeHoverStateBlocker();
    }
  },

  /**
   * For sidebar.revamp=true only, handle the keyboard or sidebar-button command to toggle the sidebar state
   */

  async handleToolbarButtonClick() {
    if (this.inSingleTabWindow || this.uninitializing) {
      return;
    }

    const initialExpandedValue = this._state.launcherExpanded;

    // What toggle means depends on the sidebar.visibility pref.
    const expandOnToggle = ["always-show""expand-on-hover"].includes(
      this.sidebarRevampVisibility
    );

    // when the launcher is toggled open by the user, we disable expand-on-hover interactions.
    if (this.sidebarRevampVisibility === "expand-on-hover") {
      await this.toggleExpandOnHover(initialExpandedValue);
    }

    if (this._animationEnabled && !window.gReduceMotion) {
      this._animateSidebarContainer();
    }

    if (expandOnToggle) {
      // just expand/collapse the launcher
      this._state.updateVisibility(true, !initialExpandedValue);
      this.updateToolbarButton();
      return;
    }

    const shouldShowLauncher = !this._state.launcherVisible;
    // show/hide the launcher
    this._state.updateVisibility(shouldShowLauncher);
    // if we're showing and there was panel open, open it again
    if (shouldShowLauncher && this._state.command) {
      await this.show(this._state.command);
    } else if (!shouldShowLauncher) {
      // hide the open panel. It will re-open next time as we don't change the command value
      this.hide({ dismissPanel: false });
    }
    this.updateToolbarButton();
  },

  /**
   * Update `checked` state and tooltip text of the toolbar button.
   */

  updateToolbarButton(toolbarButton = this.toolbarButton) {
    if (!toolbarButton || this.inSingleTabWindow) {
      return;
    }
    if (!this.sidebarRevampEnabled) {
      toolbarButton.dataset.l10nId = "show-sidebars";
      toolbarButton.checked = this.isOpen;
    } else {
      let sidebarToggleKey = document.getElementById("toggleSidebarKb");
      const shortcut = ShortcutUtils.prettifyShortcut(sidebarToggleKey);
      toolbarButton.dataset.l10nArgs = JSON.stringify({ shortcut });
      // we need to use the pref rather than SidebarController's getter here
      // as the getter might not have the new value yet
      const isVerticalTabs = Services.prefs.getBoolPref("sidebar.verticalTabs");
      if (isVerticalTabs) {
        toolbarButton.toggleAttribute("expanded"this.sidebarMain.expanded);
      } else {
        toolbarButton.toggleAttribute("expanded"false);
      }
      this.handleToolBadges();
      switch (this.sidebarRevampVisibility) {
        case "always-show":
        case "expand-on-hover": {
          // Toolbar button controls expanded state.
          const isExpanded = this.sidebarMain.expanded;
          toolbarButton.checked = isVerticalTabs && isExpanded;
          toolbarButton.dataset.l10nId = isExpanded
            ? "sidebar-widget-collapse-sidebar2"
            : "sidebar-widget-expand-sidebar2";
          break;
        }
        case "hide-sidebar": {
          // Toolbar button controls hidden state.
          const isVisible = !this.sidebarContainer.hidden;
          toolbarButton.checked = isVerticalTabs && isVisible;
          toolbarButton.dataset.l10nId = isVisible
            ? "sidebar-widget-hide-sidebar2"
            : "sidebar-widget-show-sidebar2";
          break;
        }
      }
    }
  },

  /**
   * Handles badges display for the toolbar and sidebar.
   * Check if a tool(toolID) has requested a badge from pref (i.e) sidebar.notification.badge.{toolID})
   * Ensure that badges are shown or cleared based on the sidebar visibility and user interaction.
   *
   * @param {string|null} toolID
   */

  handleToolBadges(toolID = null) {
    const toolPrefList = this.SidebarManager.getBadgeTools();

    for (const pref of toolPrefList) {
      if (toolID && toolID !== pref) {
        continue;
      }

      const badgePref = Services.prefs.getBoolPref(
        `sidebar.notification.badge.${pref}`,
        false
      );
      const commandID = [...this.toolsAndExtensions.keys()].find(
        id => toolsNameMap[id] === pref
      );

      if (!commandID) {
        continue;
      }

      const isSidebarClosed = !this._state?.launcherVisible;
      const isCurrentView = this._state?.command === commandID;

      // Don't show sidebar badge if sidebar is open and user is already viewing the tool panel
      if (badgePref && isCurrentView && this.isOpen) {
        this.dismissSidebarBadge(commandID);
      }

      // Show badge on toolbar if we would have shown it on a visible tool but sidebar is closed
      const tool = this.toolsAndExtensions.get(commandID);
      if (
        this.sidebarRevampEnabled &&
        badgePref &&
        !tool.disabled &&
        !tool.hidden &&
        isSidebarClosed
      ) {
        this._showToolbarButtonBadge();
      } else {
        this._clearToolbarButtonBadge();
      }

      window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
    }
  },

  _addHoverStateBlocker() {
    this._hoverBlockerCount++;
    MousePosTracker.removeListener(this);
  },

  async _removeHoverStateBlocker() {
    if (this._hoverBlockerCount == 1) {
      let isHovered = this._checkIsHoveredOverLauncher();

      // Collapse sidebar if needed
      if (this._state.launcherExpanded && !isHovered) {
        if (this._animationEnabled && !window.gReduceMotion) {
          this._animateSidebarContainer();
        }
        this._state.launcherExpanded = false;
        await this.waitUntilStable();
      }

      // Re-add MousePosTracker listener
      MousePosTracker.addListener(this);
    }
    if (this._hoverBlockerCount > 0) {
      this._hoverBlockerCount--;
    }
  },

  _showToolbarButtonBadge() {
    const badgeEl = this.toolbarButton?.querySelector(".toolbarbutton-badge");
    return badgeEl?.classList.add("feature-callout");
  },

  _clearToolbarButtonBadge() {
    const badgeEl = this.toolbarButton?.querySelector(".toolbarbutton-badge");
    return badgeEl?.classList.remove("feature-callout");
  },

  /**
   * Set badge toolID pref false on clicking the tool icon
   *
   * @param {string} view
   */

  dismissSidebarBadge(view) {
    const prefName = `sidebar.notification.badge.${toolsNameMap[view]}`;
    if (Services.prefs.getBoolPref(prefName, false)) {
      Services.prefs.setBoolPref(prefName, false);
    }
  },

  /**
   * Enable the splitter which can be used to resize the launcher.
   */

  _enableLauncherDragging() {
    if (this._launcherDropHandler) {
      // Already set up observers and handler.
      return;
    }
    if (!this._panelResizeObserver) {
      this._panelResizeObserver = new ResizeObserver(
        ([entry]) =>
          (this._state.panelWidth = entry.contentBoxSize[0].inlineSize)
      );
    }
    this._panelResizeObserver.observe(this._box);

    this._launcherDropHandler = () => {
      this._state.launcherDragActive = false;
      this.updatePinnedTabsHeightOnResize();
    };
    this._launcherSplitter.addEventListener(
      "command",
      this._launcherDropHandler
    );
  },

  /**
   * Enable the splitter which can be used to resize the pinned tabs container.
   */

  _enablePinnedTabsSplitterDragging() {
    if (!this._pinnedTabsSplitter.hidden) {
      // Already showing the launcher splitter with observers connected.
      // Nothing to do.
      return;
    }
    this._pinnedTabsResizeObserver = new ResizeObserver(() => {
      if (this.isPinnedTabsDragging) {
        this._state.pinnedTabsDragActive = true;
      }
    });

    this._itemsWrapperResizeObserver = new ResizeObserver(async ([entry]) => {
      await window.promiseDocumentFlushed(() => {
        requestAnimationFrame(() => {
          if (this._pinnedTabsContainer.hasAttribute("dragActive")) {
            return;
          }
          // Only respond to WIDTH changes (sidebar being resized wider/narrower).
          // Ignoring height changes prevents an infinite resize loop.
          const newWidth = entry.contentBoxSize[0].inlineSize;
          if (newWidth === this._pinnedTabsItemsWrapperWidth) {
            return;
          }
          this._pinnedTabsItemsWrapperWidth = newWidth;
          this.updatePinnedTabsHeightOnResize();
        });
      });
    });
    this._pinnedTabsResizeObserver.observe(this._pinnedTabsContainer);
    this._itemsWrapperResizeObserver.observe(this._pinnedTabsItemsWrapper);

    this._pinnedTabsDropHandler = () =>
      (this._state.pinnedTabsDragActive = false);
    this._pinnedTabsSplitter.addEventListener(
      "command",
      this._pinnedTabsDropHandler
    );

    this._pinnedTabsSplitter.hidden = false;
  },

  /**
   * Disable the launcher splitter and remove any active observers.
   */

  _disableLauncherDragging() {
    if (this._panelResizeObserver) {
      this._panelResizeObserver.disconnect();
    }
    this._launcherSplitter.removeEventListener(
      "command",
      this._launcherDropHandler
    );
    delete this._launcherDropHandler;
  },

  /**
   * Disable the pinned tabs splitter and remove any active observers.
   */

  _disablePinnedTabsDragging() {
    if (this._pinnedTabsResizeObserver) {
      this._pinnedTabsResizeObserver.disconnect();
    }
    if (this._itemsWrapperResizeObserver) {
      this._itemsWrapperResizeObserver.disconnect();
    }

    this._pinnedTabsSplitter.hidden = true;
  },

  _loadSidebarExtension(commandID) {
    let sidebar = this.sidebars.get(commandID);
    if (typeof sidebar?.onload === "function") {
      sidebar.onload();
    }
  },

  updatePinnedTabsHeightOnResize() {
    // Skip during any splitter drag to avoid flickering.
    if (this.isLauncherDragging || this.isPinnedTabsDragging) {
      return;
    }

    const preferredHeight = this._state.launcherExpanded
      ? this._state.expandedPinnedTabsHeight
      : this._state.collapsedPinnedTabsHeight;

    if (!preferredHeight || !this._pinnedTabsContainer.childElementCount) {
      return;
    }

    let itemsWrapperHeight = window.windowUtils.getBoundsWithoutFlushing(
      this._pinnedTabsItemsWrapper
    ).height;

    // Clamp for display only — never overwrite the user's saved preference
    const clampedHeight = Math.min(preferredHeight, itemsWrapperHeight);
    this._pinnedTabsContainer.style.height = `${clampedHeight}px`;
  },

  /**
   * Ensure tools reflect the current pref state
   */

  refreshTools() {
    let changed = false;
    const tools = new Set(this.sidebarRevampTools.split(","));
    this.toolsAndExtensions.forEach(tool => {
      const expected = !tools.has(tool.name);
      if (tool.disabled != expected) {
        tool.disabled = expected;
        changed = true;
      }
    });
    if (changed) {
      window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
    }
  },

  /**
   * Sets the disabled property for a tool when customizing sidebar options
   *
   * @param {string} commandID
   */

  toggleTool(commandID) {
    const toggledTool = this.toolsAndExtensions.get(commandID);
    const toolName = toggledTool.name;
    toggledTool.disabled = !toggledTool.disabled;

    if (!toggledTool.disabled) {
      // If re-enabling tool, remove from the map and add it to the end
      this.toolsAndExtensions.delete(commandID);
      this.toolsAndExtensions.set(commandID, toggledTool);
    }

    this.SidebarManager.updateToolsPref(toolName, toggledTool.disabled);

    if (toggledTool.disabled) {
      this.dismissSidebarBadge(commandID);
    }
    window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
  },

  addOrUpdateExtension(commandID, extension) {
    if (this.inSingleTabWindow) {
      return;
    }
    if (this.toolsAndExtensions.has(commandID)) {
      // Update existing extension
      let extensionToUpdate = this.toolsAndExtensions.get(commandID);
      extensionToUpdate.iconUrl = extension.iconUrl;
      extensionToUpdate.tooltiptext = extension.label;
      window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
    } else {
      // Add new extension
      const name = extension.extensionId;
      this.toolsAndExtensions.set(commandID, {
        view: commandID,
        extensionId: extension.extensionId,
        iconUrl: extension.iconUrl,
        tooltiptext: extension.label,
        disabled: !this.sidebarTools.includes(name), // name is the extensionID
        name,
      });
      window.dispatchEvent(new CustomEvent("SidebarItemAdded"));
    }
  },

  /**
   * Add menu items for a browser extension. Add the extension to the
   * `sidebars` map.
   *
   * @param {string} commandID
   * @param {object} props
   */

  registerExtension(commandID, props) {
    const sidebarTools = this.sidebarTools;
    const installedExtensions = this.sidebarExtensions;
    const name = props.extensionId;

    // An extension that is newly installed will be added to the sidebar.main.tools
    // pref by default until a user deselects it; separately we update our list of
    // sidebar extensions to ensure it keeps track of what's been installed.
    if (!installedExtensions.includes(name) && !sidebarTools.includes(name)) {
      sidebarTools.push(name);
      installedExtensions.push(name);
      Services.prefs.setStringPref(this.TOOLS_PREF, sidebarTools.join());
      Services.prefs.setStringPref(
        this.INSTALLED_EXTENSIONS,
        installedExtensions.join()
      );
    }

    const sidebar = {
      title: props.title,
      url: "chrome://browser/content/webext-panels.xhtml",
      menuId: props.menuId,
      switcherMenuId: `sidebarswitcher_menu_${commandID}`,
      keyId: `ext-key-id-${commandID}`,
      label: props.title,
      iconUrl: props.iconUrl,
      classAttribute: "menuitem-iconic webextension-menuitem",
      // The following properties are specific to extensions
      extensionId: props.extensionId,
      onload: props.onload,
      name,
    };
    this.sidebars.set(commandID, sidebar);

    // Insert a menuitem for View->Show Sidebars.
    const menuitem = this.createMenuItem(commandID, sidebar);
    document.getElementById("viewSidebarMenu").appendChild(menuitem);
    this.addOrUpdateExtension(commandID, sidebar);

    if (!this.sidebarRevampEnabled) {
      // Insert a toolbarbutton for the sidebar dropdown selector.
      let switcherMenuitem = this.createMenuItem(commandID, sidebar);
      switcherMenuitem.setAttribute("id", sidebar.switcherMenuId);
      switcherMenuitem.removeAttribute("type");

      let separator = document.getElementById("sidebar-extensions-separator");
      separator.parentNode.insertBefore(switcherMenuitem, separator);
    }
    this._setExtensionAttributes(
      commandID,
      { iconUrl: props.iconUrl, label: props.title },
      sidebar
    );
  },

  /**
   * Create a menu item for the View>Sidebars submenu in the menubar.
   *
   * @param {string} commandID
   * @param {object} sidebar
   * @returns {Element}
   */

  createMenuItem(commandID, sidebar) {
    const menuitem = document.createXULElement("menuitem");
    menuitem.setAttribute("id", sidebar.menuId);
    menuitem.setAttribute("type""checkbox");
    // Some menu items get checkbox type removed, so should show the sidebar
    menuitem.addEventListener("command", () =>
      this[menuitem.hasAttribute("type") ? "toggle" : "show"](commandID)
    );
    if (sidebar.classAttribute) {
      menuitem.setAttribute("class", sidebar.classAttribute);
    }
    if (sidebar.keyId) {
      menuitem.setAttribute("key", sidebar.keyId);
    }
    if (sidebar.menuL10nId) {
      menuitem.dataset.l10nId = sidebar.menuL10nId;
    }
    if (this.inSingleTabWindow) {
      menuitem.setAttribute("disabled""true");
    }
    return menuitem;
  },

  /**
   * Update attributes on all existing menu items for a browser extension.
   *
   * @param {string} commandID
   * @param {object} attributes
   * @param {string} attributes.iconUrl
   * @param {string} attributes.label
   * @param {boolean} needsRefresh
   */

  setExtensionAttributes(commandID, attributes, needsRefresh) {
    const sidebar = this.sidebars.get(commandID);
    this._setExtensionAttributes(commandID, attributes, sidebar, needsRefresh);
    this.addOrUpdateExtension(commandID, sidebar);
  },

  _setExtensionAttributes(
    commandID,
    { iconUrl, label },
    sidebar,
    needsRefresh = false
  ) {
    sidebar.iconUrl = iconUrl;
    sidebar.label = label;

    const updateAttributes = el => {
      // TODO Bug 1996762 - Add support for dark-theme sidebar icons
      // --webextension-menuitem-image-dark is used in dark themes
      el.style.setProperty(
        "--webextension-menuitem-image",
        `url("${sidebar.iconUrl}")`
      );
      el.setAttribute("label", sidebar.label);
    };

    updateAttributes(document.getElementById(sidebar.menuId), sidebar);
    const switcherMenu = document.getElementById(sidebar.switcherMenuId);
    if (switcherMenu) {
      updateAttributes(switcherMenu, sidebar);
    }
    if (this.initialized && this.currentID === commandID) {
      // Update the sidebar title if this extension is the current sidebar.
      this.title = label;
      if (this.isOpen && needsRefresh) {
        this.show(commandID);
      }
    }
  },

  /**
   * Retrieve the list of registered browser extensions.
   *
   * @returns {Array}
   */

  getExtensions() {
    const extensions = [];
    for (const [commandID, sidebar] of this.sidebars.entries()) {
      if (Object.hasOwn(sidebar, "extensionId")) {
        const disabled = !this.sidebarTools.includes(sidebar.name);

        extensions.push({
          commandID,
          view: commandID,
          extensionId: sidebar.extensionId,
          iconUrl: sidebar.iconUrl,
          tooltiptext: sidebar.label,
          disabled,
          name: sidebar.name,
        });
      }
    }

    return extensions;
  },

  /**
   * Retrieve the list of tools in the sidebar
   *
   * @returns {Array}
   */

  getTools() {
    return Object.keys(toolsNameMap)
      .filter(commandID => this.sidebars.get(commandID))
      .map(commandID => {
        const sidebar = this.sidebars.get(commandID);
        const disabled = !this.sidebarTools.includes(toolsNameMap[commandID]);
        return {
          commandID,
          view: commandID,
          name: sidebar.name,
          iconUrl: sidebar.iconUrl,
          l10nId: sidebar.revampL10nId,
          disabled,
          // Reflect the current tool state defaulting to visible
          get hidden() {
            return !(sidebar.visible ?? true);
          },
          get attention() {
            return sidebar.attention ?? false;
          },
          contextMenu: sidebar.toolContextMenuId,
        };
      });
  },

  /**
   * Remove a browser extension.
   *
   * @param {string} commandID
   */

  removeExtension(commandID) {
    if (this.inSingleTabWindow) {
      return;
    }
    const sidebar = this.sidebars.get(commandID);
    if (!sidebar) {
      return;
    }
    if (this.currentID === commandID) {
      // If the extension removal is a update, we don't want to forget this panel.
      // So, let the sidebarAction extension API code remove the lastOpenedId as needed
      this.hide({ dismissPanel: false });
    }
    document.getElementById(sidebar.menuId)?.remove();
    document.getElementById(sidebar.switcherMenuId)?.remove();

    this.sidebars.delete(commandID);
    this.toolsAndExtensions.delete(commandID);
    window.dispatchEvent(new CustomEvent("SidebarItemRemoved"));
  },

  /**
   * Check whether a sidebar panel can be shown in the current window.
   *
   * @param {string} commandID
   * @returns {boolean}
   */

  _canShow(commandID) {
    // Extensions without private window access wont be in the sidebars map.
    // Some sidebars can be conditionally visible depending on the window.
    const sidebar = this.sidebars.get(commandID);
    return !!sidebar && (sidebar.visible ?? true);
  },

  /**
   * Show the sidebar.
   *
   * This wraps the internal method, including a ping to telemetry.
   *
   * @param {string}  commandID     ID of the sidebar to use.
   * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
   *                                showing of the sidebar.
   * @returns {Promise<boolean>}
   */

  async show(commandID, triggerNode) {
    if (this.inSingleTabWindow) {
      return false;
    }
    if (this.currentID && commandID !== this.currentID) {
      // If there is currently a panel open, we are about to hide it in order
      // to show another one, so record a "hide" event on the current panel.
      this._recordPanelToggle(this.currentID, false);
    }
    this._recordPanelToggle(commandID, true);

    if (!this._canShow(commandID)) {
      return false;
    }
    // capture the launcher state so we can revert to it when the panel closes
    if (this._launcherStateAtOpen === undefined) {
      this._launcherStateAtOpen = this._state.launcherVisible;
    }

    return this._show(commandID).then(() => {
      this._loadSidebarExtension(commandID);

      if (triggerNode) {
        updateToggleControlLabel(triggerNode);
      }
      this.updateToolbarButton();
      this.dismissSidebarBadge(commandID);

      this._fireFocusedEvent();
      return true;
    });
  },

  /**
   * Show the sidebar, without firing the focused event or logging telemetry.
   * This is intended to be used when the sidebar is opened automatically
   * when a window opens (not triggered by user interaction).
   *
   * @param {string} commandID ID of the sidebar.
   * @returns {Promise<boolean>}
   */

  async showInitially(commandID) {
    if (this.inSingleTabWindow) {
      return false;
    }
    this._recordPanelToggle(commandID, true);

    if (!this._canShow(commandID)) {
      return false;
    }
    return this._show(commandID).then(() => {
      this._loadSidebarExtension(commandID);
      return true;
    });
  },

  /**
   * Implementation for show. Also used internally for sidebars that are shown
   * when a window is opened and we don't want to ping telemetry.
   *
   * @param {string} commandID ID of the sidebar.
   * @returns {Promise<void>}
   */

  _show(commandID) {
    return new Promise(resolve => {
      const willShowEvent = new CustomEvent("SidebarWillShow");
      this.browser.contentWindow?.dispatchEvent(willShowEvent);

      this._state.panelOpen = true;
      if (this.sidebarRevampEnabled) {
        this._box.dispatchEvent(
          new CustomEvent("sidebar-show", { detail: { viewId: commandID } })
        );
      } else {
        this.hideSwitcherPanel();
      }

      this.selectMenuItem(commandID);
      this._box.hidden = this._splitter.hidden = false;

      this._box.setAttribute("checked""true");
      this._state.command = commandID;

      let { icon, url, title, sourceL10nEl, contextMenuId } =
        this.sidebars.get(commandID);
      if (icon) {
        this._switcherTarget.style.setProperty(
          "--webextension-menuitem-image",
          icon
        );
      } else {
        this._switcherTarget.style.removeProperty(
          "--webextension-menuitem-image"
        );
      }

      if (contextMenuId) {
        this._box.setAttribute("context", contextMenuId);
      } else {
        this._box.removeAttribute("context");
      }

      // use to live update <tree> elements if the locale changes
      this.lastOpenedId = commandID;
      // These title changes only apply to the old sidebar menu
      if (!this.sidebarRevampEnabled) {
        this.title = title;
        // Keep the title element in the switcher in sync with any l10n changes.
        this.observeTitleChanges(sourceL10nEl);
      }

      this.browser.setAttribute("src", url); // kick off async load

      if (this.browser.contentDocument.location.href != url) {
        // make sure to clear the timeout if the load is aborted
        this.browser.addEventListener("unload", () => {
          if (this.browser.loadingTimerID) {
            clearTimeout(this.browser.loadingTimerID);
            delete this.browser.loadingTimerID;
            resolve();
          }
        });
        this.browser.addEventListener(
          "load",
          () => {
            // We're handling the 'load' event before it bubbles up to the usual
            // (non-capturing) event handlers. Let it bubble up before resolving.
            this.browser.loadingTimerID = setTimeout(() => {
              delete this.browser.loadingTimerID;
              resolve();

              // Now that the currentId is updated, fire a show event.
              this._fireShowEvent();
              this._recordBrowserSize();

              const sidebar = this.sidebars.get(commandID);
              // Initialize sidebar permissions UI
              if (sidebar?.permissions) {
                if (!this._permissions) {
                  this._permissions = new this.SidebarPermissions(window);
                }
                this._permissions.init(this.browser);
              }
            }, 0);
          },
          { capture: true, once: true }
        );
      } else {
        resolve();

        // Now that the currentId is updated, fire a show event.
        this._fireShowEvent();
        this._recordBrowserSize();
      }
    });
  },

  /**
   * Hide the sidebar.
   *
   * @param {object} options - Parameter object.
   * @param {DOMNode} options.triggerNode - Node, usually a button, that triggered the
   *                                        hiding of the sidebar.
   * @param {boolean} options.dismissPanel -Only close the panel or close the whole sidebar (the default.)
   */

  hide({ triggerNode, dismissPanel = this.sidebarRevampEnabled } = {}) {
    if (!this.isOpen) {
      return;
    }

    const willHideEvent = new CustomEvent("SidebarWillHide", {
      cancelable: true,
    });
    this.browser.contentWindow?.dispatchEvent(willHideEvent);
    if (willHideEvent.defaultPrevented) {
      return;
    }

    this.hideSwitcherPanel();
    this._recordPanelToggle(this.currentID, false);
    this._state.panelOpen = false;
    if (dismissPanel) {
      // The user is explicitly closing this panel so we don't want it to
      // automatically re-open next time the sidebar is shown
      this._state.command = "";
      this.lastOpenedId = null;
      if (this._launcherStateAtOpen !== undefined) {
        if (this.sidebarRevampVisibility === "hide-sidebar") {
          this._state.launcherVisible = this._launcherStateAtOpen;
        }
        delete this._launcherStateAtOpen;
      }
    }

    if (this.sidebarRevampEnabled) {
      this._box.dispatchEvent(new CustomEvent("sidebar-hide"));
    }
    this.selectMenuItem("");

    // Replace the document currently displayed in the sidebar with about:blank
    // so that we can free memory by unloading the page. We need to explicitly
    // create a new content viewer because the old one doesn't get destroyed
    // until about:blank has loaded (which does not happen as long as the
    // element is hidden).
    this.browser.setAttribute("src""about:blank");
    this.browser.docShell?.createAboutBlankDocumentViewer(nullnull);

    this._box.removeAttribute("checked");
    this._box.removeAttribute("context");
    this._box.hidden = this._splitter.hidden = true;

    let selBrowser = gBrowser.selectedBrowser;
    selBrowser.focus();
    if (triggerNode) {
      updateToggleControlLabel(triggerNode);
    }
    this.updateToolbarButton();
  },

  /**
   * Record to Glean when any of the sidebar panels is loaded or unloaded.
   *
   * @param {string} commandID
   * @param {boolean} opened
   */

  _recordPanelToggle(commandID, opened) {
    const sidebar = this.sidebars.get(commandID);
    if (!sidebar) {
      return;
    }
    const isExtension = sidebar && Object.hasOwn(sidebar, "extensionId");
    const version = this.sidebarRevampEnabled ? "new" : "old";
    if (isExtension) {
      const addonId = sidebar.extensionId;
      const addonName = WebExtensionPolicy.getByID(addonId)?.name;
      Glean.extension.sidebarToggle.record({
        opened,
        version,
        addon_id: AMTelemetry.getTrimmedString(addonId),
        addon_name: addonName && AMTelemetry.getTrimmedString(addonName),
      });
    } else if (sidebar.gleanEvent && sidebar.recordSidebarVersion) {
      sidebar.gleanEvent.record({ opened, version });
    } else if (sidebar.gleanEvent) {
      sidebar.gleanEvent.record({ opened });
    }
  },

  /**
   * Use MousePosTracker to manually check for hover state over launcher
   */

  _checkIsHoveredOverLauncher() {
    // Manually check mouse position
    let isHovered;
    MousePosTracker._callListener({
      onMouseEnter: () => (isHovered = true),
      onMouseLeave: () => (isHovered = false),
      getMouseTargetRect: () => this.getMouseTargetRect(),
    });
    return isHovered;
  },

  /**
   * Record to Glean when any of the sidebar icons are clicked.
   *
   * @param {string} commandID - Command ID of the icon.
   * @param {boolean} expanded - Whether the sidebar was expanded when clicked.
   */

  recordIconClick(commandID, expanded) {
    const sidebar = this.sidebars.get(commandID);
    const isExtension = sidebar && Object.hasOwn(sidebar, "extensionId");
    if (isExtension) {
      const addonId = sidebar.extensionId;
      Glean.sidebar.addonIconClick.record({
        sidebar_open: expanded,
        addon_id: AMTelemetry.getTrimmedString(addonId),
      });
    } else if (sidebar.gleanClickEvent) {
      sidebar.gleanClickEvent.record({
        sidebar_open: expanded,
      });
    }
  },

  /**
   * Sets the checked state only on the menu items of the specified sidebar, or
   * none if the argument is an empty string.
   */

  selectMenuItem(commandID) {
    for (let [id, { menuId, triggerButtonId }] of this.sidebars) {
      let menu = document.getElementById(menuId);
      if (!menu) {
        continue;
      }
      let triggerbutton =
        triggerButtonId && document.getElementById(triggerButtonId);
      if (id == commandID) {
        menu.setAttribute("checked""true");
        if (triggerbutton) {
          triggerbutton.setAttribute("checked""true");
          updateToggleControlLabel(triggerbutton);
        }
      } else {
        menu.removeAttribute("checked");
        if (triggerbutton) {
          triggerbutton.removeAttribute("checked");
          updateToggleControlLabel(triggerbutton);
        }
      }
    }
  },

  toggleTabstrip() {
    let toVerticalTabs = CustomizableUI.verticalTabsEnabled;
    let tabStrip = gBrowser.tabContainer;
    let arrowScrollbox = tabStrip.arrowScrollbox;
    let currentScrollOrientation = arrowScrollbox.getAttribute("orient");

    if (
      (!toVerticalTabs && currentScrollOrientation !== "vertical") ||
      (toVerticalTabs && currentScrollOrientation === "vertical")
    ) {
      // Nothing to update
      return;
    }

    if (toVerticalTabs) {
      arrowScrollbox.setAttribute("orient""vertical");
      tabStrip.setAttribute("orient""vertical");
      this._clearToolbarButtonBadge();
    } else {
      arrowScrollbox.setAttribute("orient""horizontal");
      tabStrip.removeAttribute("expanded");
      tabStrip.setAttribute("orient""horizontal");
    }

    let verticalToolbar = document.getElementById(
      CustomizableUI.AREA_VERTICAL_TABSTRIP
    );
    verticalToolbar.toggleAttribute("visible", toVerticalTabs);
    // Re-render sidebar-main so that templating is updated
    // for proper keyboard navigation for Tools
    this.sidebarMain.requestUpdate();
    if (
      !this.verticalTabsEnabled &&
      this.sidebarRevampVisibility == "hide-sidebar"
    ) {
      // the sidebar.visibility pref didn't change so launcherExpanded hasn't
      // been updated; we need to set it here to un-expand the launcher
      this._state.launcherExpanded = false;
    }
  },

  debouncedMouseEnter() {
    const contentArea = document.getElementById("tabbrowser-tabbox");
    this._box.toggleAttribute("sidebar-launcher-hovered"true);
    contentArea.toggleAttribute("sidebar-launcher-hovered"true);
    this._state.launcherHoverActive = true;
    if (this._animationEnabled && !window.gReduceMotion) {
      this._animateSidebarContainer();
    }
    this._state.launcherExpanded = true;
    this._mouseEnterDeferred.resolve();
  },

  _collapseLauncher() {
    this.mouseEnterTask?.disarm();
    this._mouseEnterDeferred?.resolve();
    const contentArea = document.getElementById("tabbrowser-tabbox");
    this._box.toggleAttribute("sidebar-launcher-hovered"false);
    contentArea.toggleAttribute("sidebar-launcher-hovered"false);
    this._state.launcherHoverActive = false;
    if (this._animationEnabled && !window.gReduceMotion) {
      this._animateSidebarContainer();
    }
    this._state.launcherExpanded = false;
  },

  collapseOnEscape() {
    if (
      this.sidebarRevampVisibility !== "expand-on-hover" ||
      !this._state.launcherExpanded
    ) {
      return;
    }
    this._escapedWhileHovered = true;
    this._mouseLeftSinceEscape = false;
    this._collapseLauncher();
  },

  onMouseLeave() {
    if (this._escapedWhileHovered) {
      this._mouseLeftSinceEscape = true;
      return;
    }
    if (!this._state.launcherExpanded) {
      return;
    }
    this._collapseLauncher();
  },

  onMouseEnter() {
    if (this._state.launcherExpanded) {
      return;
    }
    if (this._escapedWhileHovered) {
      if (this._mouseLeftSinceEscape) {
        this._escapedWhileHovered = false;
        this._mouseLeftSinceEscape = false;
      } else {
        return;
      }
    }
    this._mouseEnterDeferred = Promise.withResolvers();
    this.mouseEnterTask = new DeferredTask(
      () => {
        let isHovered = this._checkIsHoveredOverLauncher();
        // Only expand sidebar if mouse is still hovering over sidebar launcher
        if (isHovered) {
          this.debouncedMouseEnter();
        }
      },
      this._animationExpandOnHoverDelayDurationMs,
      EXPAND_ON_HOVER_DEBOUNCE_TIMEOUT_MS
    );
    this.mouseEnterTask?.arm();
  },

  get expandOnHoverComplete() {
    return this._mouseEnterDeferred?.promise || Promise.resolve();
  },

  async setLauncherCollapsedWidth() {
    let browserEl = document.getElementById("browser");
    if (this.getUIState().launcherExpanded) {
      this._state.launcherExpanded = false;
    }
    await this.waitUntilStable();
    let collapsedWidth = await new Promise(resolve => {
      requestAnimationFrame(() => {
        resolve(this._getRects([this.sidebarContainer])[0][1].width);
      });
    });

    browserEl.style.setProperty(
      "--sidebar-launcher-collapsed-width",
      `${collapsedWidth}px`
    );
  },

  getMouseTargetRect() {
    let launcherRect = window.windowUtils.getBoundsWithoutFlushing(
      SidebarController.sidebarMain
    );
    return {
      top: launcherRect.top,
      bottom: launcherRect.bottom,
      left: this._positionStart
        ? launcherRect.left
        : launcherRect.left + LAUNCHER_SPLITTER_WIDTH,
      right: this._positionStart
        ? launcherRect.right - LAUNCHER_SPLITTER_WIDTH
        : launcherRect.right,
    };
  },

  async handleEvent(e) {
    switch (e.type) {
      case "popupshown":
        /* Temporarily remove MousePosTracker listener when a context menu is open */
        if (e.composedTarget.tagName !== "tooltip") {
          this._addHoverStateBlocker();
        }
        break;
      case "popuphidden":
        if (e.composedTarget.tagName !== "tooltip") {
          await this._removeHoverStateBlocker();
        }
        break;
      default:
        break;
    }
  },

  async toggleExpandOnHover(isEnabled, isDragEnded) {
    document.documentElement.toggleAttribute(
      "sidebar-expand-on-hover",
      isEnabled
    );
    if (isEnabled) {
      if (!this._state) {
        this._state = new this.SidebarState(this);
      }
      await this.waitUntilStable();
      MousePosTracker.addListener(this);
      if (!isDragEnded) {
        await this.setLauncherCollapsedWidth();
      }
      document.addEventListener("popupshown"this);
      document.addEventListener("popuphidden"this);
      // Reset user-preferred height
      this.sidebarMain.buttonsWrapper.style.height = this._state
        .launcherExpanded
        ? ""
        : "0";
    } else {
      this._removeHoverStateBlocker();
      MousePosTracker.removeListener(this);
      if (!this.mouseOverTask?.isFinalized) {
        this.mouseOverTask?.finalize();
      }
      document.removeEventListener("popupshown"this);
      document.removeEventListener("popuphidden"this);
      // Add back user-preferred height if defined
      if (
        this._state.launcherExpanded &&
        this._state.expandedToolsHeight !== undefined &&
        this.sidebarMain.buttonGroup
      ) {
        this.sidebarMain.buttonGroup.style.height =
          this._state.expandedToolsHeight;
      } else if (
        !this._state.launcherExpanded &&
        this._state.collapsedToolsHeight !== undefined &&
        this.sidebarMain.buttonGroup
      ) {
        this.sidebarMain.buttonGroup.style.height =
          this._state.collapsedToolsHeight;
      }
    }

    document.documentElement.toggleAttribute(
      "sidebar-expand-on-hover",
      isEnabled
    );
  },

  /**
   * Report visibility preference to Glean.
   *
   * @param {string} [value] - The preference value.
   */

  recordVisibilitySetting(value = this.sidebarRevampVisibility) {
    let visibilitySetting = "hide";
    if (value === "always-show") {
      visibilitySetting = "always";
    } else if (value === "expand-on-hover") {
      visibilitySetting = "expand-on-hover";
    }
    Glean.sidebar.displaySettings.set(visibilitySetting);
  },

  /**
   * Report position preference to Glean.
   *
   * @param {boolean} [value] - The preference value.
   */

  recordPositionSetting(value = this._positionStart) {
    Glean.sidebar.positionSettings.set(value !== RTL_UI ? "left" : "right");
  },

  /**
   * Report tabs layout preference to Glean.
   *
   * @param {boolean} [value] - The preference value.
   */

  recordTabsLayoutSetting(value = this.sidebarVerticalTabsEnabled) {
    Glean.sidebar.tabsLayout.set(value ? "vertical" : "horizontal");
  },
};

ChromeUtils.defineESModuleGetters(SidebarController, {
  AIWindow:
    "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs",
  SidebarManager:
    "moz-src:///browser/components/sidebar/SidebarManager.sys.mjs",
  SidebarState: "moz-src:///browser/components/sidebar/SidebarState.sys.mjs",
  SidebarPermissions:
    "chrome://browser/content/sidebar/sidebar-permissions.mjs",
});

// Add getters related to the position here, since we will want them
// available for both startDelayedLoad and init.
XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "_positionStart",
  SidebarController.POSITION_START_PREF,
  true,
  (_aPreference, _previousValue, newValue) => {
    if (
      !SidebarController.uninitializing &&
      !SidebarController.inSingleTabWindow
    ) {
      SidebarController.setPosition();
      SidebarController.recordPositionSetting(newValue);
    }
  }
);
XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "_animationEnabled",
  "sidebar.animation.enabled",
  true
);
XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "_animationDurationMs",
  "sidebar.animation.duration-ms",
  200
);
XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "_animationExpandOnHoverDurationMs",
  "sidebar.animation.expand-on-hover.duration-ms",
  400
);
XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "_animationExpandOnHoverDelayDurationMs",
  "sidebar.animation.expand-on-hover.delay-duration-ms",
  200
);
XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "sidebarRevampEnabled",
  "sidebar.revamp",
  false,
  (_aPreference, _previousValue, newValue) => {
    if (!SidebarController.uninitializing) {
      SidebarController.toggleRevampSidebar();
      SidebarController._state.revampEnabled = newValue;
    }
  }
);
XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "updatedBookmarksPanelEnabled",
  "sidebar.updatedBookmarks.enabled",
  false,
  (_aPreference, _previousValue, newValue) => {
    if (!SidebarController.uninitializing) {
      const bookmarksSidebar = SidebarController.sidebars.get(
        "viewBookmarksSidebar"
      );
      if (bookmarksSidebar) {
        bookmarksSidebar.url =
          SidebarController.sidebarRevampEnabled && newValue
            ? "chrome://browser/content/sidebar/sidebar-bookmarks.html"
            : "chrome://browser/content/places/bookmarksSidebar.xhtml";
        if (SidebarController.currentID === "viewBookmarksSidebar") {
          SidebarController.show("viewBookmarksSidebar");
        }
      }
    }
  }
);
XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "sidebarRevampTools",
  "sidebar.main.tools",
  "",
  () => {
    if (
      !SidebarController.inSingleTabWindow &&
      !SidebarController.uninitializing
    ) {
      SidebarController.refreshTools();
    }
  }
);
XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "installedExtensions",
  "sidebar.installed.extensions",
  ""
);

XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "sidebarRevampVisibility",
  "sidebar.visibility",
  "always-show",
  (_aPreference, _previousValue, newValue) => {
    if (
      !SidebarController.inSingleTabWindow &&
      !SidebarController.uninitializing
    ) {
      SidebarController.toggleExpandOnHover(newValue === "expand-on-hover");
      SidebarController.recordVisibilitySetting(newValue);
      if (SidebarController._state) {
        // we need to use the pref rather than SidebarController's getter here
        // as the getter might not have the new value yet
        const isVerticalTabs = Services.prefs.getBoolPref(
          "sidebar.verticalTabs"
        );
        SidebarController._state.revampVisibility = newValue;
        if (
          SidebarController._animationEnabled &&
          !window.gReduceMotion &&
          newValue !== "expand-on-hover"
        ) {
          SidebarController._animateSidebarContainer();
        }

        // launcher is always initially expanded with vertical tabs unless we're doing expand-on-hover
        let forceExpand = false;
        if (
          isVerticalTabs &&
          ["always-show""hide-sidebar"].includes(newValue)
        ) {
          forceExpand = true;
        }

        // horizontal tabs and hide-sidebar = visible initially.
        // vertical tab and hide-sidebar = not visible initially
        let showLauncher = true;
        if (newValue == "hide-sidebar" && isVerticalTabs) {
          showLauncher = false;
        }
        SidebarController._state.updateVisibility(showLauncher, forceExpand);
      }
      SidebarController.updateToolbarButton();
    }
  }
);

XPCOMUtils.defineLazyPreferenceGetter(
  SidebarController,
  "sidebarVerticalTabsEnabled",
  "sidebar.verticalTabs",
  false,
  (_aPreference, _previousValue, newValue) => {
    if (
      !SidebarController.uninitializing &&
      !SidebarController.inSingleTabWindow
    ) {
      SidebarController.recordTabsLayoutSetting(newValue);
      if (newValue) {
        SidebarController._enablePinnedTabsSplitterDragging();
      } else {
        SidebarController._disablePinnedTabsDragging();
      }
      SidebarController._state.updatePinnedTabsHeight();
      SidebarController._state.updateToolsHeight();
    }
  }
);

Messung V0.5 in Prozent
C=92 H=93 G=92

¤ Dauer der Verarbeitung: 0.46 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.