Bug 380960 - Implement closing tabs animation. r=fyan,gavin
authorDão Gottwald <dao@mozilla.com>
Fri, 06 Aug 2010 22:15:18 +0200
changeset 49132 06b0aaa3623b6dc30cdd8829401c552dbc1b61ec
parent 49131 804ddd711e76af783275773ae5f5be3fff2e2e35
child 49133 e601f6dcd81ae8029f96071d028b844bc116de73
push id14914
push userdgottwald@mozilla.com
push dateSat, 07 Aug 2010 07:19:25 +0000
treeherdermozilla-central@2cf6079d86dd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfyan, gavin
bugs380960
milestone2.0b4pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 380960 - Implement closing tabs animation. r=fyan,gavin
browser/app/profile/firefox.js
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/tabbrowser.xml
browser/base/content/test/Makefile.in
browser/base/content/test/browser_bug380960.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -338,16 +338,17 @@ pref("browser.tabs.insertRelatedAfterCur
 pref("browser.tabs.warnOnClose", true);
 pref("browser.tabs.warnOnOpen", true);
 pref("browser.tabs.maxOpenBeforeWarn", 15);
 pref("browser.tabs.loadInBackground", true);
 pref("browser.tabs.opentabfor.middleclick", true);
 pref("browser.tabs.loadDivertedInBackground", false);
 pref("browser.tabs.loadBookmarksInBackground", false);
 pref("browser.tabs.tabClipWidth", 140);
+pref("browser.tabs.animate", true);
 
 // Where to show tab close buttons:
 // 0  on active tab only
 // 1  on all tabs until tabClipWidth is reached, then active tab only
 // 2  no close buttons at all
 // 3  at the end of the tabstrip
 pref("browser.tabs.closeButtons", 1);
 
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -24,36 +24,33 @@ tabbrowser {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tab");
 }
 
 .tabbrowser-tab:not([pinned]) {
   -moz-box-flex: 100;
   max-width: 250px;
   min-width: 100px;
   width: 0;
+  -moz-transition: min-width .2s ease-out, max-width .25s ease-out;
 }
 
 .tabbrowser-tab:not([pinned]):not([fadein]) {
   max-width: 1px;
   min-width: 1px;
 }
 
-.tabbrowser-tab[fadein]:not([pinned]) {
-  -moz-transition: min-width .2s ease-out, max-width .25s ease-out;
-}
-
 .tabbrowser-tab:not([fadein]):not([pinned]) > .tab-text,
 .tabbrowser-tab:not([fadein]):not([pinned]) > .tab-icon-image,
 .tabbrowser-tab:not([fadein]):not([pinned]) > .tab-close-button {
   opacity: 0 !important;
 }
 
-.tabbrowser-tab[fadein] > .tab-text,
-.tabbrowser-tab[fadein] > .tab-icon-image,
-.tabbrowser-tab[fadein] > .tab-close-button {
+.tabbrowser-tab > .tab-text,
+.tabbrowser-tab > .tab-icon-image,
+.tabbrowser-tab > .tab-close-button {
   -moz-transition: opacity .25s;
 }
 
 .tabbrowser-tab[pinned] {
   position: fixed;
   display: block; /* position:fixed already does this (bug 579776), but let's be explicit */
 }
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2043,17 +2043,17 @@ function BrowserCloseTabOrWindow() {
   // If we're not a browser window, just close the window
   if (window.location.href != getBrowserURL()) {
     closeWindow(true);
     return;
   }
 #endif
 
   // If the current tab is the last one, this will close the window.
-  gBrowser.removeCurrentTab();
+  gBrowser.removeCurrentTab({animate: true});
 }
 
 function BrowserTryToCloseWindow()
 {
   if (WindowIsClosing())
     window.close();     // WindowIsClosing does all the necessary checks
 }
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1092,17 +1092,18 @@
             t.setAttribute("validate", "never");
             t.setAttribute("onerror", "this.removeAttribute('image');");
             t.className = "tabbrowser-tab";
 
             // When overflowing, new tabs are scrolled into view smoothly, which
             // doesn't go well together with the width transition. So we skip the
             // transition in that case.
             if (aSkipAnimation ||
-                this.tabContainer.getAttribute("overflow") == "true") {
+                this.tabContainer.getAttribute("overflow") == "true" ||
+                !Services.prefs.getBoolPref("browser.tabs.animate")) {
               t.setAttribute("fadein", "true");
               setTimeout(function (tabContainer) {
                 tabContainer._handleNewTab(t);
               }, 0, this.tabContainer);
             } else {
               setTimeout(function (tabContainer) {
                 if (t.pinned)
                   tabContainer._handleNewTab(t);
@@ -1295,59 +1296,86 @@
                   this.removeTab(this.tabs[i]);
               }
             }
           ]]>
         </body>
       </method>
 
       <method name="removeCurrentTab">
+        <parameter name="aParams"/>
         <body>
           <![CDATA[
-            this.removeTab(this.mCurrentTab);
+            this.removeTab(this.mCurrentTab, aParams);
           ]]>
         </body>
       </method>
 
       <field name="_removingTabs">
         []
       </field>
 
       <method name="removeTab">
         <parameter name="aTab"/>
+        <parameter name="aParams"/>
         <body>
           <![CDATA[
-            this._endRemoveTab(this._beginRemoveTab(aTab, false, null, true));
+            var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
+            if (aParams)
+              var animate = aParams.animate;
+
+            if (!this._beginRemoveTab(aTab, false, null, true))
+              return;
+
+            /* Don't animate if:
+                - the caller didn't opt in
+                - this is the last tab in the window
+                - this is a pinned tab
+                - a bunch of other tabs are already closing (arbitrary threshold)
+                - the fadein attribute hasn't been set yet
+                - browser.tabs.animate is false   */
+
+            if (!animate ||
+                isLastTab ||
+                aTab.pinned ||
+                this._removingTabs.length > 3 ||
+                aTab.getAttribute("fadein") != "true" ||
+                !Services.prefs.getBoolPref("browser.tabs.animate")) {
+              this._endRemoveTab(aTab);
+              return;
+            }
+
+            this._blurTab(aTab);
+            aTab.removeAttribute("fadein");
           ]]>
         </body>
       </method>
 
       <!-- Tab close requests are ignored if the window is closing anyway,
            e.g. when holding Ctrl+W. -->
       <field name="_windowIsClosing">
         false
       </field>
 
-      <!-- Returns everything that _endRemoveTab needs in an array. -->
       <method name="_beginRemoveTab">
         <parameter name="aTab"/>
         <parameter name="aTabWillBeMoved"/>
         <parameter name="aCloseWindowWithLastTab"/>
         <parameter name="aCloseWindowFastpath"/>
         <body>
           <![CDATA[
             if (this._removingTabs.indexOf(aTab) > -1 || this._windowIsClosing)
-              return null;
+              return false;
 
             var browser = this.getBrowserForTab(aTab);
 
             if (!aTabWillBeMoved) {
               let ds = browser.docShell;
               if (ds && ds.contentViewer && !ds.contentViewer.permitUnload())
-                return null;
+                return false;
             }
 
             var closeWindow = false;
             var newTab = false;
             if (this.tabs.length - this._removingTabs.length == 1) {
               closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab :
                             !window.toolbar.visible ||
                               this.tabContainer._closeWindowWithLastTab;
@@ -1391,44 +1419,52 @@
 
             // Remove this tab as the owner of any other tabs, since it's going away.
             Array.forEach(this.tabs, function (tab) {
               if ("owner" in tab && tab.owner == aTab)
                 // |tab| is a child of the tab we're removing, make it an orphan
                 tab.owner = null;
             });
 
-            return [aTab, closeWindow, newTab];
+            aTab._endRemoveArgs = [closeWindow, newTab];
+            return true;
           ]]>
         </body>
       </method>
 
       <method name="_endRemoveTab">
-        <parameter name="args"/>
+        <parameter name="aTab"/>
         <body>
           <![CDATA[
-            if (!args)
+            if (!aTab || !aTab._endRemoveArgs)
               return;
-            var [aTab, aCloseWindow, aNewTab] = args;
+
+            var [aCloseWindow, aNewTab] = aTab._endRemoveArgs;
+            aTab._endRemoveArgs = null;
+
+            if (this._windowIsClosing) {
+              aCloseWindow = false;
+              aNewTab = false;
+            }
 
             this._lastRelatedTab = null;
 
             // update the UI early for responsiveness
             aTab.collapsed = true;
             if (aNewTab)
               this.addTab("about:blank", {skipAnimation: true});
             this.tabContainer._fillTrailingGap();
             this._blurTab(aTab);
 
             this._removingTabs.splice(this._removingTabs.indexOf(aTab), 1);
 
             if (aCloseWindow) {
               this._windowIsClosing = true;
               while (this._removingTabs.length)
-                this._endRemoveTab([this._removingTabs[0], false]);
+                this._endRemoveTab(this._removingTabs[0]);
             } else if (!this._windowIsClosing) {
               if (aNewTab)
                 focusAndSelectUrlBar();
 
               // workaround for bug 345399
               this.tabContainer.mTabstrip._updateScrollButtonsDisabledState();
             }
 
@@ -1544,23 +1580,23 @@
       </method>
 
       <method name="swapBrowsersAndCloseOther">
         <parameter name="aOurTab"/>
         <parameter name="aOtherTab"/>
         <body>
           <![CDATA[
             // That's gBrowser for the other window, not the tab's browser!
-            var remoteBrowser =
-              aOtherTab.ownerDocument.defaultView.getBrowser();
+            var remoteBrowser = aOtherTab.ownerDocument.defaultView.gBrowser;
 
             // First, start teardown of the other browser.  Make sure to not
             // fire the beforeunload event in the process.  Close the other
             // window if this was its last tab.
-            var endRemoveArgs = remoteBrowser._beginRemoveTab(aOtherTab, true, true);
+            if (!remoteBrowser._beginRemoveTab(aOtherTab, true, true))
+              return;
 
             // Unhook our progress listener
             var ourIndex = aOurTab._tPos;
             const filter = this.mTabFilters[ourIndex];
             var tabListener = this.mTabListeners[ourIndex];
             var ourBrowser = this.getBrowserForTab(aOurTab);
             ourBrowser.webProgress.removeProgressListener(filter);
             filter.removeProgressListener(tabListener);
@@ -1577,17 +1613,17 @@
               if (aOurTab == this.selectedTab)
                 this.mIsBusy = true;
             }
 
             // Swap the docshells
             ourBrowser.swapDocShells(aOtherTab.linkedBrowser);
 
             // Finish tearing down the tab that's going away.
-            remoteBrowser._endRemoveTab(endRemoveArgs);
+            remoteBrowser._endRemoveTab(aOtherTab);
 
             // Restore the progress listener
             tabListener = this.mTabProgressListener(aOurTab, ourBrowser,
                                                     tabListenerBlank);
             this.mTabListeners[ourIndex] = tabListener;
             filter.addProgressListener(tabListener,
               Components.interfaces.nsIWebProgress.NOTIFY_ALL);
 
@@ -2156,17 +2192,17 @@
               this.tabContainer.advanceSelectedTab(offset, true);
               aEvent.stopPropagation();
               aEvent.preventDefault();
           }
 #else
           if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey &&
               aEvent.keyCode == KeyEvent.DOM_VK_F4 &&
               this.mTabBox.handleCtrlPageUpDown) {
-            this.removeCurrentTab();
+            this.removeCurrentTab({animate: true});
             aEvent.stopPropagation();
             aEvent.preventDefault();
           }
 #endif
         ]]></body>
       </method>
 
       <property name="userTypedClear"
@@ -2358,16 +2394,20 @@
 
     <handlers>
       <handler event="underflow"><![CDATA[
          if (event.detail == 0)
            return; // Ignore vertical events
 
          var tabs = document.getBindingParent(this);
          tabs.removeAttribute("overflow");
+
+         tabs.tabbrowser._removingTabs.forEach(tabs.tabbrowser._endRemoveTab,
+                                               tabs.tabbrowser);
+
          tabs._positionPinnedTabs();
       ]]></handler>
       <handler event="overflow"><![CDATA[
          if (event.detail == 0)
            return; // Ignore vertical events
 
          var tabs = document.getBindingParent(this);
          tabs.setAttribute("overflow", "true");
@@ -2741,34 +2781,41 @@
       <property name="mAllTabsPopup" readonly="true"
                 onget="return document.getElementById('alltabs-popup');"/>
     </implementation>
 
     <handlers>
       <handler event="TabSelect" action="this._handleTabSelect();"/>
 
       <handler event="transitionend"><![CDATA[
-        if (event.propertyName == "max-width")
-          this._handleNewTab(event.target);
+        if (event.propertyName != "max-width")
+          return;
+
+        var tab = event.target;
+
+        if (tab.getAttribute("fadein") == "true")
+          this._handleNewTab(tab);
+        else if (this.tabbrowser._removingTabs.indexOf(tab) > -1)
+          this.tabbrowser._endRemoveTab(tab);
       ]]></handler>
 
       <handler event="dblclick"><![CDATA[
         // See hack note in the tabbrowser-close-button binding
         if (!this._blockDblClick && event.button == 0 &&
             event.originalTarget.localName == "box")
           BrowserOpenTab();
       ]]></handler>
 
       <handler event="click"><![CDATA[
         if (event.button != 1)
           return;
 
         if (event.target.localName == "tab") {
           if (this.childNodes.length > 1 || !this._closeWindowWithLastTab)
-            this.tabbrowser.removeTab(event.target);
+            this.tabbrowser.removeTab(event.target, {animate: true});
         } else if (event.originalTarget.localName == "box") {
           BrowserOpenTab();
         } else {
           return;
         }
 
         event.stopPropagation();
       ]]></handler>
@@ -3075,17 +3122,17 @@
         if (event.detail > 1 && !this._ignoredClick) {
           this._ignoredClick = true;
           return;
         }
 
         // Reset the "ignored click" flag
         this._ignoredClick = false;
 
-        tabContainer.tabbrowser.removeTab(bindingParent);
+        tabContainer.tabbrowser.removeTab(bindingParent, {animate: true});
         tabContainer._blockDblClick = true;
 
         /* XXXmano hack (see bug 343628):
          * Since we're removing the event target, if the user
          * double-clicks this button, the dblclick event will be dispatched
          * with the tabbar as its event target (and explicit/originalTarget),
          * which treats that as a mouse gesture for opening a new tab.
          * In this context, we're manually blocking the dblclick event
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -94,16 +94,17 @@ endif
                  browser_NetworkPrioritizer.js \
                  browser_allTabsPanel.js \
                  browser_alltabslistener.js \
                  browser_bug304198.js \
                  browser_bug321000.js \
                  title_test.svg \
                  browser_bug329212.js \
                  browser_bug356571.js \
+                 browser_bug380960.js \
                  browser_bug386835.js \
                  browser_bug405137.js \
                  browser_bug406216.js \
                  browser_bug409481.js \
                  browser_bug413915.js \
                  browser_bug416661.js \
                  browser_bug417483.js \
                  browser_bug419612.js \
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_bug380960.js
@@ -0,0 +1,69 @@
+function test() {
+  var tab = gBrowser.addTab("about:blank", { skipAnimation: true });
+  gBrowser.removeTab(tab);
+  is(tab.parentNode, null, "tab removed immediately");
+
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref("browser.tabs.animate", true);
+  nextAsyncText();
+}
+
+function cleanup() {
+  if (Services.prefs.prefHasUserValue("browser.tabs.animate"))
+    Services.prefs.clearUserPref("browser.tabs.animate");
+  finish();
+}
+
+var asyncTests = [
+  function (tab) {
+    info("closing tab with middle click");
+    EventUtils.synthesizeMouse(tab, 2, 2, { button: 1 });
+  },
+  function (tab) {
+    info("closing tab with accel+w");
+    gBrowser.selectedTab = tab;
+    content.focus();
+    EventUtils.synthesizeKey("w", { accelKey: true });
+  },
+  function (tab) {
+    info("closing tab by clicking the tab close button");
+    gBrowser.selectedTab = tab;
+    var button = document.getAnonymousElementByAttribute(tab, "anonid", "close-button");
+    EventUtils.synthesizeMouse(button, 2, 2, {});
+  }
+];
+
+function nextAsyncText() {
+  var tab = gBrowser.addTab("about:blank", { skipAnimation: true });
+
+  var gotCloseEvent = false;
+
+  tab.addEventListener("TabClose", function () {
+    gotCloseEvent = true;
+
+    const DEFAULT_ANIMATION_LENGTH = 250;
+    const MAX_WAIT_TIME = DEFAULT_ANIMATION_LENGTH * 3;
+    const INTERVAL_LENGTH = 100;
+    var polls = Math.ceil(MAX_WAIT_TIME / INTERVAL_LENGTH);
+    var pollTabRemoved = setInterval(function () {
+      --polls;
+      if (tab.parentNode && polls > 0)
+        return;
+      clearInterval(pollTabRemoved);
+
+      is(tab.parentNode, null, "tab removed after at most " + MAX_WAIT_TIME + " ms");
+
+      if (asyncTests.length)
+        nextAsyncText();
+      else
+        cleanup();
+    }, INTERVAL_LENGTH);
+  }, false);
+
+  asyncTests.shift()(tab);
+
+  ok(gotCloseEvent, "got the close event syncronously");
+
+  is(tab.parentNode, gBrowser.tabContainer, "tab still exists when it's about to be removed asynchronously");
+}