Bug 1446101 - Part 2: Convert the all tabs panel to photon styles r?dao draft
authorMark Striemer <mstriemer@mozilla.com>
Wed, 06 Jun 2018 15:48:21 -0500
changeset 809607 e88e11bada22be57273b8c03c4462fc34546d31d
parent 809606 d84df0e392d2b55f82bc5e3b1fe776ca6e6d7b1e
child 809608 0e34167d196773f0bb6142544514a0f5ed96aa05
push id113724
push userbmo:mstriemer@mozilla.com
push dateFri, 22 Jun 2018 14:48:42 +0000
reviewersdao
bugs1446101
milestone62.0a1
Bug 1446101 - Part 2: Convert the all tabs panel to photon styles r?dao MozReview-Commit-ID: 3VzqnG6X5rw
browser/base/content/browser-allTabsMenu.inc.xul
browser/base/content/browser-allTabsMenu.js
browser/base/content/browser.js
browser/base/content/browser.xul
browser/base/content/moz.build
browser/base/jar.mn
browser/components/customizableui/content/panelUI.js
browser/components/originattributes/test/browser/browser_favicon_firstParty.js
browser/components/originattributes/test/browser/browser_favicon_userContextId.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/modules/TabsList.jsm
browser/modules/TabsPopup.jsm
browser/modules/moz.build
browser/themes/shared/icons/undo.svg
browser/themes/shared/jar.inc.mn
browser/themes/shared/tabs.inc.css
toolkit/content/widgets/toolbarbutton.xml
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-allTabsMenu.inc.xul
@@ -0,0 +1,51 @@
+<!-- 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/. -->
+
+<panel id="allTabsMenu"
+       class="cui-widget-panel"
+       role="group"
+       type="arrow"
+       hidden="true"
+       flip="slide"
+       position="bottomcenter topright"
+       noautofocus="true">
+  <panelmultiview mainViewId="allTabsMenu-allTabsView" disablekeynav="true">
+    <panelview id="allTabsMenu-allTabsView" class="PanelUI-subView">
+      <vbox class="panel-subview-body">
+        <toolbarbutton id="allTabsUndoCloseButton"
+                       class="undo-close-tab subviewbutton subviewbutton-iconic"
+                       label="&undoCloseTab.label;"
+                       key="key_undoCloseTab"
+                       command="History:UndoCloseTab"/>
+        <toolbarseparator/>
+        <toolbarbutton class="container-tabs-button subviewbutton subviewbutton-nav"
+                       closemenu="none"
+                       oncommand="PanelUI.showSubView('allTabsMenu-containerTabsView', this);"
+                       label="&newUserContext.label;"/>
+        <toolbarseparator class="container-tabs-separator"/>
+        <toolbarbutton id="allTabsMenu-hiddenTabsButton"
+                       class="hidden-tabs-button subviewbutton subviewbutton-nav"
+                       closemenu="none"
+                       oncommand="PanelUI.showSubView('allTabsMenu-hiddenTabsView', this);"
+                       label="&hiddenTabs.label;" />
+        <toolbarseparator class="hidden-tabs-separator"/>
+        <vbox id="allTabsMenu-allTabsViewTabs" class="panel-subview-body"/>
+      </vbox>
+    </panelview>
+
+    <panelview id="allTabsMenu-hiddenTabsView" class="PanelUI-subView">
+      <vbox class="panel-subview-body"/>
+    </panelview>
+
+    <panelview id="allTabsMenu-containerTabsView" class="PanelUI-subView">
+      <vbox class="panel-subview-body">
+        <toolbarseparator class="container-tabs-submenu-separator"/>
+        <toolbarbutton class="subviewbutton"
+                       label="&manageUserContext.label;"
+                       accesskey="&manageUserContext.accesskey;"
+                       command="Browser:OpenAboutContainers"/>
+      </vbox>
+    </panelview>
+  </panelmultiview>
+</panel>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-allTabsMenu.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+ChromeUtils.defineModuleGetter(this, "TabsPanel",
+                               "resource:///modules/TabsList.jsm");
+
+var gTabsPanel = {
+  kElements: {
+    allTabsButton: "alltabs-button",
+    allTabsView: "allTabsMenu-allTabsView",
+    allTabsViewTabs: "allTabsMenu-allTabsViewTabs",
+    containerTabsView: "allTabsMenu-containerTabsView",
+    hiddenTabsButton: "allTabsMenu-hiddenTabsButton",
+    hiddenTabsView: "allTabsMenu-hiddenTabsView",
+  },
+  _initialized: false,
+
+  initElements() {
+    for (let [name, id] of Object.entries(this.kElements)) {
+      this[name] = document.getElementById(id);
+    }
+  },
+
+  init() {
+    if (this._initialized) return;
+
+    this.initElements();
+
+    let hiddenTabsMenuButton = this.allTabsView.querySelector(".hidden-tabs-button");
+    let hiddenTabsSeparator = this.allTabsView.querySelector(".hidden-tabs-separator");
+    this.hiddenAudioTabsPopup = new TabsPanel({
+      view: this.allTabsView,
+      insertBefore: hiddenTabsSeparator,
+      filterFn: (tab) => tab.hidden && tab.soundPlaying,
+    });
+    this.allTabsPanel = new TabsPanel({
+      view: this.allTabsView,
+      containerNode: this.allTabsViewTabs,
+      filterFn: (tab) => !tab.pinned && !tab.hidden,
+      onPopulate() {
+        // Set the visible tab status.
+        let tabContainer = gBrowser.tabContainer;
+        // We don't want menu item decoration unless there is overflow.
+        if (tabContainer.getAttribute("overflow") != "true") {
+          return;
+        }
+
+        let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                                .getInterface(Ci.nsIDOMWindowUtils);
+        let arrowScrollboxRect = windowUtils.getBoundsWithoutFlushing(tabContainer.arrowScrollbox);
+        for (let row of this.rows) {
+          let curTabRect = windowUtils.getBoundsWithoutFlushing(row.tab);
+          if (curTabRect.left >= arrowScrollboxRect.left &&
+              curTabRect.right <= arrowScrollboxRect.right) {
+            row.setAttribute("tabIsVisible", "true");
+          } else {
+            row.removeAttribute("tabIsVisible");
+          }
+        }
+      },
+    });
+
+    let containerTabsButton = this.allTabsView.querySelector(".container-tabs-button");
+    let containerTabsSeparator = this.allTabsView.querySelector(".container-tabs-separator");
+    this.allTabsView.addEventListener("ViewShowing", (e) => {
+      PanelUI._ensureShortcutsShown(this.allTabsView);
+      e.target.querySelector(".undo-close-tab").disabled =
+          SessionStore.getClosedTabCount(window) == 0;
+
+      let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled")
+                                && !PrivateBrowsingUtils.isWindowPrivate(window);
+      containerTabsButton.hidden = !containersEnabled;
+      containerTabsSeparator.hidden = !containersEnabled;
+
+      let hasHiddenTabs = gBrowser.visibleTabs.length < gBrowser.tabs.length;
+      hiddenTabsMenuButton.hidden = !hasHiddenTabs;
+      hiddenTabsSeparator.hidden = !hasHiddenTabs;
+    });
+
+    this.allTabsView.addEventListener("ViewShown", (e) => {
+      let selectedRow = this.allTabsView.querySelector(".all-tabs-item[selected]");
+      selectedRow.scrollIntoView({block: "center"});
+    });
+
+    let containerTabsMenuSeparator = this.containerTabsView.querySelector("toolbarseparator");
+    this.containerTabsView.addEventListener("ViewShowing", (e) => {
+      let elements = [];
+      let frag = document.createDocumentFragment();
+
+      ContextualIdentityService.getPublicIdentities().forEach(identity => {
+        let menuitem = document.createElement("toolbarbutton");
+        menuitem.setAttribute("class", "subviewbutton subviewbutton-iconic");
+        menuitem.setAttribute("label", ContextualIdentityService.getUserContextLabel(identity.userContextId));
+        // The styles depend on this.
+        menuitem.setAttribute("usercontextid", identity.userContextId);
+        // The command handler depends on this.
+        menuitem.setAttribute("data-usercontextid", identity.userContextId);
+        menuitem.setAttribute("data-identity-color", identity.color);
+        menuitem.setAttribute("data-identity-icon", identity.icon);
+
+        menuitem.setAttribute("command", "Browser:NewUserContextTab");
+
+        frag.appendChild(menuitem);
+        elements.push(menuitem);
+      });
+
+      e.target.addEventListener("ViewHiding", () => {
+        for (let element of elements) {
+          element.remove();
+        }
+      }, {once: true});
+      containerTabsMenuSeparator.parentNode.insertBefore(frag, containerTabsMenuSeparator);
+    });
+
+    this.hiddenTabsPopup = new TabsPanel({
+      view: this.hiddenTabsView,
+      filterFn: (tab) => tab.hidden,
+    });
+
+    this._initialized = true;
+  },
+
+  showAllTabsPanel() {
+    this.init();
+    PanelUI.showSubView(this.kElements.allTabsView, this.allTabsButton);
+  },
+
+  hideAllTabsPanel() {
+    this.init();
+    PanelMultiView.hidePopup(this.allTabsView.closest("panel"));
+  },
+
+  showHiddenTabsPanel() {
+    this.init();
+    this.allTabsView.addEventListener("ViewShown", (e) => {
+      PanelUI.showSubView(this.kElements.hiddenTabsView, this.hiddenTabsButton);
+    }, {once: true});
+    this.showAllTabsPanel();
+  },
+};
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -51,17 +51,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   Sanitizer: "resource:///modules/Sanitizer.jsm",
   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
   SchedulePressure: "resource:///modules/SchedulePressure.jsm",
   ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
   SimpleServiceDiscovery: "resource://gre/modules/SimpleServiceDiscovery.jsm",
   SiteDataManager: "resource:///modules/SiteDataManager.jsm",
   SitePermissions: "resource:///modules/SitePermissions.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
