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 }