Bug 1219725 - Add a button for session restore to the tab bar. r=gijs,r=mikedeboer,r=whimboo,r=dao ui-r=shorlander draft
authorErica Wright <ewright@mozilla.com>
Tue, 20 Jun 2017 11:33:55 -0700
changeset 598365 f8621e0b17598fa42be89f746017e93fd8b0354a
parent 598307 92eb911c35da48907d326604c4c92cf55e551895
child 634449 07c0bdd83f63741413bfd5604f228392f1dbe13e
push id65175
push userbmo:ewright@mozilla.com
push dateWed, 21 Jun 2017 16:28:03 +0000
reviewersgijs, mikedeboer, whimboo, dao, shorlander
bugs1219725
milestone56.0a1
Bug 1219725 - Add a button for session restore to the tab bar. r=gijs,r=mikedeboer,r=whimboo,r=dao ui-r=shorlander MozReview-Commit-ID: 6zrEbIxXp8c
browser/app/profile/firefox.js
browser/base/content/browser.js
browser/base/content/tabbrowser.xml
browser/modules/BrowserUITelemetry.jsm
browser/themes/linux/browser.css
browser/themes/shared/tabs.inc.css
browser/themes/windows/browser.css
testing/firefox-ui/tests/functional/sessionstore/manifest.ini
testing/firefox-ui/tests/functional/sessionstore/test_tabbar_session_restore_button.py
testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -474,16 +474,21 @@ pref("browser.tabs.loadDivertedInBackgro
 pref("browser.tabs.loadBookmarksInBackground", false);
 pref("browser.tabs.tabClipWidth", 140);
 #ifdef UNIX_BUT_NOT_MAC
 pref("browser.tabs.drawInTitlebar", false);
 #else
 pref("browser.tabs.drawInTitlebar", true);
 #endif
 
+// false - disable the tabbar session restore button
+// true - enable the tabbar session restore button
+// To be enabled with shield
+pref("browser.tabs.restorebutton", false);
+
 // When tabs opened by links in other tabs via a combination of
 // browser.link.open_newwindow being set to 3 and target="_blank" etc are
 // closed:
 // true   return to the tab that opened this tab (its owner)
 // false  return to the adjacent tab (old default)
 pref("browser.tabs.selectOwnerOnClose", true);
 
 pref("browser.tabs.showAudioPlayingIcon", true);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -8161,26 +8161,57 @@ function switchToTabHavingURI(aURI, aOpe
 
   return false;
 }
 
 var RestoreLastSessionObserver = {
   init() {
     if (SessionStore.canRestoreLastSession &&
         !PrivateBrowsingUtils.isWindowPrivate(window)) {
+      if (Services.prefs.getBoolPref("browser.tabs.restorebutton")) {
+        let {restoreTabsButton} = gBrowser.tabContainer;
+        let restoreTabsButtonWrapper = restoreTabsButton.parentNode;
+        restoreTabsButtonWrapper.setAttribute("session-exists", "true");
+        gBrowser.tabContainer.updateSessionRestoreVisibility();
+        gBrowser.tabContainer.addEventListener("TabOpen", this);
+      }
       Services.obs.addObserver(this, "sessionstore-last-session-cleared", true);
       goSetCommandEnabled("Browser:RestoreLastSession", true);
     }
   },
 
+  handleEvent(event) {
+    switch (event.type) {
+     case "TabOpen":
+        this.removeRestoreButton();
+        break;
+    }
+  },
+
+  removeRestoreButton() {
+    let {restoreTabsButton, restoreTabsButtonWrapperWidth} = gBrowser.tabContainer;
+    let restoreTabsButtonWrapper = restoreTabsButton.parentNode;
+    restoreTabsButtonWrapper.removeAttribute("session-exists");
+    gBrowser.tabContainer.addEventListener("transitionend", function maxWidthTransitionHandler(e) {
+      if (e.propertyName == "max-width") {
+        gBrowser.tabContainer.updateSessionRestoreVisibility();
+        gBrowser.tabContainer.removeEventListener("transitionend", maxWidthTransitionHandler);
+      }
+    });
+    restoreTabsButton.style.maxWidth = `${restoreTabsButtonWrapperWidth}px`;
+    requestAnimationFrame(() => restoreTabsButton.style.maxWidth = 0);
+    gBrowser.tabContainer.removeEventListener("TabOpen", this);
+  },
+
   observe() {
     // The last session can only be restored once so there's
     // no way we need to re-enable our menu item.
     Services.obs.removeObserver(this, "sessionstore-last-session-cleared");
     goSetCommandEnabled("Browser:RestoreLastSession", false);
+    this.removeRestoreButton();
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference])
 };
 
 function restoreLastSession() {
   SessionStore.restoreLastSession();
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -5867,26 +5867,36 @@
         <children/>
         <xul:toolbarbutton class="tabs-newtab-button"
                            anonid="tabs-newtab-button"
                            command="cmd_newNavigatorTab"
                            onclick="checkForMiddleClick(this, event);"
                            onmouseover="document.getBindingParent(this)._enterNewTab();"
                            onmouseout="document.getBindingParent(this)._leaveNewTab();"
                            tooltip="dynamic-shortcut-tooltip"/>
+        <xul:hbox class="restore-tabs-button-wrapper"
+                  anonid="restore-tabs-button-wrapper">
+          <xul:toolbarbutton anonid="restore-tabs-button"
+                             class="restore-tabs-button"
+                             onclick="SessionStore.restoreLastSession();"/>
+        </xul:hbox>
+
         <xul:spacer class="closing-tabs-spacer" anonid="closing-tabs-spacer"
                     style="width: 0;"/>
       </xul:arrowscrollbox>
     </content>
 
     <implementation implements="nsIDOMEventListener, nsIObserver">
       <constructor>
         <![CDATA[
           this.mTabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth");
 
+          let { restoreTabsButton } = this;
+          restoreTabsButton.setAttribute("label", this.tabbrowser.mStringBundle.getString("tabs.restoreLastTabs"));
+
           var tab = this.firstChild;
           tab.label = this.tabbrowser.mStringBundle.getString("tabs.emptyTabTitle");
           tab.setAttribute("onerror", "this.removeAttribute('image');");
 
           window.addEventListener("resize", this);
           window.addEventListener("load", this);
 
           Services.prefs.addObserver("privacy.userContext", this);
@@ -5919,16 +5929,66 @@
       </field>
 
       <field name="_firstTab">null</field>
       <field name="_lastTab">null</field>
       <field name="_afterSelectedTab">null</field>
       <field name="_beforeHoveredTab">null</field>
       <field name="_afterHoveredTab">null</field>
       <field name="_hoveredTab">null</field>
+      <field name="restoreTabsButton">
+        document.getAnonymousElementByAttribute(this, "anonid", "restore-tabs-button");
+      </field>
+      <field name="_restoreTabsButtonWrapperWidth">0</field>
+      <field name="windowUtils">
+        window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      </field>
+
+      <property name="restoreTabsButtonWrapperWidth" readonly="true">
+        <getter>
+          if (!this._restoreTabsButtonWrapperWidth) {
+            this._restoreTabsButtonWrapperWidth = this.windowUtils
+              .getBoundsWithoutFlushing(this.restoreTabsButton.parentNode)
+              .width;
+          }
+          return this._restoreTabsButtonWrapperWidth;
+        </getter>
+      </property>
+
+      <method name="updateSessionRestoreVisibility">
+        <body><![CDATA[
+          let {restoreTabsButton, restoreTabsButtonWrapperWidth, windowUtils, mTabstripWidth} = this;
+          let restoreTabsButtonWrapper = restoreTabsButton.parentNode;
+
+          if (!restoreTabsButtonWrapper.getAttribute("session-exists")) {
+            restoreTabsButtonWrapper.removeAttribute("shown");
+            return;
+          }
+
+          let newTabButton = document.getAnonymousElementByAttribute(
+            this, "anonid", "tabs-newtab-button");
+
+          // If there are no pinned tabs it will multiply by 0 and result in 0
+          let pinnedTabsWidth = windowUtils.getBoundsWithoutFlushing(this.firstChild).width * this._lastNumPinned;
+
+          let numUnpinnedTabs = this.childNodes.length - this._lastNumPinned;
+          let unpinnedTabsWidth = windowUtils.getBoundsWithoutFlushing(this.lastChild).width * numUnpinnedTabs;
+
+          let tabbarUsedSpace = pinnedTabsWidth + unpinnedTabsWidth
+            + windowUtils.getBoundsWithoutFlushing(newTabButton).width;
+
+          // Subtract the elements' widths from the available space to ensure
+          // that showing the restoreTabsButton won't cause any overflow.
+          if ((mTabstripWidth - tabbarUsedSpace) > restoreTabsButtonWrapperWidth) {
+            restoreTabsButtonWrapper.setAttribute("shown", "true");
+          } else {
+            restoreTabsButtonWrapper.removeAttribute("shown");
+          }
+        ]]></body>
+      </method>
 
       <method name="observe">
         <parameter name="aSubject"/>
         <parameter name="aTopic"/>
         <parameter name="aData"/>
         <body><![CDATA[
           switch (aTopic) {
             case "nsPref:changed":
@@ -6449,16 +6509,17 @@
 
               TabsInTitlebar.updateAppearance();
 
               var width = this.mTabstrip.boxObject.width;
               if (width != this.mTabstripWidth) {
                 this.adjustTabstrip();
                 this._handleTabSelect(false);
                 this.mTabstripWidth = width;
+                this.updateSessionRestoreVisibility();
               }
               break;
             case "mouseout":
               // If the "related target" (the node to which the pointer went) is not
               // a child of the current document, the mouse just left the window.
               let relatedTarget = aEvent.relatedTarget;
               if (relatedTarget && relatedTarget.ownerDocument == document)
                 break;
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -150,16 +150,17 @@ XPCOMUtils.defineLazyGetter(this, "ALL_B
     "zoom-out-button",
     "zoom-reset-button",
     "zoom-in-button",
     "BMB_bookmarksPopup",
     "BMB_unsortedBookmarksPopup",
     "BMB_bookmarksToolbarPopup",
     "search-go-button",
     "soundplaying-icon",
+    "restore-tabs-button",
   ]
   return DEFAULT_ITEMS.concat(PALETTE_ITEMS)
                       .concat(SPECIAL_CASES);
 });
 
 const OTHER_MOUSEUP_MONITORED_ITEMS = [
   "PlacesChevron",
   "PlacesToolbarItems",
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1279,8 +1279,13 @@ notification.pluginVulnerable > .notific
 .browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
   overflow: hidden;
 }
 
 .webextension-popup-browser {
   border-radius: inherit;
 }
+
+/* Prevent movement in the restore-tabs-button when it's clicked. */
+.restore-tabs-button:hover:active:not([disabled="true"]) {
+  padding: 3px;
+}
\ No newline at end of file
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -599,8 +599,61 @@
 
 .alltabs-endimage[muted] {
   list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio-muted.svg);
 }
 
 .alltabs-endimage[blocked] {
   list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio-blocked.svg);
 }
+
+.restore-tabs-button-wrapper {
+  visibility: hidden;
+  position: fixed; /* so the button does not take up actual space and cause overflow buttons in the tabbar when hidden */
+}
+
+.restore-tabs-button-wrapper[shown] {
+  visibility: visible;
+  position: initial;
+}
+
+.restore-tabs-button {
+  box-sizing: border-box;
+  -moz-appearance: none;
+  background-color: hsl(0,0%,0%,.04);
+  border: 1px solid hsla(0,0%,16%,.2);
+  border-radius: 3px;
+  margin: 3px;
+  margin-inline-start: 9px;
+  transition: max-width 300ms;
+}
+
+.restore-tabs-button:hover {
+  background-color: hsl(0,0%,0%,.08);
+}
+
+.restore-tabs-button:active {
+  background-color: hsl(0,0%,0%,.11);
+}
+
+#TabsToolbar[brighttext] .restore-tabs-button {
+  background-color: hsl(0,0%,100%,.07);
+  border-color:currentColor;
+  color: currentColor;
+  opacity: .7;
+}
+
+#TabsToolbar[brighttext] .restore-tabs-button:hover {
+  background-color: hsl(0,0%,100%,.17);
+}
+
+#TabsToolbar[brighttext] .restore-tabs-button:active {
+  background-color: hsl(0,0%,100%,.27);
+}
+
+.restore-tabs-button > .toolbarbutton-icon {
+  display: none;
+}
+
+.restore-tabs-button > .toolbarbutton-text {
+  display: -moz-box;
+  padding: 0 5px;
+}
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2054,8 +2054,13 @@ notification.pluginVulnerable > .notific
 
 #ContentSelectDropdown > .isOpenedViaTouch > menucaption > .menu-iconic-text,
 #ContentSelectDropdown > .isOpenedViaTouch > menuitem > .menu-iconic-text {
   /* Touch padding should follow the 11/12 ratio, where 12px is the default
      font-size with 11px being the preferred padding size. */
   padding-top: .9167em;
   padding-bottom: .9167em;
 }