-  TabsPopup: "resource:///modules/TabsPopup.jsm",
+  TabsPopup: "resource:///modules/TabsList.jsm",
   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
   Translation: "resource:///modules/translation/Translation.jsm",
   UITour: "resource:///modules/UITour.jsm",
   UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
   Utils: "resource://gre/modules/sessionstore/Utils.jsm",
   Weave: "resource://services-sync/main.js",
   WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
   fxAccounts: "resource://gre/modules/FxAccounts.jsm",
@@ -79,16 +79,18 @@ XPCOMUtils.defineLazyScriptGetter(this, 
 XPCOMUtils.defineLazyScriptGetter(this, "ZoomManager",
                                   "chrome://global/content/viewZoomOverlay.js");
 XPCOMUtils.defineLazyScriptGetter(this, "FullZoom",
                                   "chrome://browser/content/browser-fullZoom.js");
 XPCOMUtils.defineLazyScriptGetter(this, "PanelUI",
                                   "chrome://browser/content/customizableui/panelUI.js");
 XPCOMUtils.defineLazyScriptGetter(this, "gViewSourceUtils",
                                   "chrome://global/content/viewSourceUtils.js");
+XPCOMUtils.defineLazyScriptGetter(this, "gTabsPanel",
+                                  "chrome://browser/content/browser-allTabsMenu.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["LightWeightThemeWebInstaller",
                                          "gExtensionsNotifications",
                                          "gXPInstallObserver"],
                                   "chrome://browser/content/browser-addons.js");
 XPCOMUtils.defineLazyScriptGetter(this, "ctrlTab",
                                   "chrome://browser/content/browser-ctrlTab.js");
 XPCOMUtils.defineLazyScriptGetter(this, "CustomizationHandler",
                                   "chrome://browser/content/browser-customization.js");
