tor-browser

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

panelUI.js (38438B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 ChromeUtils.defineESModuleGetters(this, {
      6  AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
      7  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
      8  MenuMessage: "resource:///modules/asrouter/MenuMessage.sys.mjs",
      9  NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
     10  PanelMultiView:
     11    "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs",
     12  updateZoomUI: "resource:///modules/ZoomUI.sys.mjs",
     13 });
     14 
     15 /**
     16 * Maintains the state and dispatches events for the main menu panel.
     17 */
     18 
     19 const PanelUI = {
     20  /** Panel events that we listen for. **/
     21  get kEvents() {
     22    return ["popupshowing", "popupshown", "popuphiding", "popuphidden"];
     23  },
     24 
     25  /// Notification events used for overwriting notification actions
     26  get kNotificationEvents() {
     27    return ["buttoncommand", "secondarybuttoncommand", "learnmoreclick"];
     28  },
     29 
     30  /**
     31   * Used for lazily getting and memoizing elements from the document. Lazy
     32   * getters are set in init, and memoizing happens after the first retrieval.
     33   */
     34  get kElements() {
     35    return {
     36      multiView: "appMenu-multiView",
     37      menuButton: "PanelUI-menu-button",
     38      panel: "appMenu-popup",
     39      overflowFixedList: "widget-overflow-fixed-list",
     40      overflowPanel: "widget-overflow",
     41      navbar: "nav-bar",
     42    };
     43  },
     44 
     45  _initialized: false,
     46  _notifications: null,
     47  _notificationPanel: null,
     48 
     49  init(shouldSuppress) {
     50    this._shouldSuppress = shouldSuppress;
     51    this._initElements();
     52 
     53    this.menuButton.addEventListener("mousedown", this);
     54    this.menuButton.addEventListener("keypress", this);
     55 
     56    Services.obs.addObserver(this, "ai-window-state-changed");
     57    Services.obs.addObserver(this, "fullscreen-nav-toolbox");
     58    Services.obs.addObserver(this, "appMenu-notifications");
     59    Services.obs.addObserver(this, "show-update-progress");
     60 
     61    XPCOMUtils.defineLazyPreferenceGetter(
     62      this,
     63      "autoHideToolbarInFullScreen",
     64      "browser.fullscreen.autohide",
     65      false,
     66      (pref, previousValue, newValue) => {
     67        // On OSX, or with autohide preffed off, MozDOMFullscreen is the only
     68        // event we care about, since fullscreen should behave just like non
     69        // fullscreen. Otherwise, we don't want to listen to these because
     70        // we'd just be spamming ourselves with both of them whenever a user
     71        // opened a video.
     72        if (newValue) {
     73          window.removeEventListener("MozDOMFullscreen:Entered", this);
     74          window.removeEventListener("MozDOMFullscreen:Exited", this);
     75          window.addEventListener("fullscreen", this);
     76        } else {
     77          window.addEventListener("MozDOMFullscreen:Entered", this);
     78          window.addEventListener("MozDOMFullscreen:Exited", this);
     79          window.removeEventListener("fullscreen", this);
     80        }
     81 
     82        this.updateNotifications(false);
     83      },
     84      autoHidePref => autoHidePref && Services.appinfo.OS !== "Darwin"
     85    );
     86 
     87    XPCOMUtils.defineLazyPreferenceGetter(
     88      this,
     89      "isAIWindowEnabled",
     90      "browser.aiwindow.enabled",
     91      false,
     92      (_pref, _previousValue, _newValue) => {
     93        this._showAIMenuItem();
     94      }
     95    );
     96 
     97    if (this.autoHideToolbarInFullScreen) {
     98      window.addEventListener("fullscreen", this);
     99    } else {
    100      window.addEventListener("MozDOMFullscreen:Entered", this);
    101      window.addEventListener("MozDOMFullscreen:Exited", this);
    102    }
    103 
    104    window.addEventListener("activate", this);
    105    CustomizableUI.addListener(this);
    106 
    107    // We do this sync on init because in order to have the overflow button show up
    108    // we need to know whether anything is in the permanent panel area.
    109    this.overflowFixedList.hidden = false;
    110    // Also unhide the separator. We use CSS to hide/show it based on the panel's content.
    111    this.overflowFixedList.previousElementSibling.hidden = false;
    112    CustomizableUI.registerPanelNode(
    113      this.overflowFixedList,
    114      CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
    115    );
    116    this.updateOverflowStatus();
    117 
    118    Services.obs.notifyObservers(
    119      null,
    120      "appMenu-notifications-request",
    121      "refresh"
    122    );
    123 
    124    this._showAIMenuItem();
    125    this._initialized = true;
    126  },
    127 
    128  _initElements() {
    129    for (let [k, v] of Object.entries(this.kElements)) {
    130      // Need to do fresh let-bindings per iteration
    131      let getKey = k;
    132      let id = v;
    133      this.__defineGetter__(getKey, function () {
    134        delete this[getKey];
    135        return (this[getKey] = document.getElementById(id));
    136      });
    137    }
    138  },
    139 
    140  _eventListenersAdded: false,
    141  _ensureEventListenersAdded() {
    142    if (this._eventListenersAdded) {
    143      return;
    144    }
    145    this._addEventListeners();
    146  },
    147 
    148  _addEventListeners() {
    149    for (let event of this.kEvents) {
    150      this.panel.addEventListener(event, this);
    151    }
    152 
    153    let helpView = PanelMultiView.getViewNode(document, "PanelUI-helpView");
    154    helpView.addEventListener("ViewShowing", this._onHelpViewShow);
    155    helpView.addEventListener("command", this._onHelpCommand);
    156    this._onLibraryCommand = this._onLibraryCommand.bind(this);
    157    PanelMultiView.getViewNode(
    158      document,
    159      "appMenu-libraryView"
    160    ).addEventListener("command", this._onLibraryCommand);
    161    this.mainView.addEventListener("command", this);
    162    this.mainView.addEventListener("ViewShowing", this._onMainViewShow);
    163    this._eventListenersAdded = true;
    164  },
    165 
    166  _removeEventListeners() {
    167    for (let event of this.kEvents) {
    168      this.panel.removeEventListener(event, this);
    169    }
    170    let helpView = PanelMultiView.getViewNode(document, "PanelUI-helpView");
    171    helpView.removeEventListener("ViewShowing", this._onHelpViewShow);
    172    helpView.removeEventListener("command", this._onHelpCommand);
    173    PanelMultiView.getViewNode(
    174      document,
    175      "appMenu-libraryView"
    176    ).removeEventListener("command", this._onLibraryCommand);
    177    this.mainView.removeEventListener("command", this);
    178    this._eventListenersAdded = false;
    179  },
    180 
    181  uninit() {
    182    this._removeEventListeners();
    183 
    184    if (this._notificationPanel) {
    185      for (let event of this.kEvents) {
    186        this.notificationPanel.removeEventListener(event, this);
    187      }
    188      for (let event of this.kNotificationEvents) {
    189        this.notificationPanel.removeEventListener(event, this);
    190      }
    191    }
    192 
    193    Services.obs.removeObserver(this, "ai-window-state-changed");
    194    Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
    195    Services.obs.removeObserver(this, "appMenu-notifications");
    196    Services.obs.removeObserver(this, "show-update-progress");
    197 
    198    window.removeEventListener("MozDOMFullscreen:Entered", this);
    199    window.removeEventListener("MozDOMFullscreen:Exited", this);
    200    window.removeEventListener("fullscreen", this);
    201    window.removeEventListener("activate", this);
    202    this.menuButton.removeEventListener("mousedown", this);
    203    this.menuButton.removeEventListener("keypress", this);
    204    CustomizableUI.removeListener(this);
    205  },
    206 
    207  /**
    208   * Opens the menu panel if it's closed, or closes it if it's
    209   * open.
    210   *
    211   * @param aEvent the event that triggers the toggle.
    212   */
    213  toggle(aEvent) {
    214    // Don't show the panel if the window is in customization mode,
    215    // since this button doubles as an exit path for the user in this case.
    216    if (document.documentElement.hasAttribute("customizing")) {
    217      return;
    218    }
    219    this._ensureEventListenersAdded();
    220    if (this.panel.state == "open") {
    221      this.hide();
    222    } else if (this.panel.state == "closed") {
    223      this.show(aEvent);
    224    }
    225  },
    226 
    227  /**
    228   * Opens the menu panel. If the event target has a child with the
    229   * toolbarbutton-icon attribute, the panel will be anchored on that child.
    230   * Otherwise, the panel is anchored on the event target itself.
    231   *
    232   * @param aEvent the event (if any) that triggers showing the menu.
    233   */
    234  show(aEvent) {
    235    this._ensureShortcutsShown();
    236    (async () => {
    237      await this.ensureReady();
    238 
    239      if (
    240        this.panel.state == "open" ||
    241        document.documentElement.hasAttribute("customizing")
    242      ) {
    243        return;
    244      }
    245 
    246      if (ASRouter.initialized) {
    247        await ASRouter.sendTriggerMessage({
    248          browser: gBrowser.selectedBrowser,
    249          id: "menuOpened",
    250          context: { source: MenuMessage.SOURCES.APP_MENU },
    251        });
    252      }
    253 
    254      let domEvent = null;
    255      if (aEvent && aEvent.type != "command") {
    256        domEvent = aEvent;
    257      }
    258 
    259      let anchor = this._getPanelAnchor(this.menuButton);
    260      await PanelMultiView.openPopup(this.panel, anchor, {
    261        triggerEvent: domEvent,
    262      });
    263    })().catch(console.error);
    264  },
    265 
    266  /**
    267   * If the menu panel is being shown, hide it.
    268   */
    269  hide() {
    270    if (document.documentElement.hasAttribute("customizing")) {
    271      return;
    272    }
    273 
    274    PanelMultiView.hidePopup(this.panel);
    275  },
    276 
    277  observe(subject, topic, status) {
    278    switch (topic) {
    279      case "ai-window-state-changed":
    280        if (subject == window) {
    281          this._showAIMenuItem();
    282        }
    283        break;
    284 
    285      case "fullscreen-nav-toolbox":
    286        if (this._notifications) {
    287          this.updateNotifications(false);
    288        }
    289        break;
    290      case "appMenu-notifications":
    291        // Don't initialize twice.
    292        if (status == "init" && this._notifications) {
    293          break;
    294        }
    295        this._notifications = AppMenuNotifications.notifications;
    296        this.updateNotifications(true);
    297        break;
    298      case "show-update-progress":
    299        openAboutDialog();
    300        break;
    301    }
    302  },
    303 
    304  handleEvent(aEvent) {
    305    // Ignore context menus and menu button menus showing and hiding:
    306    if (aEvent.type.startsWith("popup") && aEvent.target != this.panel) {
    307      return;
    308    }
    309    switch (aEvent.type) {
    310      case "popupshowing":
    311        updateEditUIVisibility();
    312      // Fall through
    313      case "popupshown":
    314        if (aEvent.type == "popupshown") {
    315          CustomizableUI.addPanelCloseListeners(this.panel);
    316        }
    317      // Fall through
    318      case "popuphiding":
    319        if (aEvent.type == "popuphiding") {
    320          updateEditUIVisibility();
    321        }
    322      // Fall through
    323      case "popuphidden":
    324        this.updateNotifications();
    325        this._updatePanelButton(aEvent.target);
    326        if (aEvent.type == "popuphidden") {
    327          CustomizableUI.removePanelCloseListeners(this.panel);
    328          MenuMessage.hideAppMenuMessage(gBrowser.selectedBrowser);
    329        }
    330        break;
    331      case "mousedown":
    332        // On Mac, ctrl-click will send a context menu event from the widget, so
    333        // we don't want to bring up the panel when ctrl key is pressed.
    334        if (
    335          aEvent.button == 0 &&
    336          (AppConstants.platform != "macosx" || !aEvent.ctrlKey)
    337        ) {
    338          this.toggle(aEvent);
    339        }
    340        break;
    341      case "keypress":
    342        if (aEvent.key == " " || aEvent.key == "Enter") {
    343          this.toggle(aEvent);
    344          aEvent.stopPropagation();
    345        }
    346        break;
    347      case "MozDOMFullscreen:Entered":
    348      case "MozDOMFullscreen:Exited":
    349      case "fullscreen":
    350      case "activate":
    351        this.updateNotifications();
    352        break;
    353      case "command":
    354        this.onCommand(aEvent);
    355        break;
    356      case "buttoncommand":
    357        this._onNotificationButtonEvent(aEvent, "buttoncommand");
    358        break;
    359      case "secondarybuttoncommand":
    360        this._onNotificationButtonEvent(aEvent, "secondarybuttoncommand");
    361        break;
    362      case "learnmoreclick":
    363        // Don't fall back to PopupNotifications.
    364        aEvent.preventDefault();
    365        break;
    366    }
    367  },
    368 
    369  // Note that we listen for bubbling command events. In the case where the
    370  // button that the user clicks has a command attribute, those events are
    371  // redirected to the relevant command element, and we never see them in
    372  // here. Bear this in mind if you want to write code that applies to
    373  // all commands, for which this wouldn't work well.
    374  onCommand(aEvent) {
    375    let { target } = aEvent;
    376    switch (target.id) {
    377      case "appMenu-update-banner":
    378        this._onBannerItemSelected(aEvent);
    379        break;
    380      case "appMenu-fxa-label2":
    381        gSync.toggleAccountPanel(target, aEvent);
    382        break;
    383      case "appMenu-bookmarks-button":
    384        BookmarkingUI.showSubView(target);
    385        break;
    386      case "appMenu-history-button":
    387        this.showSubView("PanelUI-history", target);
    388        break;
    389      case "appMenu-passwords-button":
    390        LoginHelper.openPasswordManager(window, { entryPoint: "Mainmenu" });
    391        break;
    392      case "appMenu-fullscreen-button2":
    393        // Note that we're custom-handling the hiding of the panel to make
    394        // sure it disappears before entering fullscreen. Otherwise it can
    395        // end up moving around on the screen during the fullscreen transition.
    396        target.closest("panel").hidePopup();
    397        setTimeout(() => BrowserCommands.fullScreen(), 0);
    398        break;
    399      case "appMenu-settings-button":
    400        openPreferences();
    401        break;
    402      case "appMenu-more-button2":
    403        this.showMoreToolsPanel(target);
    404        break;
    405      case "appMenu-help-button2":
    406        this.showSubView("PanelUI-helpView", target);
    407        break;
    408    }
    409  },
    410 
    411  get isReady() {
    412    return !!this._isReady;
    413  },
    414 
    415  get isNotificationPanelOpen() {
    416    let panelState = this.notificationPanel.state;
    417 
    418    return panelState == "showing" || panelState == "open";
    419  },
    420 
    421  /**
    422   * Registering the menu panel is done lazily for performance reasons. This
    423   * method is exposed so that CustomizationMode can force panel-readyness in the
    424   * event that customization mode is started before the panel has been opened
    425   * by the user.
    426   *
    427   * @param aCustomizing (optional) set to true if this was called while entering
    428   *        customization mode. If that's the case, we trust that customization
    429   *        mode will handle calling beginBatchUpdate and endBatchUpdate.
    430   *
    431   * @return a Promise that resolves once the panel is ready to roll.
    432   */
    433  async ensureReady() {
    434    if (this._isReady) {
    435      return;
    436    }
    437 
    438    await window.delayedStartupPromise;
    439    this._ensureEventListenersAdded();
    440    this.panel.hidden = false;
    441    this._isReady = true;
    442  },
    443 
    444  /**
    445   * Switch the panel to the help view if it's not already
    446   * in that view.
    447   */
    448  showHelpView(aAnchor) {
    449    this._ensureEventListenersAdded();
    450    this.multiView.showSubView("PanelUI-helpView", aAnchor);
    451  },
    452 
    453  /**
    454   * Switch the panel to the "More Tools" view.
    455   *
    456   * @param moreTools The panel showing the "More Tools" view.
    457   */
    458  showMoreToolsPanel(moreTools) {
    459    this.showSubView("appmenu-moreTools", moreTools);
    460 
    461    // Notify DevTools the panel view is showing and need it to populate the
    462    // "Browser Tools" section of the panel. We notify the observer setup by
    463    // DevTools because we want to ensure the same menuitem list is shared
    464    // between both the AppMenu and toolbar button views.
    465    let view = document.getElementById("appmenu-developer-tools-view");
    466    Services.obs.notifyObservers(view, "web-developer-tools-view-showing");
    467  },
    468 
    469  /**
    470   * Shows a subview in the panel with a given ID.
    471   *
    472   * @param aViewId the ID of the subview to show.
    473   * @param aAnchor the element that spawned the subview.
    474   * @param aEvent the event triggering the view showing.
    475   */
    476  async showSubView(aViewId, aAnchor, aEvent) {
    477    if (aEvent) {
    478      // On Mac, ctrl-click will send a context menu event from the widget, so
    479      // we don't want to bring up the panel when ctrl key is pressed.
    480      if (
    481        aEvent.type == "mousedown" &&
    482        (aEvent.button != 0 ||
    483          (AppConstants.platform == "macosx" && aEvent.ctrlKey))
    484      ) {
    485        return;
    486      }
    487      if (
    488        aEvent.type == "keypress" &&
    489        aEvent.key != " " &&
    490        aEvent.key != "Enter"
    491      ) {
    492        return;
    493      }
    494    }
    495 
    496    this._ensureEventListenersAdded();
    497 
    498    let viewNode = PanelMultiView.getViewNode(document, aViewId);
    499    if (!viewNode) {
    500      console.error("Could not show panel subview with id: ", aViewId);
    501      return;
    502    }
    503 
    504    if (!aAnchor) {
    505      console.error(
    506        "Expected an anchor when opening subview with id: ",
    507        aViewId
    508      );
    509      return;
    510    }
    511 
    512    this._ensureShortcutsShown(viewNode);
    513    this.ensurePanicViewInitialized(viewNode);
    514 
    515    let container = aAnchor.closest("panelmultiview");
    516    if (container && !viewNode.hasAttribute("disallowSubView")) {
    517      container.showSubView(aViewId, aAnchor);
    518    } else if (!aAnchor.open) {
    519      aAnchor.open = true;
    520 
    521      let tempPanel = document.createXULElement("panel");
    522      tempPanel.setAttribute("type", "arrow");
    523      tempPanel.setAttribute("id", "customizationui-widget-panel");
    524      if (viewNode.hasAttribute("neverhidden")) {
    525        tempPanel.setAttribute("neverhidden", "true");
    526      }
    527 
    528      tempPanel.setAttribute("class", "cui-widget-panel panel-no-padding");
    529      tempPanel.setAttribute("viewId", aViewId);
    530      if (aAnchor.getAttribute("tabspecific")) {
    531        tempPanel.setAttribute("tabspecific", true);
    532      }
    533      if (aAnchor.getAttribute("locationspecific")) {
    534        tempPanel.setAttribute("locationspecific", true);
    535      }
    536      if (this._disableAnimations) {
    537        tempPanel.setAttribute("animate", "false");
    538      }
    539      tempPanel.setAttribute("context", "");
    540      document.getElementById("mainPopupSet").appendChild(tempPanel);
    541 
    542      let multiView = document.createXULElement("panelmultiview");
    543      multiView.setAttribute("id", "customizationui-widget-multiview");
    544      multiView.setAttribute("viewCacheId", "appMenu-viewCache");
    545      multiView.setAttribute("mainViewId", viewNode.id);
    546      multiView.appendChild(viewNode);
    547      tempPanel.appendChild(multiView);
    548      viewNode.classList.add("cui-widget-panelview", "PanelUI-subView");
    549 
    550      let viewShown = false;
    551      let panelRemover = event => {
    552        // Avoid bubbled events triggering the panel closing.
    553        if (event && event.target != tempPanel) {
    554          return;
    555        }
    556        viewNode.classList.remove("cui-widget-panelview");
    557        if (viewShown) {
    558          CustomizableUI.removePanelCloseListeners(tempPanel);
    559          tempPanel.removeEventListener("popuphidden", panelRemover);
    560        }
    561        aAnchor.open = false;
    562 
    563        PanelMultiView.removePopup(tempPanel);
    564      };
    565 
    566      if (aAnchor.parentNode.id == "PersonalToolbar") {
    567        tempPanel.classList.add("bookmarks-toolbar");
    568      }
    569 
    570      let anchor = this._getPanelAnchor(aAnchor);
    571 
    572      if (aAnchor != anchor && aAnchor.id) {
    573        anchor.setAttribute("consumeanchor", aAnchor.id);
    574      }
    575 
    576      try {
    577        viewShown = await PanelMultiView.openPopup(tempPanel, anchor, {
    578          position: "bottomright topright",
    579          triggerEvent: aEvent,
    580        });
    581      } catch (ex) {
    582        console.error(ex);
    583      }
    584 
    585      if (viewShown) {
    586        CustomizableUI.addPanelCloseListeners(tempPanel);
    587        tempPanel.addEventListener("popuphidden", panelRemover);
    588      } else {
    589        panelRemover();
    590      }
    591    }
    592  },
    593 
    594  /**
    595   * Adds FTL before appending the panic view markup to the main DOM.
    596   *
    597   * @param {panelview} panelView The Panic View panelview.
    598   */
    599  ensurePanicViewInitialized(panelView) {
    600    if (panelView.id != "PanelUI-panicView" || panelView._initialized) {
    601      return;
    602    }
    603 
    604    if (!this.panic) {
    605      this.panic = panelView;
    606    }
    607 
    608    MozXULElement.insertFTLIfNeeded("browser/panicButton.ftl");
    609    panelView._initialized = true;
    610  },
    611 
    612  /**
    613   * NB: The enable- and disableSingleSubviewPanelAnimations methods only
    614   * affect the hiding/showing animations of single-subview panels (tempPanel
    615   * in the showSubView method).
    616   */
    617  disableSingleSubviewPanelAnimations() {
    618    this._disableAnimations = true;
    619  },
    620 
    621  enableSingleSubviewPanelAnimations() {
    622    this._disableAnimations = false;
    623  },
    624 
    625  updateOverflowStatus() {
    626    let hasKids = this.overflowFixedList.hasChildNodes();
    627    if (hasKids && !this.navbar.hasAttribute("nonemptyoverflow")) {
    628      this.navbar.setAttribute("nonemptyoverflow", "true");
    629      this.overflowPanel.setAttribute("hasfixeditems", "true");
    630    } else if (!hasKids && this.navbar.hasAttribute("nonemptyoverflow")) {
    631      PanelMultiView.hidePopup(this.overflowPanel);
    632      this.overflowPanel.removeAttribute("hasfixeditems");
    633      this.navbar.removeAttribute("nonemptyoverflow");
    634    }
    635  },
    636 
    637  onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
    638    if (aContainer == this.overflowFixedList) {
    639      this.updateOverflowStatus();
    640    }
    641  },
    642 
    643  onAreaReset(aArea, aContainer) {
    644    if (aContainer == this.overflowFixedList) {
    645      this.updateOverflowStatus();
    646    }
    647  },
    648 
    649  /**
    650   * Sets the anchor node into the open or closed state, depending
    651   * on the state of the panel.
    652   */
    653  _updatePanelButton() {
    654    let { state } = this.panel;
    655    if (state == "open" || state == "showing") {
    656      this.menuButton.open = true;
    657      document.l10n.setAttributes(
    658        this.menuButton,
    659        "appmenu-menu-button-opened2"
    660      );
    661    } else {
    662      this.menuButton.open = false;
    663      document.l10n.setAttributes(
    664        this.menuButton,
    665        "appmenu-menu-button-closed2"
    666      );
    667    }
    668  },
    669 
    670  _onMainViewShow(event) {
    671    let panelview = event.target;
    672    let messageId = panelview.getAttribute(
    673      MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR
    674    );
    675    if (messageId) {
    676      MenuMessage.recordMenuMessageTelemetry(
    677        "IMPRESSION",
    678        MenuMessage.SOURCES.APP_MENU,
    679        messageId
    680      );
    681      let message = ASRouter.getMessageById(messageId);
    682      ASRouter.addImpression(message);
    683    }
    684    updateZoomUI(gBrowser.selectedBrowser);
    685  },
    686 
    687  _onHelpViewShow() {
    688    // Call global menu setup function
    689    buildHelpMenu();
    690 
    691    let helpMenu = document.getElementById("menu_HelpPopup");
    692    let items = this.getElementsByTagName("vbox")[0];
    693    let attrs = ["command", "onclick", "key", "disabled", "accesskey", "label"];
    694 
    695    // Remove all buttons from the view
    696    while (items.firstChild) {
    697      items.firstChild.remove();
    698    }
    699 
    700    // Add the current set of menuitems of the Help menu to this view
    701    let menuItems = Array.prototype.slice.call(
    702      helpMenu.getElementsByTagName("menuitem")
    703    );
    704    let fragment = document.createDocumentFragment();
    705    for (let node of menuItems) {
    706      if (node.hidden) {
    707        continue;
    708      }
    709      let button = document.createXULElement("toolbarbutton");
    710      // Copy specific attributes from a menuitem of the Help menu
    711      for (let attrName of attrs) {
    712        if (!node.hasAttribute(attrName)) {
    713          continue;
    714        }
    715        button.setAttribute(attrName, node.getAttribute(attrName));
    716      }
    717 
    718      // We have AppMenu-specific strings for the Help menu. By convention,
    719      // their localization IDs are set on "appmenu-data-l10n-id" attributes.
    720      let l10nId = node.getAttribute("appmenu-data-l10n-id");
    721      if (l10nId) {
    722        document.l10n.setAttributes(button, l10nId);
    723      }
    724 
    725      if (node.id) {
    726        button.id = "appMenu_" + node.id;
    727      }
    728 
    729      button.classList.add("subviewbutton");
    730      fragment.appendChild(button);
    731    }
    732 
    733    // The Enterprise Support menu item has a different location than its
    734    // placement in the menubar, so we need to specify it here.
    735    let helpPolicySupport = fragment.querySelector(
    736      "#appMenu_helpPolicySupport"
    737    );
    738    if (helpPolicySupport) {
    739      fragment.insertBefore(
    740        helpPolicySupport,
    741        fragment.querySelector("#appMenu_menu_HelpPopup_reportPhishingtoolmenu")
    742          .nextSibling
    743      );
    744    }
    745 
    746    items.appendChild(fragment);
    747  },
    748 
    749  _onHelpCommand(aEvent) {
    750    switch (aEvent.target.id) {
    751      case "appMenu_menu_openHelp":
    752        openHelpLink("firefox-help");
    753        break;
    754      case "appMenu_menu_layout_debugger":
    755        toOpenWindowByType(
    756          "mozapp:layoutdebug",
    757          "chrome://layoutdebug/content/layoutdebug.xhtml"
    758        );
    759        break;
    760      case "appMenu_feedbackPage":
    761        openFeedbackPage();
    762        break;
    763      case "appMenu_helpSafeMode":
    764        safeModeRestart();
    765        break;
    766      case "appMenu_troubleShooting":
    767        openTroubleshootingPage();
    768        break;
    769      case "appMenu_menu_HelpPopup_reportPhishingtoolmenu":
    770        openUILink(gSafeBrowsing.getReportURL("Phish"), aEvent, {
    771          triggeringPrincipal:
    772            Services.scriptSecurityManager.createNullPrincipal({}),
    773        });
    774        break;
    775      case "appMenu_menu_HelpPopup_reportPhishingErrortoolmenu":
    776        gSafeBrowsing.reportFalseDeceptiveSite();
    777        break;
    778      case "appMenu_helpSwitchDevice":
    779        openSwitchingDevicesPage();
    780        break;
    781      case "appMenu_aboutName":
    782        openAboutDialog();
    783        break;
    784      case "appMenu_helpPolicySupport":
    785        openTrustedLinkIn(Services.policies.getSupportMenu().URL.href, "tab");
    786        break;
    787      case "appMenu_torBrowserUserManual":
    788        gBrowser.selectedTab = gBrowser.addTab("about:manual", {
    789          triggeringPrincipal:
    790            Services.scriptSecurityManager.getSystemPrincipal(),
    791        });
    792        break;
    793    }
    794  },
    795 
    796  _onLibraryCommand(aEvent) {
    797    let button = aEvent.target;
    798    let { BookmarkingUI, DownloadsPanel } = button.ownerGlobal;
    799    switch (button.id) {
    800      case "appMenu-library-bookmarks-button":
    801        BookmarkingUI.showSubView(button);
    802        break;
    803      case "appMenu-library-history-button":
    804        this.showSubView("PanelUI-history", button);
    805        break;
    806      case "appMenu-library-downloads-button":
    807        DownloadsPanel.showDownloadsHistory();
    808        break;
    809    }
    810  },
    811 
    812  _hidePopup() {
    813    if (!this._notificationPanel) {
    814      return;
    815    }
    816 
    817    if (this.isNotificationPanelOpen) {
    818      this.notificationPanel.hidePopup();
    819    }
    820  },
    821 
    822  /**
    823   * Selects and marks an item by id from the main view. The ids are an array,
    824   * the first in the main view and the later ids in subsequent subviews that
    825   * become marked when the user opens the subview. The subview marking is
    826   * cancelled if a different subview is opened.
    827   */
    828  async selectAndMarkItem(itemIds) {
    829    // This shouldn't really occur, but return early just in case.
    830    if (document.documentElement.hasAttribute("customizing")) {
    831      return;
    832    }
    833 
    834    // This function was triggered from a button while the menu was
    835    // already open, so the panel should be in the process of hiding.
    836    // Wait for the panel to hide first, then reopen it.
    837    if (this.panel.state == "hiding") {
    838      await new Promise(resolve => {
    839        this.panel.addEventListener("popuphidden", resolve, { once: true });
    840      });
    841    }
    842 
    843    if (this.panel.state != "open") {
    844      await new Promise(resolve => {
    845        this.panel.addEventListener("ViewShown", resolve, { once: true });
    846        this.show();
    847      });
    848    }
    849 
    850    let currentView;
    851 
    852    let viewShownCB = event => {
    853      viewHidingCB();
    854 
    855      if (itemIds.length) {
    856        let subItem = window.document.getElementById(itemIds[0]);
    857        if (event.target.id == subItem?.closest("panelview")?.id) {
    858          Services.tm.dispatchToMainThread(() => {
    859            markItem(event.target);
    860          });
    861        } else {
    862          itemIds = [];
    863        }
    864      }
    865    };
    866 
    867    let viewHidingCB = () => {
    868      if (currentView) {
    869        currentView.ignoreMouseMove = false;
    870      }
    871      currentView = null;
    872    };
    873 
    874    let popupHiddenCB = () => {
    875      viewHidingCB();
    876      this.panel.removeEventListener("ViewShown", viewShownCB);
    877    };
    878 
    879    let markItem = viewNode => {
    880      let id = itemIds.shift();
    881      let item = window.document.getElementById(id);
    882      item.setAttribute("tabindex", "-1");
    883 
    884      currentView = PanelView.forNode(viewNode);
    885      currentView.selectedElement = item;
    886      currentView.focusSelectedElement(true);
    887 
    888      // Prevent the mouse from changing the highlight temporarily.
    889      // This flag gets removed when the view is hidden or a key
    890      // is pressed.
    891      currentView.ignoreMouseMove = true;
    892 
    893      if (itemIds.length) {
    894        this.panel.addEventListener("ViewShown", viewShownCB, { once: true });
    895      }
    896      this.panel.addEventListener("ViewHiding", viewHidingCB, { once: true });
    897    };
    898 
    899    this.panel.addEventListener("popuphidden", popupHiddenCB, { once: true });
    900    markItem(this.mainView);
    901  },
    902 
    903  updateNotifications(notificationsChanged) {
    904    let notifications = this._notifications;
    905    if (!notifications || !notifications.length) {
    906      if (notificationsChanged) {
    907        this._clearAllNotifications();
    908        this._hidePopup();
    909      }
    910      return;
    911    }
    912 
    913    if (
    914      (window.fullScreen && FullScreen.navToolboxHidden) ||
    915      document.fullscreenElement ||
    916      this._shouldSuppress()
    917    ) {
    918      this._hidePopup();
    919      return;
    920    }
    921 
    922    let doorhangers = notifications.filter(
    923      n => !n.dismissed && !n.options.badgeOnly
    924    );
    925 
    926    if (this.panel.state == "showing" || this.panel.state == "open") {
    927      // If the menu is already showing, then we need to dismiss all
    928      // notifications since we don't want their doorhangers competing for
    929      // attention. Don't hide the badge though; it isn't really in competition
    930      // with anything.
    931      doorhangers.forEach(n => {
    932        n.dismissed = true;
    933        if (n.options.onDismissed) {
    934          n.options.onDismissed(window);
    935        }
    936      });
    937      this._hidePopup();
    938      if (!notifications[0].options.badgeOnly) {
    939        this._showBannerItem(notifications[0]);
    940      }
    941    } else if (doorhangers.length) {
    942      // Only show the doorhanger if the window is focused and not fullscreen
    943      if (
    944        (window.fullScreen && this.autoHideToolbarInFullScreen) ||
    945        Services.focus.activeWindow !== window
    946      ) {
    947        this._hidePopup();
    948        this._showBadge(doorhangers[0]);
    949        this._showBannerItem(doorhangers[0]);
    950      } else {
    951        this._clearBadge();
    952        this._showNotificationPanel(doorhangers[0]);
    953      }
    954    } else {
    955      this._hidePopup();
    956      this._showBadge(notifications[0]);
    957      this._showBannerItem(notifications[0]);
    958    }
    959  },
    960 
    961  _showNotificationPanel(notification) {
    962    this._refreshNotificationPanel(notification);
    963 
    964    if (this.isNotificationPanelOpen) {
    965      return;
    966    }
    967 
    968    if (notification.options.beforeShowDoorhanger) {
    969      notification.options.beforeShowDoorhanger(document);
    970    }
    971 
    972    let anchor = this._getPanelAnchor(this.menuButton);
    973 
    974    // Insert Fluent files when needed before notification is opened
    975    MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
    976    MozXULElement.insertFTLIfNeeded("browser/appMenuNotifications.ftl");
    977 
    978    // After Fluent files are loaded into document replace data-lazy-l10n-ids with actual ones
    979    document
    980      .getElementById("appMenu-notification-popup")
    981      .querySelectorAll("[data-lazy-l10n-id]")
    982      .forEach(el => {
    983        el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
    984        el.removeAttribute("data-lazy-l10n-id");
    985      });
    986 
    987    this.notificationPanel.openPopup(anchor, "bottomright topright");
    988  },
    989 
    990  _clearNotificationPanel() {
    991    for (let popupnotification of this.notificationPanel.children) {
    992      popupnotification.hidden = true;
    993      popupnotification.notification = null;
    994    }
    995  },
    996 
    997  _clearAllNotifications() {
    998    this._clearNotificationPanel();
    999    this._clearBadge();
   1000    this._clearBannerItem();
   1001  },
   1002 
   1003  get notificationPanel() {
   1004    // Lazy load the panic-button-success-notification panel the first time we need to display it.
   1005    if (!this._notificationPanel) {
   1006      let template = document.getElementById("appMenuNotificationTemplate");
   1007      template.replaceWith(template.content);
   1008      this._notificationPanel = document.getElementById(
   1009        "appMenu-notification-popup"
   1010      );
   1011      for (let event of this.kEvents) {
   1012        this._notificationPanel.addEventListener(event, this);
   1013      }
   1014      for (let event of this.kNotificationEvents) {
   1015        this._notificationPanel.addEventListener(event, this);
   1016      }
   1017    }
   1018    return this._notificationPanel;
   1019  },
   1020 
   1021  get mainView() {
   1022    if (!this._mainView) {
   1023      this._mainView = PanelMultiView.getViewNode(document, "appMenu-mainView");
   1024    }
   1025    return this._mainView;
   1026  },
   1027 
   1028  get addonNotificationContainer() {
   1029    if (!this._addonNotificationContainer) {
   1030      this._addonNotificationContainer = PanelMultiView.getViewNode(
   1031        document,
   1032        "appMenu-addon-banners"
   1033      );
   1034    }
   1035 
   1036    return this._addonNotificationContainer;
   1037  },
   1038 
   1039  _formatDescriptionMessage(n) {
   1040    let text = {};
   1041    let array = n.options.message.split("<>");
   1042    text.start = array[0] || "";
   1043    text.name = n.options.name || "";
   1044    text.end = array[1] || "";
   1045    return text;
   1046  },
   1047 
   1048  _refreshNotificationPanel(notification) {
   1049    this._clearNotificationPanel();
   1050 
   1051    let popupnotificationID = this._getPopupId(notification);
   1052    let popupnotification = document.getElementById(popupnotificationID);
   1053 
   1054    popupnotification.setAttribute("id", popupnotificationID);
   1055 
   1056    if (notification.options.message) {
   1057      let desc = this._formatDescriptionMessage(notification);
   1058      popupnotification.setAttribute("label", desc.start);
   1059      popupnotification.setAttribute("name", desc.name);
   1060      popupnotification.setAttribute("endlabel", desc.end);
   1061    }
   1062    if (notification.options.onRefresh) {
   1063      notification.options.onRefresh(window);
   1064    }
   1065    if (notification.options.popupIconURL) {
   1066      popupnotification.setAttribute("icon", notification.options.popupIconURL);
   1067      popupnotification.setAttribute("hasicon", true);
   1068    }
   1069    if (notification.options.learnMoreURL) {
   1070      popupnotification.setAttribute(
   1071        "learnmoreurl",
   1072        notification.options.learnMoreURL
   1073      );
   1074    }
   1075 
   1076    popupnotification.notification = notification;
   1077    popupnotification.show();
   1078  },
   1079 
   1080  _showAIMenuItem() {
   1081    const isAIWindowActive = document.documentElement.hasAttribute("ai-window");
   1082    const aiMenuItem = PanelMultiView.getViewNode(
   1083      document,
   1084      "appMenu-new-ai-window-button"
   1085    );
   1086    const classicWindowMenuItem = PanelMultiView.getViewNode(
   1087      document,
   1088      "appMenu-new-classic-window-button"
   1089    );
   1090    const chatHistoryMenuItem = PanelMultiView.getViewNode(
   1091      document,
   1092      "appMenu-chats-history-button"
   1093    );
   1094 
   1095    aiMenuItem.hidden = !this.isAIWindowEnabled || isAIWindowActive;
   1096    classicWindowMenuItem.hidden = !this.isAIWindowEnabled || !isAIWindowActive;
   1097 
   1098    chatHistoryMenuItem.hidden = !this.isAIWindowEnabled || !isAIWindowActive;
   1099  },
   1100 
   1101  _showBadge(notification) {
   1102    let badgeStatus = this._getBadgeStatus(notification);
   1103    this.menuButton.setAttribute("badge-status", badgeStatus);
   1104  },
   1105 
   1106  // "Banner item" here refers to an item in the hamburger panel menu. They will
   1107  // typically show up as a colored row in the panel.
   1108  _showBannerItem(notification) {
   1109    const supportedIds = [
   1110      "update-downloading",
   1111      "update-available",
   1112      "update-manual",
   1113      "update-unsupported",
   1114      "update-restart",
   1115    ];
   1116    if (!supportedIds.includes(notification.id)) {
   1117      return;
   1118    }
   1119 
   1120    if (!this._panelBannerItem) {
   1121      this._panelBannerItem = this.mainView.querySelector(".panel-banner-item");
   1122    }
   1123 
   1124    const messageIDs = {
   1125      "update-downloading": "appmenuitem-banner-update-downloading",
   1126      "update-available": "appmenuitem-banner-update-available",
   1127      "update-manual": "appmenuitem-banner-update-manual",
   1128      "update-unsupported": "appmenuitem-banner-update-unsupported",
   1129      "update-restart": "appmenuitem-banner-update-restart",
   1130    };
   1131 
   1132    document.l10n.setAttributes(
   1133      this._panelBannerItem,
   1134      messageIDs[notification.id]
   1135    );
   1136 
   1137    this._panelBannerItem.setAttribute("notificationid", notification.id);
   1138    this._panelBannerItem.hidden = false;
   1139    this._panelBannerItem.notification = notification;
   1140  },
   1141 
   1142  _clearBadge() {
   1143    this.menuButton.removeAttribute("badge-status");
   1144  },
   1145 
   1146  _clearBannerItem() {
   1147    if (this._panelBannerItem) {
   1148      this._panelBannerItem.notification = null;
   1149      this._panelBannerItem.hidden = true;
   1150    }
   1151  },
   1152 
   1153  _onNotificationButtonEvent(event, type) {
   1154    event.preventDefault();
   1155 
   1156    let notificationEl = getNotificationFromElement(event.originalTarget);
   1157 
   1158    if (!notificationEl) {
   1159      throw new Error(
   1160        "PanelUI._onNotificationButtonEvent: couldn't find notification element"
   1161      );
   1162    }
   1163 
   1164    if (!notificationEl.notification) {
   1165      throw new Error(
   1166        "PanelUI._onNotificationButtonEvent: couldn't find notification"
   1167      );
   1168    }
   1169 
   1170    let notification = notificationEl.notification;
   1171 
   1172    if (type == "secondarybuttoncommand") {
   1173      AppMenuNotifications.callSecondaryAction(window, notification);
   1174    } else {
   1175      AppMenuNotifications.callMainAction(window, notification, true);
   1176    }
   1177  },
   1178 
   1179  _onBannerItemSelected(event) {
   1180    let target = event.originalTarget;
   1181    if (!target.notification) {
   1182      throw new Error(
   1183        "menucommand target has no associated action/notification"
   1184      );
   1185    }
   1186 
   1187    event.stopPropagation();
   1188    AppMenuNotifications.callMainAction(window, target.notification, false);
   1189  },
   1190 
   1191  _getPopupId(notification) {
   1192    return "appMenu-" + notification.id + "-notification";
   1193  },
   1194 
   1195  _getBadgeStatus(notification) {
   1196    return notification.id;
   1197  },
   1198 
   1199  _getPanelAnchor(candidate) {
   1200    let iconAnchor = candidate.badgeStack || candidate.icon;
   1201    return iconAnchor || candidate;
   1202  },
   1203 
   1204  _ensureShortcutsShown(view = this.mainView) {
   1205    if (view.hasAttribute("added-shortcuts")) {
   1206      return;
   1207    }
   1208    view.setAttribute("added-shortcuts", "true");
   1209    for (let button of view.querySelectorAll("toolbarbutton[key]")) {
   1210      let keyId = button.getAttribute("key");
   1211      let key = document.getElementById(keyId);
   1212      if (!key) {
   1213        continue;
   1214      }
   1215      button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
   1216    }
   1217  },
   1218 };
   1219 
   1220 XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
   1221 
   1222 /**
   1223 * Gets the currently selected locale for display.
   1224 * @return  the selected locale
   1225 */
   1226 function getLocale() {
   1227  return Services.locale.appLocaleAsBCP47;
   1228 }
   1229 
   1230 /**
   1231 * Given a DOM node inside a <popupnotification>, return the parent <popupnotification>.
   1232 */
   1233 function getNotificationFromElement(aElement) {
   1234  return aElement.closest("popupnotification");
   1235 }