+
+/* Prevent movement in the restore-tabs-button when it's clicked. */
+.restore-tabs-button:hover:active:not([disabled="true"]) {
+  padding: 3px;
+}
--- a/testing/firefox-ui/tests/functional/sessionstore/manifest.ini
+++ b/testing/firefox-ui/tests/functional/sessionstore/manifest.ini
@@ -1,5 +1,6 @@
 [DEFAULT]
 tags = local
 
+[test_tabbar_session_restore_button.py]
 [test_restore_windows_after_restart.py]
 skip-if = (os == "win" || e10s) # Bug 1291844 and Bug 1228446
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/functional/sessionstore/test_tabbar_session_restore_button.py
@@ -0,0 +1,106 @@
+# 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/.
+from firefox_puppeteer import PuppeteerMixin
+from marionette_harness import MarionetteTestCase
+from marionette_driver import Wait
+
+
+class TestBaseTabbarSessionRestoreButton(PuppeteerMixin, MarionetteTestCase):
+    def setUp(self, prefValue=True):
+        super(TestBaseTabbarSessionRestoreButton, self).setUp()
+        self.marionette.enforce_gecko_prefs({'browser.tabs.restorebutton': prefValue})
+
+        # Each list element represents a window of tabs loaded at
+        # some testing URL, the URLS are arbitrary.
+        self.test_windows = set([
+            (self.marionette.absolute_url('layout/mozilla_projects.html'),
+             self.marionette.absolute_url('layout/mozilla.html'),
+             self.marionette.absolute_url('layout/mozilla_mission.html')),
+        ])
+
+        self.open_windows(self.test_windows)
+
+        self.marionette.quit(in_app=True)
+        self.marionette.start_session()
+        self.marionette.set_context('chrome')
+
+    def open_windows(self, window_sets):
+        win = self.browser
+        for index, urls in enumerate(window_sets):
+            if index > 0:
+                win = self.browser.open_browser()
+            win.switch_to()
+            self.open_tabs(win, urls)
+
+    def open_tabs(self, win, urls):
+        # If there are any remaining URLs for this window,
+        # open some new tabs and navigate to them.
+        with self.marionette.using_context('content'):
+            if isinstance(urls, str):
+                self.marionette.navigate(urls)
+            else:
+                for index, url in enumerate(urls):
+                    if index > 0:
+                        with self.marionette.using_context('chrome'):
+                            win.tabbar.open_tab()
+                    self.marionette.navigate(url)
+
+    def tearDown(self):
+        try:
+            # Create a fresh profile for subsequent tests.
+            self.restart(clean=True)
+        finally:
+            super(TestBaseTabbarSessionRestoreButton, self).tearDown()
+
+
+class TestTabbarSessionRestoreButton(TestBaseTabbarSessionRestoreButton):
+    def setUp(self):
+        super(TestTabbarSessionRestoreButton, self).setUp()
+
+    def test_session_exists(self):
+        wrapper = self.puppeteer.windows.current.tabbar.restore_tabs_button_wrapper
+
+        # A session-exists attribute has been added to the button,
+        # which allows it to show.
+        self.assertEqual(wrapper.get_attribute('session-exists'), 'true')
+
+    def test_window_resizing(self):
+        wrapper = self.puppeteer.windows.current.tabbar.restore_tabs_button_wrapper
+
+        # Ensure the window is large enough to show the button.
+        self.marionette.set_window_size(1200, 1200)
+        self.assertEqual(wrapper.value_of_css_property('visibility'), 'visible')
+
+        # Set the window small enough that the button disappears.
+        self.marionette.set_window_size(335, 335)
+        self.assertEqual(wrapper.value_of_css_property('visibility'), 'hidden')
+
+    def test_click_restore(self):
+        button = self.puppeteer.windows.current.tabbar.restore_tabs_button
+
+        # The new browser window is not the same window as last time,
+        # and did not automatically restore the session, so there is only one tab.
+        self.assertEqual(len(self.puppeteer.windows.current.tabbar.tabs), 1)
+
+        button.click()
+
+        Wait(self.marionette, timeout=20).until(
+            lambda _: len(self.puppeteer.windows.current.tabbar.tabs) > 1,
+            message='there is only one tab, so the session was not restored')
+
+        # After clicking the button to restore the session,
+        # there is more than one tab.
+        self.assertTrue(len(self.puppeteer.windows.current.tabbar.tabs) > 1)
+
+
+class TestNoTabbarSessionRestoreButton(TestBaseTabbarSessionRestoreButton):
+    def setUp(self):
+        super(TestNoTabbarSessionRestoreButton, self).setUp(False)
+
+    def test_pref_off_button_does_not_show(self):
+        wrapper = self.puppeteer.windows.current.tabbar.restore_tabs_button_wrapper
+
+        # A session-exists attribute is not on the button,
+        # since the button will never show itself with the pref off.
+        self.assertEqual(wrapper.get_attribute('session-exists'), '')
--- a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py
@@ -32,16 +32,33 @@ class TabBar(UIBaseLib):
     def newtab_button(self):
         """The DOM element which represents the new tab button.
 
         :returns: Reference to the new tab button.
         """
         return self.toolbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'tabs-newtab-button'})
 
     @property
+    def restore_tabs_button(self):
+        """The DOM element which represents the restore tabs button.
+
+        :returns: Reference to the restore tabs button.
+        """
+        return self.toolbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'restore-tabs-button'})
+
+    @property
+    def restore_tabs_button_wrapper(self):
+        """The DOM element which represents the restore tabs button wrapper.
+
+        :returns: Reference to the restore tabs button wrapper.
+        """
+        return self.toolbar.find_element(
+            By.ANON_ATTRIBUTE, {'anonid': 'restore-tabs-button-wrapper'})
+
+    @property
     def tabs(self):
         """List of all the :class:`Tab` instances of the current browser window.
 
         :returns: List of :class:`Tab` instances.
         """
         tabs = self.toolbar.find_elements(By.TAG_NAME, 'tab')
 
         return [Tab(self.marionette, self.window, tab) for tab in tabs]