@@ -1743,16 +1745,21 @@ var gBrowserInit = {
     }
 
     scheduleIdleTask(() => {
       // Initialize the Sync UI
       gSync.init();
     });
 
     scheduleIdleTask(() => {
+      // Initialize the all tabs menu
+      gTabsPanel.init();
+    });
+
+    scheduleIdleTask(() => {
       CombinedStopReload.startAnimationPrefMonitoring();
     });
 
     scheduleIdleTask(() => {
       // setup simple gestures support
       gGestureSupport.init(true);
 
       // setup history swipe animation
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -523,16 +523,17 @@
 #endif
     </tooltip>
 
 #include popup-notifications.inc
 
 #include ../../components/customizableui/content/panelUI.inc.xul
 #include ../../components/controlcenter/content/panel.inc.xul
 #include ../../components/downloads/content/downloadsPanel.inc.xul
+#include browser-allTabsMenu.inc.xul
 
     <hbox id="downloads-animation-container" mousethrough="always">
       <vbox id="downloads-notification-anchor" hidden="true">
         <vbox id="downloads-indicator-notification"/>
       </vbox>
     </hbox>
 
     <tooltip id="dynamic-shortcut-tooltip"
@@ -674,39 +675,20 @@
                      ondragover="newTabButtonObserver.onDragOver(event)"
                      ondragenter="newTabButtonObserver.onDragOver(event)"
                      ondragexit="newTabButtonObserver.onDragExit(event)"
                      cui-areatype="toolbar"
                      removable="true"/>
 
       <toolbarbutton id="alltabs-button"
                      class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button badged-button"
-                     type="menu"
+                     oncommand="gTabsPanel.showAllTabsPanel();"
                      label="&listAllTabs.label;"
                      tooltiptext="&listAllTabs.label;"
-                     removable="false">
-        <menupopup id="alltabs-popup"
-                   position="after_end">
-          <menuitem id="alltabs_undoCloseTab"
-                    key="key_undoCloseTab"
-                    label="&undoCloseTab.label;"
-                    observes="History:UndoCloseTab"/>
-          <menuseparator id="alltabs-popup-separator-1"/>
-          <menu id="alltabs_containersTab"
-                label="&newUserContext.label;">
-            <menupopup id="alltabs_containersMenuTab" />
-          </menu>
-          <menuseparator id="alltabs-popup-separator-2"/>
-          <menu id="alltabs_hiddenTabs"
-                label="&hiddenTabs.label;">
-            <menupopup id="alltabs_hiddenTabsMenu"/>
-          </menu>
-          <menuseparator id="alltabs-popup-separator-3"/>
-        </menupopup>
-      </toolbarbutton>
+                     removable="false"/>
 
       <hbox class="titlebar-placeholder" type="post-tabs"
             ordinal="1000"
             skipintoolbarset="true"/>
 
       <button class="accessibility-indicator" tooltiptext="&accessibilityIndicator.tooltip;"
               ordinal="1000"
               aria-live="polite" skipintoolbarset="true"/>
--- a/browser/base/content/moz.build
+++ b/browser/base/content/moz.build
@@ -153,13 +153,16 @@ with Files("hiddenWindow.xul"):
     BUG_COMPONENT = ("Firefox", "Device Permissions")
 
 with Files("macWindow.inc.xul"):
     BUG_COMPONENT = ("Firefox", "Shell Integration")
 
 with Files("tabbrowser*"):
     BUG_COMPONENT = ("Firefox", "Tabbed Browser")
 
+with Files("browser-allTabsMenu.js"):
+    BUG_COMPONENT = ("Firefox", "Tabbed Browser")
+
 with Files("webext-panels*"):
     BUG_COMPONENT = ("WebExtensions", "Frontend")
 
 with Files("webrtcIndicator*"):
     BUG_COMPONENT = ("Firefox", "Device Permissions")
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -25,16 +25,17 @@ browser.jar:
         content/browser/aboutRobots-widget-left.png   (content/aboutRobots-widget-left.png)
         content/browser/aboutTabCrashed.css           (content/aboutTabCrashed.css)
         content/browser/aboutTabCrashed.js            (content/aboutTabCrashed.js)
         content/browser/aboutTabCrashed.xhtml         (content/aboutTabCrashed.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
         content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
         content/browser/browser-addons.js             (content/browser-addons.js)
+        content/browser/browser-allTabsMenu.js        (content/browser-allTabsMenu.js)
         content/browser/browser-captivePortal.js      (content/browser-captivePortal.js)
         content/browser/browser-ctrlTab.js            (content/browser-ctrlTab.js)
         content/browser/browser-customization.js      (content/browser-customization.js)
         content/browser/browser-data-submission-info-bar.js (content/browser-data-submission-info-bar.js)
         content/browser/browser-compacttheme.js       (content/browser-compacttheme.js)
 #ifndef MOZILLA_OFFICIAL
         content/browser/browser-development-helpers.js (content/browser-development-helpers.js)
 #endif
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -838,22 +838,22 @@ const PanelUI = {
       document.getAnonymousElementByAttribute(candidate, "class",
                                               "toolbarbutton-badge-stack") ||
       document.getAnonymousElementByAttribute(candidate, "class",
                                               "toolbarbutton-icon");
     return iconAnchor || candidate;
   },
 
   _addedShortcuts: false,
-  _ensureShortcutsShown() {
-    if (this._addedShortcuts) {
+  _ensureShortcutsShown(view = this.mainView) {
+    if (view.hasAttribute("added-shortcuts")) {
       return;
     }
-    this._addedShortcuts = true;
-    for (let button of this.mainView.querySelectorAll("toolbarbutton[key]")) {
+    view.setAttribute("added-shortcuts", "true");
+    for (let button of view.querySelectorAll("toolbarbutton[key]")) {
       let keyId = button.getAttribute("key");
       let key = document.getElementById(keyId);
       if (!key) {
         continue;
       }
       button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
     }
   },
--- a/browser/components/originattributes/test/browser/browser_favicon_firstParty.js
+++ b/browser/components/originattributes/test/browser/browser_favicon_firstParty.js
@@ -254,17 +254,16 @@ async function doTest(aTestPage, aExpect
 async function doTestForAllTabsFavicon(aTestPage, aExpectedCookies, aIsThirdParty) {
   let firstPageURI = makeURI(TEST_SITE_ONE + aTestPage);
   let secondPageURI = makeURI(TEST_SITE_TWO + aTestPage);
   let faviconURI = aIsThirdParty ? THIRD_PARTY_SITE + FAVICON_URI :
                                    TEST_SITE_ONE + FAVICON_URI;
 
   // Set the 'overflow' attribute to make allTabs button available.
   let tabBrowser = document.getElementById("tabbrowser-tabs");
-  let allTabsBtn = document.getElementById("alltabs-button");
   tabBrowser.setAttribute("overflow", true);
 
   // Start to observe the event of that the favicon has been fully loaded.
   let promiseFaviconLoaded = waitOnFaviconLoaded(faviconURI);
 
   // Open the tab for the first site.
   let tabInfo = await openTab(TEST_SITE_ONE + aTestPage);
 
@@ -275,24 +274,25 @@ async function doTestForAllTabsFavicon(a
   // be made for the favicon of allTabs menuitem.
   clearAllImageCaches();
 
   // Start to observe the allTabs favicon requests earlier in case we miss it.
   let promiseObserveFavicon = observeFavicon(FIRST_PARTY_ONE, aExpectedCookies[0],
                                              firstPageURI, true);
 
   // Make the popup of allTabs showing up and trigger the loading of the favicon.
-  let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(allTabsBtn, "popupshown");
-  EventUtils.synthesizeMouseAtCenter(allTabsBtn, {});
+  let allTabsView = document.getElementById("allTabsMenu-allTabsView");
+  let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(allTabsView, "ViewShown");
+  gTabsPanel.showAllTabsPanel();
   await promiseObserveFavicon;
   await allTabsPopupShownPromise;
 
   // Close the popup of allTabs and wait until it's done.
-  let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(allTabsBtn, "popuphidden");
-  EventUtils.synthesizeMouseAtCenter(allTabsBtn, {});
+  let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(allTabsView.panelMultiView, "PanelMultiViewHidden");
+  gTabsPanel.hideAllTabsPanel();
   await allTabsPopupHiddenPromise;
 
   // Close the tab.
   BrowserTestUtils.removeTab(tabInfo.tab);
 
   faviconURI = aIsThirdParty ? THIRD_PARTY_SITE + FAVICON_URI :
                                TEST_SITE_TWO + FAVICON_URI;
 
@@ -308,24 +308,24 @@ async function doTestForAllTabsFavicon(a
   // Clear the image cache for the favicon of the second site.
   clearAllImageCaches();
 
   // Start to observe the allTabs favicon requests earlier in case we miss it.
   promiseObserveFavicon = observeFavicon(FIRST_PARTY_TWO, aExpectedCookies[1],
                                          secondPageURI, true);
 
   // Make the popup of allTabs showing up again.
-  allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(allTabsBtn, "popupshown");
-  EventUtils.synthesizeMouseAtCenter(allTabsBtn, {});
+  allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(allTabsView, "ViewShown");
+  gTabsPanel.showAllTabsPanel();
   await promiseObserveFavicon;
   await allTabsPopupShownPromise;
 
   // Close the popup of allTabs and wait until it's done.
-  allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(allTabsBtn, "popuphidden");
-  EventUtils.synthesizeMouseAtCenter(allTabsBtn, {});
+  allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(allTabsView.panelMultiView, "PanelMultiViewHidden");
+  gTabsPanel.hideAllTabsPanel();
   await allTabsPopupHiddenPromise;
 
   // Close the tab.
   BrowserTestUtils.removeTab(tabInfo.tab);
 
   // Reset the 'overflow' attribute to make the allTabs button hidden again.
   tabBrowser.removeAttribute("overflow");
 }
--- a/browser/components/originattributes/test/browser/browser_favicon_userContextId.js
+++ b/browser/components/originattributes/test/browser/browser_favicon_userContextId.js
@@ -210,17 +210,16 @@ async function doTest(aTestPage, aFavico
 }
 
 async function doTestForAllTabsFavicon(aTestPage, aFaviconHost, aFaviconURL) {
   let cookies = await generateCookies(aFaviconHost);
   let pageURI = makeURI(aTestPage);
 
   // Set the 'overflow' attribute to make allTabs button available.
   let tabBrowser = document.getElementById("tabbrowser-tabs");
-  let allTabsBtn = document.getElementById("alltabs-button");
   tabBrowser.setAttribute("overflow", true);
 
   // Create the observer object for observing request channels of the personal
   // container.
   let observer = new FaviconObserver(USER_CONTEXT_ID_PERSONAL, cookies[0], pageURI, aFaviconURL, true);
 
   // Add the observer earlier in case we miss it.
   let promiseWaitOnFaviconLoaded = waitOnFaviconLoaded(aFaviconURL);
@@ -234,24 +233,25 @@ async function doTestForAllTabsFavicon(a
   // We need to clear the image cache here for making sure the network request will
   // be made for the favicon of allTabs menuitem.
   clearAllImageCaches();
 
   // Add the observer for listening favicon requests.
   Services.obs.addObserver(observer, "http-on-modify-request");
 
   // Make the popup of allTabs showing up and trigger the loading of the favicon.
-  let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(allTabsBtn, "popupshown");
-  EventUtils.synthesizeMouseAtCenter(allTabsBtn, {});
+  let allTabsView = document.getElementById("allTabsMenu-allTabsView");
+  let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(allTabsView, "ViewShown");
+  gTabsPanel.showAllTabsPanel();
   await observer.promise;
   await allTabsPopupShownPromise;
 
   // Close the popup of allTabs and wait until it's done.
-  let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(allTabsBtn, "popuphidden");
-  EventUtils.synthesizeMouseAtCenter(allTabsBtn, {});
+  let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(allTabsView.panelMultiView, "PanelMultiViewHidden");
+  gTabsPanel.hideAllTabsPanel();
   await allTabsPopupHiddenPromise;
 
   // Remove the observer for not receiving the favicon requests for opening a tab
   // since we want to focus on the favicon of allTabs menu here.
   Services.obs.removeObserver(observer, "http-on-modify-request");
 
   // Close the tab.
   BrowserTestUtils.removeTab(tabInfo.tab);
@@ -269,24 +269,24 @@ async function doTestForAllTabsFavicon(a
 
   // Reset the observer for observing requests for the work container.
   observer.reset(USER_CONTEXT_ID_WORK, cookies[1], pageURI, aFaviconURL, true);
 
   // Add the observer back for listening the favicon requests for allTabs menuitem.
   Services.obs.addObserver(observer, "http-on-modify-request");
 
   // Make the popup of allTabs showing up again.
-  allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(allTabsBtn, "popupshown");
-  EventUtils.synthesizeMouseAtCenter(allTabsBtn, {});
+  allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(allTabsView, "ViewShown");
+  gTabsPanel.showAllTabsPanel();
   await observer.promise;
   await allTabsPopupShownPromise;
 
   // Close the popup of allTabs and wait until it's done.
-  allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(allTabsBtn, "popuphidden");
-  EventUtils.synthesizeMouseAtCenter(allTabsBtn, {});
+  allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(allTabsView.panelMultiView, "PanelMultiViewHidden");
+  gTabsPanel.hideAllTabsPanel();
   await allTabsPopupHiddenPromise;
 
   Services.obs.removeObserver(observer, "http-on-modify-request");
 
   // Close the tab.
   BrowserTestUtils.removeTab(tabInfo.tab);
 
   // Reset the 'overflow' attribute to make the allTabs button hidden again.
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -303,16 +303,18 @@ These should match what Safari and other
 <!ENTITY inspectContextMenu.accesskey "Q">
 
 <!ENTITY inspectA11YContextMenu.label     "Inspect Accessibility Properties">
 
 <!ENTITY fileMenu.label         "File">
 <!ENTITY fileMenu.accesskey       "F">
 <!ENTITY newUserContext.label             "New Container Tab">
 <!ENTITY newUserContext.accesskey         "B">
+<!ENTITY manageUserContext.label          "Manage Containers">
+<!ENTITY manageUserContext.accesskey      "O">
 <!ENTITY newNavigatorCmd.label        "New Window">
 <!ENTITY newNavigatorCmd.key        "N">
 <!ENTITY newNavigatorCmd.accesskey      "N">
 <!ENTITY newPrivateWindow.label     "New Private Window">
 <!ENTITY newPrivateWindow.accesskey "W">
 
 <!ENTITY editMenu.label         "Edit">
 <!ENTITY editMenu.accesskey       "E">
rename from browser/modules/TabsPopup.jsm
rename to browser/modules/TabsList.jsm
--- a/browser/modules/TabsPopup.jsm
+++ b/browser/modules/TabsList.jsm
@@ -1,17 +1,30 @@
 /* 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/. */
 
 "use strict";
 
-var EXPORTED_SYMBOLS = ["TabsPopup"];
+ChromeUtils.defineModuleGetter(this, "PanelMultiView",
+                               "resource:///modules/PanelMultiView.jsm");
+
+var EXPORTED_SYMBOLS = ["TabsPanel", "TabsPopup"];
 const NSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
+function setAttributes(element, attrs) {
+  for (let [name, value] of Object.entries(attrs)) {
+    if (value) {
+      element.setAttribute(name, value);
+    } else {
+      element.removeAttribute(name);
+    }
+  }
+}
+
 class TabsListBase {
   constructor({className, filterFn, insertBefore, onPopulate, containerNode}) {
     this.className = className;
     this.filterFn = filterFn;
     this.insertBefore = insertBefore;
     this.onPopulate = onPopulate;
     this.containerNode = containerNode;
 
@@ -125,32 +138,33 @@ class TabsListBase {
   _removeItem(item, tab) {
     this.tabToElement.delete(tab);
     item.remove();
   }
 }
 
 class TabsPopup extends TabsListBase {
   /*
-   * Handle menuitem rows for tab objects in a menupopup.
+   * Generate toolbarbuttons for tabs that meet some criteria.
    *
    * @param {object} opts Options for configuring this instance.
-   * @param {string} opts.className
-   *                 An optional class name to be added to menuitem elements.
    * @param {function} opts.filterFn
-   *                   A function to filter which tabs are added to the popup.
+   *                   A function to filter which tabs are used.
+   * @param {object} opts.view
+   *                 A panel view to listen to events on.
+   * @param {object} opts.containerNode
+   *                 An optional element to append elements to, if ommitted they
+   *                 will be appended to opts.view.firstChild or before
+   *                 opts.insertBefore.
    * @param {object} opts.insertBefore
-   *                 An optional element to insert the menuitems before in the
-   *                 popup, if omitted they will be appended to popup.
+   *                 An optional element to insert the results before, if
+   *                 omitted they will be appended to opts.containerNode.
    * @param {function} opts.onPopulate
    *                   An optional function that will be called with the
    *                   popupshowing event that caused the menu to be populated.
-   * @param {object} opts.popup
-   *                 A menupopup element to populate and register the show/hide
-   *                 listeners on.
    */
   constructor({className, filterFn, insertBefore, onPopulate, popup}) {
     super({className, filterFn, insertBefore, onPopulate, containerNode: popup});
     this.containerNode.addEventListener("popupshowing", this);
   }
 
   handleEvent(event) {
     switch (event.type) {
@@ -225,8 +239,145 @@ class TabsPopup extends TabsListBase {
     if (item.firstChild)
       item.firstChild.remove();
     if (tab.hasAttribute("muted"))
       addEndImage().setAttribute("muted", "true");
     else if (tab.hasAttribute("soundplaying"))
       addEndImage().setAttribute("soundplaying", "true");
   }
 }
+
+const TABS_PANEL_EVENTS = {
+  show: "ViewShowing",
+  hide: "PanelMultiViewHidden",
+};
+
+class TabsPanel extends TabsListBase {
+  constructor(opts) {
+    super({
+      ...opts,
+      containerNode: opts.containerNode || opts.view.firstChild,
+      onPopulate: (...args) => {
+        this._onPopulate(...args);
+        if (typeof opts.onPopulate == "function") {
+          opts.onPopulate.call(this, ...args);
+        }
+      },
+    });
+    this.view = opts.view;
+    this.view.addEventListener(TABS_PANEL_EVENTS.show, this);
+    this.panelMultiView = null;
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case TABS_PANEL_EVENTS.hide:
+        if (event.target == this.panelMultiView) {
+          this._cleanup();
+          this.panelMultiView = null;
+        }
+        break;
+      case TABS_PANEL_EVENTS.show:
+        if (!this.listenersRegistered && event.target == this.view) {
+          this.panelMultiView = this.view.panelMultiView;
+          this._populate(event);
+        }
+        break;
+      case "command":
+        if (event.target.hasAttribute("toggle-mute")) {
+          event.target.tab.toggleMuteAudio();
+          break;
+        }
+      default:
+        super.handleEvent(event);
+        break;
+    }
+  }
+
+  _onPopulate(event) {
+    // The loading throbber can't be set until the toolbarbutton is rendered,
+    // so do that on first populate.
+    for (let row of this.rows) {
+      this._setImageAttributes(row, row.tab);
+    }
+  }
+
+  _selectTab(tab) {
+    super._selectTab(tab);
+    PanelMultiView.hidePopup(this.view.closest("panel"));
+  }
+
+  _setupListeners() {
+    super._setupListeners();
+    this.panelMultiView.addEventListener(TABS_PANEL_EVENTS.hide, this);
+  }
+
+  _cleanupListeners() {
+    super._cleanupListeners();
+    this.panelMultiView.removeEventListener(TABS_PANEL_EVENTS.hide, this);
+  }
+
+  _createRow(tab) {
+    let {doc} = this;
+    let row = doc.createElementNS(NSXUL, "toolbaritem");
+    row.setAttribute("class", "all-tabs-item");
+
+    let button = doc.createElementNS(NSXUL, "toolbarbutton");
+    button.setAttribute("class", "all-tabs-button subviewbutton subviewbutton-iconic");
+    button.setAttribute("flex", "1");
+    button.setAttribute("crop", "right");
+    button.tab = tab;
+
+    row.appendChild(button);
+
+    let secondaryButton = doc.createElementNS(NSXUL, "toolbarbutton");
+    secondaryButton.setAttribute(
+      "class", "all-tabs-secondary-button subviewbutton subviewbutton-iconic");
+    secondaryButton.setAttribute("closemenu", "none");
+    secondaryButton.setAttribute("toggle-mute", "true");
+    secondaryButton.tab = tab;
+    row.appendChild(secondaryButton);
+
+    this._setRowAttributes(row, tab);
+
+    return row;
+  }
+
+  _setRowAttributes(row, tab) {
+    setAttributes(row, {selected: tab.selected});
+
+    let busy = tab.getAttribute("busy");
+    let button = row.firstChild;
+    setAttributes(button, {
+      busy,
+      label: tab.label,
+      image: !busy && tab.getAttribute("image"),
+      iconloadingprincipal: tab.getAttribute("iconloadingprincipal"),
+    });
+
+    this._setImageAttributes(row, tab);
+
+    let secondaryButton = row.querySelector(".all-tabs-secondary-button");
+    setAttributes(secondaryButton, {
+      muted: tab.muted,
+      soundplaying: tab.soundPlaying,
+      hidden: !(tab.muted || tab.soundPlaying),
+    });
+  }
+
+  _setImageAttributes(row, tab) {
+    let button = row.firstChild;
+    let busy = tab.getAttribute("busy");
+    let image = this.doc.getAnonymousElementByAttribute(
+      button, "class", "toolbarbutton-icon") ||
+      this.doc.getAnonymousElementByAttribute(
+        button, "class", "toolbarbutton-icon tab-throbber-fallback");
+
+    if (image) {
+      setAttributes(image, {busy});
+      if (busy) {
+        image.classList.add("tab-throbber-fallback");
+      } else {
+        image.classList.remove("tab-throbber-fallback");
+      }
+    }
+  }
+}
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -98,17 +98,17 @@ with Files("Sanitizer.jsm"):
     BUG_COMPONENT = ("Firefox", "Preferences")
 
 with Files("SiteDataManager.jsm"):
     BUG_COMPONENT = ("Firefox", "Preferences")
 
 with Files("SitePermissions.jsm"):
     BUG_COMPONENT = ("Firefox", "Site Identity and Permission Panels")
 
-with Files("TabsPopup.jsm"):
+with Files("TabsList.jsm"):
     BUG_COMPONENT = ("Firefox", "Tabbed Browser")
 
 with Files("ThemeVariableMap.jsm"):
     BUG_COMPONENT = ("WebExtensions", "Themes")
 
 with Files("TransientPrefs.jsm"):
     BUG_COMPONENT = ("Firefox", "Preferences")
 
@@ -167,17 +167,17 @@ EXTRA_JS_MODULES += [
     'ProcessHangMonitor.jsm',
     'ReaderParent.jsm',
     'RemotePrompt.jsm',
     'Sanitizer.jsm',
     'SavantShieldStudy.jsm',
     'SchedulePressure.jsm',
     'SiteDataManager.jsm',
     'SitePermissions.jsm',
-    'TabsPopup.jsm',
+    'TabsList.jsm',
     'ThemeVariableMap.jsm',
     'TransientPrefs.jsm',
     'webrtcUI.jsm',
     'ZoomUI.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES += [
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/icons/undo.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+  <path fill="context-fill" d="M16,11 C16,11.5522847 15.5522847,12 15,12 C14.4477153,12 14,11.5522847 14,11 C14,8.23857625 11.7614237,6 9,6 C6.94968096,6 5.18760031,7.23409522 4.41604369,9 L7.0043326,9 C7.55661734,9 8.0043326,9.44771525 8.0043326,10 C8.0043326,10.5522847 7.55661734,11 7.0043326,11 L2,11 C1.44771525,11 1,10.5522847 1,10 L1,5 C1,4.44771525 1.44771525,4 2,4 C2.55228475,4 3,4.44771525 3,5 L3,7.39241339 C4.22489715,5.35958217 6.45367486,4 9,4 C12.8659932,4 16,7.13400675 16,11 Z"></path>
+</svg>
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -171,16 +171,17 @@
   skin/classic/browser/send-to-device.svg             (../shared/icons/send-to-device.svg)
   skin/classic/browser/settings.svg                   (../shared/icons/settings.svg)
   skin/classic/browser/sidebars.svg                   (../shared/icons/sidebars.svg)
   skin/classic/browser/sidebars-right.svg             (../shared/icons/sidebars-right.svg)
   skin/classic/browser/stop.svg                       (../shared/icons/stop.svg)
   skin/classic/browser/stop-to-reload.svg             (../shared/icons/stop-to-reload.svg)
   skin/classic/browser/sync.svg                       (../shared/icons/sync.svg)
   skin/classic/browser/tab.svg                        (../shared/icons/tab.svg)
+  skin/classic/browser/undo.svg                       (../shared/icons/undo.svg)
   skin/classic/browser/bookmarks-toolbar.svg          (../shared/icons/bookmarks-toolbar.svg)
   skin/classic/browser/webIDE.svg                     (../shared/icons/webIDE.svg)
   skin/classic/browser/window.svg                     (../shared/icons/window.svg)
   skin/classic/browser/zoom-in.svg                    (../shared/icons/zoom-in.svg)
   skin/classic/browser/zoom-out.svg                   (../shared/icons/zoom-out.svg)
 
 
   skin/classic/browser/search-engine-placeholder.png           (../shared/search/search-engine-placeholder.png)
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -779,8 +779,68 @@
 
 .alltabs-endimage[muted] {
   list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio-muted.svg);
 }
 
 .alltabs-endimage[activemedia-blocked] {
   list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio-blocked.svg);
 }
+
+#appMenu-allTabsView {
+  --blue-40: #45a1ff;
+  max-width: 42em;
+}
+
+.all-tabs-item > .all-tabs-secondary-button {
+  padding: 0;
+  padding-inline-start: 6px;
+  opacity: .8;
+  -moz-context-properties: fill;
+  fill: currentColor;
+}
+
+.all-tabs-secondary-button:hover {
+  opacity: 1;
+}
+
+.all-tabs-item:hover {
+  background-color: var(--arrowpanel-dimmed)
+}
+
+.all-tabs-item > .all-tabs-button > .tab-throbber-fallback {
+  display: block;
+  margin-inline-end: 0;
+}
+
+.all-tabs-item[selected] {
+  box-shadow: inset 4px 0 var(--blue-40);
+}
+
+.all-tabs-item[tabIsVisible] {
+  box-shadow: inset -4px 0 ThreeDShadow;
+}
+
+.all-tabs-item[selected][tabIsVisible] {
+  box-shadow: inset -4px 0 ThreeDShadow, inset 4px 0 var(--blue-40);
+}
+
+.all-tabs-button,
+.all-tabs-secondary-button {
+  background-color: transparent !important;
+}
+
+.all-tabs-secondary-button > label {
+  display: none;
+  margin: 0 5.5px;
+}
+
+.all-tabs-secondary-button[soundplaying] {
+  list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio-playing.svg);
+}
+
+.all-tabs-secondary-button[muted] {
+  list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio-muted.svg);
+}
+
+.undo-close-tab {
+  list-style-image: url(chrome://browser/skin/undo.svg);
+}
--- a/toolkit/content/widgets/toolbarbutton.xml
+++ b/toolkit/content/widgets/toolbarbutton.xml
@@ -12,17 +12,17 @@
   <binding id="toolbarbutton" display="xul:button"
            extends="chrome://global/content/bindings/button.xml#button-base">
     <resources>
       <stylesheet src="chrome://global/skin/toolbarbutton.css"/>
     </resources>
 
     <content>
       <children includes="observes|template|menupopup|panel|tooltip"/>
-      <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,type,consumeanchor"/>
+      <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,type,consumeanchor,triggeringprincipal=iconloadingprincipal"/>
       <xul:label class="toolbarbutton-text" crop="right" flex="1"
                  xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/>
       <xul:label class="toolbarbutton-multiline-text" flex="1"
                  xbl:inherits="xbl:text=label,accesskey,wrap"/>
       <children includes="box"/>
       <xul:dropmarker anonid="dropmarker" type="menu"
                       class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
     </content>