Bug 612577 - Implement tab animations for Thunderbird.
☠☠ backed out by d4c74466ca81 ☠ ☠
authorJosiah Bruner <josiah@programmer.net>
Sun, 29 Sep 2013 19:06:03 -0400
changeset 16868 49cccc8fa53c4c415aba5b85af0c1c2a17369441
parent 16867 ab678895a5a5d4c8e0f30408815e648e99cf4baa
child 16869 d4c74466ca8146d18b19d4cceb0a03d0891dfab8
push id1074
push userbugzilla@standard8.plus.com
push dateMon, 03 Feb 2014 22:47:23 +0000
treeherdercomm-beta@6b791b5369ed [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs612577
Bug 612577 - Implement tab animations for Thunderbird.
mail/app/profile/all-thunderbird.js
mail/base/content/tabmail.css
mail/base/content/tabmail.xml
--- a/mail/app/profile/all-thunderbird.js
+++ b/mail/app/profile/all-thunderbird.js
@@ -474,16 +474,19 @@ pref("mail.tabs.closeWindowWithLastTab",
 // 1 - all tabs until tabClipWidth is reached, then active tab only
 // 2 - no close buttons
 // 3 - at the end of the tabstrip
 pref("mail.tabs.closeButtons", 1);
 
 // Allow the tabs to be in the titlebar on supported systems
 pref("mail.tabs.drawInTitlebar", true);
 
+// Allows the tabs to animate on open/close
+pref("mail.tabs.animate", true);
+
 // The breakpad report server to link to in about:crashes
 pref("breakpad.reportURL", "http://crash-stats.mozilla.com/report/index/");
 
 // OS Integrated Search and Indexing
 #ifdef XP_WIN
 pref("mail.winsearch.enable", false);
 pref("mail.winsearch.firstRunDone", false);
 #else
--- a/mail/base/content/tabmail.css
+++ b/mail/base/content/tabmail.css
@@ -3,16 +3,30 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 .tabmail-tabs {
   -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail-tabs");
 }
 
 .tabmail-tab {
   -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail-tab");
+  transition: min-width 200ms ease-out, max-width 230ms ease-out, opacity 50ms ease-out;
+  transition-delay: 0ms, 0ms, 20ms;
+  opacity: 1;
+}
+
+.tabmail-tab[fadein="false"] {
+  transition: none !important;
+}
+
+.tabmail-tab:not([fadein]) {
+  opacity: 0 !important;
+  max-width: 1px !important;
+  min-width: 1px !important;
+  height: 0px !important;
 }
 
 .tabmail-tabbox {
   -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail-tabbox");
 }
 
 .tabmail-arrowscrollbox {
   -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail-arrowscrollbox");
--- a/mail/base/content/tabmail.xml
+++ b/mail/base/content/tabmail.xml
@@ -46,22 +46,26 @@
     -     there are also a few common ones that all should obey, including:
     -
     -     * "background": if this is true, the tab will be loaded in the
     -       background.
     -     * "disregardOpener": if this is true, then the tab opener will not
     -       be switched to automatically by tabmail if the new tab is immediately
     -       closed.
     -
-    - * closeTab(aOptionalTabIndexInfoOrTabNode,aNoUndo):
+    - * closeTab(aOptionalTabIndexInfoOrTabNode, aNoUndo, aNoAnimate):
+    -     Hides the tab and then sends finishCloseTab() using the same
+    -     parameters. closeTab() must be called before finishCloseTab().
     -     If no argument is provided, the current tab is closed. The first
     -     argument specifies a specific tab to be closed. It can be a tab index,
     -     a tab info object, or a tab's DOM element. In case the second
     -     argument is true, the closed tab can't be restored by calling
     -     undoCloseTab().
+    -     The third argument is optional, and should be set to true to skip
+    -     animating the tab closing. This argument defaults to false.
     -     Please note, some tabs cannot be closed. Trying to close such tab,
     -     will fail silently.
     - * undoCloseTab():
     -     Restores the most recent tab closed by the user.
     - * switchToTab(aTabIndexInfoOrTabNode):
     -     Switch to the tab by providing a tab index, tab info object, or tab
     -     node (tabmail-tab bound element.) Instead of calling this method,
     -     you can also just poke at tabmail.tabContainer and its selectedIndex
@@ -156,26 +160,28 @@
     -     displayed.
     - * shouldSwitchTo(aArgs): Optional function. Called when openTab is called
     -     on the top-level tabmail binding. It is used to decide if the openTab
     -     function should switch to an existing tab or actually open a new tab.
     -     If the openTab function should switch to an existing tab, return the
     -     index of that tab; otherwise return -1.
     -     aArgs is a set of named parameters (the ones that are later passed to
     -     openTab).
-    - * openTab(aTab, aArgs): Called when a tab of the given mode is in the
+    - * openTab(aTab, aArgs, aNoAnimate): Called when a tab of the given mode is in the
     -     process of being opened.  aTab will have its "mode" attribute
     -     set to the mode definition of the tab mode being opened.  You should
     -     set the "title" attribute on it, and may set any other attributes
     -     you wish for your own use in subsequent functions.  Note that 'this'
     -     points to the tab type definition, not the mode definition as you
     -     might expect.  This allows you to place common logic code on the
     -     tab type for use by multiple modes and to defer to it.  Any arguments
     -     provided to the caller of tabmail.openTab will be passed to your
     -     function as well, including background.
+    -     The third argument is optional, and should be set to true to skip animating
+    -     the tab opening. This argument defaults to false.
     - * closeTab(aTab): Called when aTab is being closed.  The tab need not be
     -     currently displayed.  You are responsible for properly cleaning up
     -     any state you preserved in aTab.
     - * saveTabState(aTab): Called when aTab is being switched away from so that
     -     you can preserve its state on aTab.  This is primarily for single
     -     tab panel implementations; you may not have much state to save if your
     -     tab has its own tab panel.
     - * showTab(aTab): Called when aTab is being displayed and you should
@@ -433,32 +439,42 @@
           //  contents will set themselves up correctly.
           if (this.tabInfo.length == 0) {
             let firstTab = {mode: this.defaultTabMode, busy: false,
                             canClose: false, thinking: false, _ext: {}};
             firstTab.mode.tabs.push(firstTab);
 
             this.tabInfo[0] = this.currentTabInfo = firstTab;
 
+            let [iTab, tab, tabNode] =
+              this._getTabContextForTabbyThing(firstTab, true);
+
             let tabOpenFirstFunc = firstTab.mode.openFirstTab ||
                                    firstTab.mode.tabType.openFirstTab;
             tabOpenFirstFunc.call(firstTab.mode.tabType, firstTab);
             this.setTabTitle(null);
 
+            tabNode.setAttribute("fadein", "true");
+            tabNode.style.maxWidth = this.tabContainer.mTabMaxWidth + "px";
+            tabNode.style.minWidth = this.tabContainer.mTabMinWidth + "px";
+            tabNode.removeAttribute("maxwidth");
+            tabNode.removeAttribute("minwidth");
+
             for each (let [i, tabMonitor] in Iterator(this.tabMonitors)) {
               if ("onTabOpened" in tabMonitor)
                 tabMonitor.onTabOpened(firstTab, true);
               tabMonitor.onTabSwitched(firstTab, null);
             }
           }
         ]]></body>
       </method>
       <method name="openTab">
         <parameter name="aTabModeName"/>
         <parameter name="aArgs"/>
+        <parameter name="aNoAnimate"/>
         <body><![CDATA[
         try {
           if (!(aTabModeName in this.tabModes))
             throw new Error("No such tab mode: " + aTabModeName);
           let tabMode = this.tabModes[aTabModeName];
           // if we are already at our limit for this mode, show an existing one
           if (tabMode.tabs.length == tabMode.maxTabs) {
             let desiredTab = tabMode.tabs[0];
@@ -492,18 +508,16 @@
                      thinking: false, _ext: {}};
           tabMode.tabs.push(tab);
 
           var t = document.createElementNS(
             "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
             "tab");
           tab.tabNode = t;
           t.setAttribute("crop", "end");
-          t.maxWidth = this.tabContainer.mTabMaxWidth;
-          t.minWidth = this.tabContainer.mTabMinWidth;
           t.width = 0;
           t.setAttribute("flex", "100");
           t.setAttribute("validate", "never");
           t.className = "tabmail-tab";
           this.tabContainer.appendChild(t);
           if (this.tabContainer.mCollapseToolbar.collapsed) {
             this.tabContainer.mCollapseToolbar.collapsed = false;
             this.tabContainer.adjustTabstrip();
@@ -567,16 +581,35 @@
           }
 
           // clear _mostRecentTabInfo; we only needed it during the call to
           //  openTab.
           this._mostRecentTabInfo = null;
 
           t.setAttribute("label", tab.title);
 
+          if (aNoAnimate || !Services.prefs.getBoolPref("mail.tabs.animate")) {
+            t.setAttribute("fadein", "false");
+            this.tabContainer.adjustTabstrip();
+          } else {
+            t.setAttribute("fadein", "true");
+
+            // If the tabs animate then they will appear too small when adjustTabstrip()
+            // is called normally. Therefore we must wait until the animation is finished
+            // and then adjustTabstrip() again.
+
+            t.addEventListener("transitionend", function tabOpenTransitionFinished() {
+              this.tabContainer.adjustTabstrip();
+              t.removeEventListener("transitionend", tabOpenTransitionFinished);
+            }.bind(this), false);
+          }
+
+          t.style.maxWidth = this.tabContainer.mTabMaxWidth + "px";
+          t.style.minWidth = this.tabContainer.mTabMinWidth + "px";
+
           if (!background)
             this.setDocumentTitle(tab);
 
           // for styling purposes, apply the type to the tab...
           t.setAttribute('type', tab.mode.type);
 
           if (!background)
             // Update the toolbar status - we don't need to do menus as they
@@ -676,36 +709,93 @@
 
           this.switchToTab(tab);
 
         ]]></body>
       </method>
       <method name="closeTab">
         <parameter name="aOptTabIndexNodeOrInfo"/>
         <parameter name="aNoUndo" />
+        <parameter name="aNoAnimate" />
         <body><![CDATA[
 
             let [iTab, tab, tabNode] =
               this._getTabContextForTabbyThing(aOptTabIndexNodeOrInfo, true);
 
-            if (!tab.canClose)
+            if (!tab.canClose) {
               return;
-
+            }
+
+            // First we get the index of the tab being closed and the index of
+            // the currently selected tab. If they match:
+            //   If we have a previously opened tab to go back to, and it's index
+            //   isn't -1, move to that location.
+            //   Otherwise just move to the next lower tab in the index.
+
+            let tabIndex = Array.indexOf(this.tabContainer.childNodes, tabNode);
+            let selectedIndex = this.tabContainer.selectedIndex;
+
+            if (selectedIndex == tabIndex) {
+              let lastTabOpenerIndex = this.tabInfo.indexOf(this.mLastTabOpener);
+              if (this.mLastTabOpener && (lastTabOpenerIndex != -1)) {
+                this.tabContainer.selectedIndex = this.tabInfo.indexOf(this.mLastTabOpener);
+              } else {
+                if (selectedIndex > 0) {
+                  this.tabContainer.selectedIndex = selectedIndex - 1;
+                }
+              }
+            }
+
+            // Don't animate the last tab for aesthetic purposes.
+            if (tabIndex == (this.tabContainer.childNodes.length - 1)) {
+              aNoAnimate == true;
+            }
+
+            if (aNoAnimate || !Services.prefs.getBoolPref("mail.tabs.animate")) {
+              tabNode.setAttribute("fadein", "false");
+              this._finishCloseTab(aOptTabIndexNodeOrInfo, aNoUndo);
+            } else {
+              tabNode.removeAttribute("fadein");
+              let self = this;
+              tabNode.addEventListener("transitionend", function tabCloseTransitionEnded(event) {
+                if (event.propertyName === "max-width") {
+                  tabNode.removeEventListener("transitionend", tabCloseTransitionEnded);
+                  self._finishCloseTab(aOptTabIndexNodeOrInfo, aNoUndo);
+                }
+              }.bind(this), false);
+            }
+          ]]>
+        </body>
+      </method>
+      <method name="_finishCloseTab">
+        <parameter name="aOptTabIndexNodeOrInfo"/>
+        <parameter name="aNoUndo" />
+        <body>
+          <![CDATA[
+            /*  _finishCloseTab(aOptionalTabIndexInfoOrTabNode, aNoUndo):
+            -     Finishes removing the tab when called from closeTab().
+            -     Note: Do NOT call this when trying to close a tab, use closeTab()
+            -     The arguments here are the same as closeTab(), refer to that method
+            -     for more details on the parameters purpose. */
+
+            let [iTab, tab, tabNode] = this._getTabContextForTabbyThing(aOptTabIndexNodeOrInfo, true);
             // Give the tab type a chance to make its own decisions about
             // whether its tabs can be closed or not. For instance, contentTabs
             // and chromeTabs run onbeforeunload event handlers that may
             // exercise their right to prompt the user for confirmation before
             // closing.
             let tryCloseFunc = tab.mode.tryCloseTab || tab.mode.tabType.tryCloseTab;
-            if (tryCloseFunc && !tryCloseFunc.call(tab.mode.tabType, tab))
+            if (tryCloseFunc && !tryCloseFunc.call(tab.mode.tabType, tab)) {
               return;
+            }
 
             for each (let [i, tabMonitor] in Iterator(this.tabMonitors)) {
-              if ("onTabClosing" in tabMonitor)
+              if ("onTabClosing" in tabMonitor) {
                 tabMonitor.onTabClosing(tab);
+              }
             }
 
             if (!aNoUndo) {
               // Allow user to undo accidentially closed tabs
               let session = this.persistTab(tab);
 
               if (session) {
                 this.recentlyClosedTabs.unshift(
@@ -718,46 +808,36 @@
 
             let closeFunc = tab.mode.closeTab || tab.mode.tabType.closeTab;
             closeFunc.call(tab.mode.tabType, tab);
 
             this.tabInfo.splice(iTab, 1);
             tab.mode.tabs.splice(tab.mode.tabs.indexOf(tab), 1);
             this.tabContainer.removeChild(tabNode);
 
-            if (this.tabContainer.selectedIndex == -1) {
-              let lastTabOpenerIndex = this.tabInfo.indexOf(this.mLastTabOpener);
-
-              if (this.mLastTabOpener && (lastTabOpenerIndex != -1)) {
-                this.tabContainer.selectedIndex = this.tabInfo.indexOf(this.mLastTabOpener);
-              } else {
-                this.tabContainer.selectedIndex =
-                  (iTab == this.tabContainer.childNodes.length) ? iTab - 1 : iTab;
-              }
-
-            }
-
             // Clear the last tab opener - we don't need this anymore.
             this.mLastTabOpener = null;
 
-            if (this.currentTabInfo == tab)
+            if (this.currentTabInfo == tab) {
               this.updateCurrentTab();
+            }
 
             if (tab.panel) {
               this.panelContainer.removeChild(tab.panel);
               delete tab.panel;
 
               // Ensure current tab is still selecte and displayed in the
               // panelContainer.
               this.panelContainer.selectedPanel =
                 this.currentTabInfo.panel || this.currentTabInfo.mode.tabType.panel;
             }
             if (this.tabContainer.childNodes.length == 1 &&
-                this.tabContainer.mAutoHide)
+                this.tabContainer.mAutoHide) {
               this.tabContainer.mCollapseToolbar.collapsed = true;
+            }
           ]]>
         </body>
       </method>
       <method name="removeTabByNode">
         <parameter name="aTabNode"/>
         <body>
           <![CDATA[
             this.closeTab(aTabNode);
@@ -1683,18 +1763,16 @@
           try {
             this.mAutoHide = Services.prefs.getBoolPref("mail.tabs.autoHide");
           } catch (e) {
           }
 
           if (this.mAutoHide)
             this.mCollapseToolbar.collapsed = true;
 
-          this.firstChild.minWidth = this.mTabMinWidth;
-          this.firstChild.maxWidth = this.mTabMaxWidth;
           this.adjustTabstrip();
 
           Services.prefs.addObserver("mail.tabs.", this._prefObserver, false);
 
           window.addEventListener("resize", this, false);
 
           // Listen to overflow/underflow events on the tabstrip,
           // we cannot put these as xbl handlers on the entire binding because