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
--- 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]