Bug 465086 - When closing a tab, other tabs should not resize until cursor leaves tab toolbar. r=gavin ui-r=beltzner
authorFrank Yan <fyan@mozilla.com>
Mon, 11 Apr 2011 23:50:56 -0700
changeset 67993 8a67aa03761e
parent 67992 5c1d236f0d5f
child 67994 aa0b6404ec25
push id19462
push userbzbarsky@mozilla.com
push dateTue, 12 Apr 2011 07:00:54 +0000
treeherdermozilla-central@8a67aa03761e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgavin, beltzner
bugs465086
milestone2.2a1pre
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 465086 - When closing a tab, other tabs should not resize until cursor leaves tab toolbar. r=gavin ui-r=beltzner
browser/base/content/tabbrowser.css
browser/base/content/tabbrowser.xml
browser/themes/winstripe/browser/browser.css
--- a/browser/base/content/tabbrowser.css
+++ b/browser/base/content/tabbrowser.css
@@ -42,8 +42,16 @@ tabpanels {
   position: relative;
   z-index: 2;
 }
 
 .tab-throbber:not([busy]),
 .tab-throbber[busy] + .tab-icon-image {
   display: none;
 }
+
+.closing-tabs-spacer {
+  pointer-events: none;
+}
+
+.tabbrowser-tabs:not(:hover) > .tabbrowser-arrowscrollbox > .closing-tabs-spacer {
+  -moz-transition: width .15s ease-out;
+}
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -28,16 +28,18 @@
    -   Asaf Romano <mozilla.mano@sent.com>
    -   Seth Spitzer <sspitzer@mozilla.org>
    -   Simon Bünzli <zeniko@gmail.com>
    -   Michael Ventnor <ventnor.bugzilla@yahoo.com.au>
    -   Mark Pilgrim <pilgrim@gmail.com>
    -   Dão Gottwald <dao@mozilla.com>
    -   Paul O’Shannessy <paul@oshannessy.com>
    -   Rob Arnold <tellrob@gmail.com>
+   -   Frank Yan <fyan@mozilla.com>
+   -   Patrick Walton <pcwalton@mozilla.com>
    -
    - Alternatively, the contents of this file may be used under the terms of
    - either the GNU General Public License Version 2 or later (the "GPL"), or
    - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
    - in which case the provisions of the GPL or the LGPL are applicable instead
    - of those above. If you wish to allow use of your version of this file only
    - under the terms of either the GPL or the LGPL, and not to allow others to
    - use your version of this file under the terms of the MPL, indicate your
@@ -229,16 +231,17 @@
           if (aTab.pinned)
             return;
 
           if (aTab.hidden)
             this.showTab(aTab);
 
           this.moveTabTo(aTab, this._numPinnedTabs);
           aTab.setAttribute("pinned", "true");
+          this.tabContainer._unlockTabSizing();
           this.tabContainer._positionPinnedTabs();
           this.tabContainer.adjustTabstrip();
 
           this.getBrowserForTab(aTab).docShell.isAppTab = true;
 
           if (aTab.selected)
             this._setCloseKeyState(false);
 
@@ -253,16 +256,17 @@
         <body><![CDATA[
           if (!aTab.pinned)
             return;
 
           this.moveTabTo(aTab, this._numPinnedTabs - 1);
           aTab.setAttribute("fadein", "true");
           aTab.removeAttribute("pinned");
           aTab.style.MozMarginStart = "";
+          this.tabContainer._unlockTabSizing();
           this.tabContainer._positionPinnedTabs();
           this.tabContainer.adjustTabstrip();
 
           this.getBrowserForTab(aTab).docShell.isAppTab = false;
 
           if (aTab.selected)
             this._setCloseKeyState(true);
 
@@ -1199,16 +1203,18 @@
             else
               t.setAttribute("label", aURI);
 
             t.setAttribute("crop", "end");
             t.setAttribute("validate", "never");
             t.setAttribute("onerror", "this.removeAttribute('image');");
             t.className = "tabbrowser-tab";
 
+            this.tabContainer._unlockTabSizing();
+
             // 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" ||
                 !Services.prefs.getBoolPref("browser.tabs.animate")) {
               t.setAttribute("fadein", "true");
               setTimeout(function (tabContainer) {
@@ -1430,44 +1436,52 @@
         []
       </field>
 
       <method name="removeTab">
         <parameter name="aTab"/>
         <parameter name="aParams"/>
         <body>
           <![CDATA[
-            if (aParams)
+            if (aParams) {
               var animate = aParams.animate;
+              var byMouse = aParams.byMouse;
+            }
 
             // Handle requests for synchronously removing an already
             // asynchronously closing tab.
             if (!animate &&
                 this._removingTabs.indexOf(aTab) > -1) {
               this._endRemoveTab(aTab);
               return;
             }
 
             var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
 
             if (!this._beginRemoveTab(aTab, false, null, true))
               return;
 
+            if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse)
+              this.tabContainer._lockTabSizing(aTab);
+            else
+              this.tabContainer._unlockTabSizing();
+
             if (!animate /* the caller didn't opt in */ ||
                 isLastTab ||
                 aTab.pinned ||
                 this._removingTabs.length > 3 /* don't want lots of concurrent animations */ ||
                 aTab.getAttribute("fadein") != "true" /* fade-in transition hasn't been triggered yet */ ||
                 window.getComputedStyle(aTab).maxWidth == "0.1px" /* fade-in transition hasn't moved yet */ ||
                 !Services.prefs.getBoolPref("browser.tabs.animate")) {
               this._endRemoveTab(aTab);
               return;
             }
 
             this._blurTab(aTab);
+            aTab.style.maxWidth = ""; // ensure that fade-out transition happens
             aTab.removeAttribute("fadein");
 
             setTimeout(function (tab, tabbrowser) {
               if (tab.parentNode &&
                   window.getComputedStyle(tab).maxWidth == "0.1px") {
                 NS_ASSERT(false, "Giving up waiting for the tab closing animation to finish (bug 608589)");
                 tabbrowser._endRemoveTab(tab);
               }
@@ -1632,16 +1646,20 @@
               this.tabs[i]._tPos = i;
 
             if (!this._windowIsClosing) {
               if (wasPinned)
                 this.tabContainer._positionPinnedTabs();
 
               // update tab close buttons state
               this.tabContainer.adjustTabstrip();
+
+              setTimeout(function(tabs) {
+                tabs._lastTabClosedByMouse = false;
+              }, 0, this.tabContainer);
             }
 
             // update first-tab/last-tab/beforeselected/afterselected attributes
             this.selectedTab._selected = true;
 
             // Removing the panel requires fixing up selectedPanel immediately
             // (see below), which would be hindered by the potentially expensive
             // browser removal. So we remove the browser and the panel in two
@@ -2407,16 +2425,17 @@
           this.mCurrentBrowser = this.mPanelContainer.childNodes[0].firstChild.firstChild;
           this.mCurrentTab = this.tabContainer.firstChild;
           document.addEventListener("keypress", this, false);
 
           var uniqueId = "panel" + Math.floor(Date.now());
           this.mPanelContainer.childNodes[0].id = uniqueId;
           this.mCurrentTab.linkedPanel = uniqueId;
           this.mCurrentTab._tPos = 0;
+          this.mCurrentTab._fullyOpen = true;
           this.mCurrentTab.linkedBrowser = this.mCurrentBrowser;
 
           // set up the shared autoscroll popup
           this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup();
           this._autoScrollPopup.id = "autoscroller";
           this.appendChild(this._autoScrollPopup);
           this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
           this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink;
@@ -2586,35 +2605,38 @@
         <parameter name="tab"/>
         <body><![CDATA[
           return !tab.pinned && !tab.hidden;
         ]]></body>
       </method>
     </implementation>
 
     <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.removeTab,
-                                               tabs.tabbrowser);
-
-         tabs._positionPinnedTabs();
+      <handler event="underflow" phase="capturing"><![CDATA[
+        if (event.detail == 0)
+          return; // Ignore vertical events
+
+        var tabs = document.getBindingParent(this);
+        tabs.removeAttribute("overflow");
+
+        if (tabs._lastTabClosedByMouse)
+          tabs._expandSpacerBy(this._scrollButtonDown.clientWidth);
+
+        tabs.tabbrowser._removingTabs.forEach(tabs.tabbrowser.removeTab,
+                                              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");
-         tabs._positionPinnedTabs();
+        if (event.detail == 0)
+          return; // Ignore vertical events
+
+        var tabs = document.getBindingParent(this);
+        tabs.setAttribute("overflow", "true");
+        tabs._positionPinnedTabs();
       ]]></handler>
     </handlers>
   </binding>
 
   <binding id="tabbrowser-tabs"
            extends="chrome://global/content/bindings/tabbox.xml#tabs">
     <resources>
       <stylesheet src="chrome://browser/content/tabbrowser.css"/>
@@ -2635,16 +2657,18 @@
         <children includes="tab"/>
 # This is to ensure anything extensions put here will go before the newtab
 # button, necessary due to the previous hack.
         <children/>
         <xul:toolbarbutton class="tabs-newtab-button"
                            command="cmd_newNavigatorTab"
                            onclick="checkForMiddleClick(this, event);"
                            tooltiptext="&newTabButton.tooltip;"/>
+        <xul:spacer class="closing-tabs-spacer" anonid="closing-tabs-spacer"
+                    style="width: 0;"/>
       </xul:arrowscrollbox>
     </content>
 
     <implementation implements="nsIDOMEventListener">
       <constructor>
         <![CDATA[
           this.mTabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth");
           this.mCloseButtons = Services.prefs.getIntPref("browser.tabs.closeButtons");
@@ -2806,16 +2830,114 @@
             var tabStrip = this.mTabstrip;
             if (tabStrip.scrollPosition + tabStrip.scrollClientSize >
                 tabStrip.scrollSize)
               tabStrip.scrollByPixels(-1);
           } catch (e) {}
         ]]></body>
       </method>
 
+      <field name="_closingTabsSpacer">
+        document.getAnonymousElementByAttribute(this, "anonid", "closing-tabs-spacer");
+      </field>
+
+      <field name="_tabDefaultMaxWidth">NaN</field>
+      <field name="_lastTabClosedByMouse">false</field>
+      <field name="_hasTabTempMaxWidth">false</field>
+      <field name="_usingClosingTabsSpacer">false</field>
+
+      <!-- Try to keep the active tab's close button under the mouse cursor -->
+      <method name="_lockTabSizing">
+        <parameter name="aTab"/>
+        <body><![CDATA[
+          var tabs = this.tabbrowser.visibleTabs;
+          if (!tabs.length)
+            return;
+
+          var isEndTab = (aTab._tPos > tabs[tabs.length-1]._tPos);
+          var tabWidth = aTab.getBoundingClientRect().width;
+
+          if (!this._tabDefaultMaxWidth)
+            this._tabDefaultMaxWidth =
+              parseFloat(window.getComputedStyle(aTab).maxWidth);
+          this._lastTabClosedByMouse = true;
+
+          if (this.getAttribute("overflow") == "true") {
+            // Don't need to do anything if we're in overflow mode and aren't scrolled
+            // all the way to the right, or if we're closing the last tab.
+            if (isEndTab || !this.mTabstrip._scrollButtonDown.disabled)
+              return;
+
+            // If the tab has an owner that will become the active tab, the owner will
+            // be to the left of it, so we actually want the left tab to slide over.
+            // This can't be done as easily in non-overflow mode, so we don't bother.
+            if (aTab.owner)
+              return;
+
+            this._expandSpacerBy(tabWidth);
+          } else { // non-overflow mode
+            // Locking is neither in effect nor needed, so let tabs expand normally.
+            if (isEndTab && !this._hasTabTempMaxWidth)
+              return;
+
+            let numPinned = this.tabbrowser._numPinnedTabs;
+            // Force tabs to stay the same width, unless we're closing the last tab,
+            // which case we need to let them expand just enough so that the overall
+            // tabbar width is the same.
+            if (isEndTab) {
+              let numNormalTabs = tabs.length - numPinned;
+              tabWidth = tabWidth * (numNormalTabs + 1) / numNormalTabs;
+              if (tabWidth > this._tabDefaultMaxWidth)
+                tabWidth = this._tabDefaultMaxWidth;
+            }
+            tabWidth += "px";
+            for (let i = numPinned; i < tabs.length; i++) {
+              let tab = tabs[i];
+              tab.style.maxWidth = tabWidth;
+              if (!isEndTab) { // keep tabs the same width
+                tab.style.MozTransition = "none";
+                tab.clientTop; // flush styles to skip animation; see bug 649247
+                tab.style.MozTransition = "";
+              }
+            }
+            this._hasTabTempMaxWidth = true;
+            this.tabbrowser.addEventListener("mousemove", this, false);
+            window.addEventListener("mouseout", this, false);
+          }
+        ]]></body>
+      </method>
+
+      <method name="_expandSpacerBy">
+        <parameter name="pixels"/>
+        <body><![CDATA[
+          let spacer = this._closingTabsSpacer;
+          spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
+          this._usingClosingTabsSpacer = true;
+          this.tabbrowser.addEventListener("mousemove", this, false);
+          window.addEventListener("mouseout", this, false);
+        ]]></body>
+      </method>
+
+      <method name="_unlockTabSizing">
+        <body><![CDATA[
+          this.tabbrowser.removeEventListener("mousemove", this, false);
+          window.removeEventListener("mouseout", this, false);
+          if (this._hasTabTempMaxWidth) {
+            this._hasTabTempMaxWidth = false;
+            let tabs = this.tabbrowser.visibleTabs;
+            for (let i = 0; i < tabs.length; i++)
+              tabs[i].style.maxWidth = "";
+          }
+          if (this._usingClosingTabsSpacer) {
+            this._usingClosingTabsSpacer = false;
+            this._closingTabsSpacer.style.width = 0;
+          }
+        ]]></body>
+      </method>
+
       <method name="_positionPinnedTabs">
         <body><![CDATA[
           var numPinned = this.tabbrowser._numPinnedTabs;
           var doPosition = this.getAttribute("overflow") == "true" &&
                            numPinned > 0 &&
                            numPinned < this.tabbrowser.visibleTabs.length;
 
           if (doPosition) {
@@ -2859,16 +2981,26 @@
               if (width != this.mTabstripWidth) {
                 this.adjustTabstrip();
                 this._fillTrailingGap();
                 this._handleTabSelect();
                 this.mTabstripWidth = width;
               }
               this.tabbrowser.updateWindowResizers();
               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;
+            case "mousemove":
+              if (document.getElementById("tabContextMenu").state != "open")
+                this._unlockTabSizing();
+              break;
           }
         ]]></body>
       </method>
 
       <field name="_animateElement">
         this.mTabstrip._scrollButtonDown;
       </field>
 
@@ -2994,18 +3126,19 @@
               event.screenY <= t.boxObject.screenY + t.boxObject.height)
             this.mTabstrip.ensureElementIsVisible(t);
         ]]></body>
       </method>
 
       <method name="_handleNewTab">
         <parameter name="tab"/>
         <body><![CDATA[
-          if (tab.parentNode != this)
+          if (tab.parentNode != this || tab._fullyOpen)
             return;
+          tab._fullyOpen = true;
 
           this.adjustTabstrip();
 
           if (tab.getAttribute("selected") == "true") {
             this._fillTrailingGap();
             this._handleTabSelect();
           } else {
             this._notifyBackgroundTab(tab);
@@ -3014,17 +3147,17 @@
           // XXXmano: this is a temporary workaround for bug 345399
           // We need to manually update the scroll buttons disabled state
           // if a tab was inserted to the overflow area or removed from it
           // without any scrolling and when the tabbar has already
           // overflowed.
           this.mTabstrip._updateScrollButtonsDisabledState();
         ]]></body>
       </method>
-      
+
       <method name="_canAdvanceToTab">
         <parameter name="aTab"/>
         <body>
         <![CDATA[
           return this.tabbrowser._removingTabs.indexOf(aTab) == -1;
         ]]>
         </body>
       </method>
@@ -3068,17 +3201,17 @@
       ]]></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, {animate: true});
+            this.tabbrowser.removeTab(event.target, {animate: true, byMouse: true});
         } else if (event.originalTarget.localName == "box") {
           BrowserOpenTab();
         } else {
           return;
         }
 
         event.stopPropagation();
       ]]></handler>
@@ -3394,17 +3527,17 @@
         if (event.detail > 1 && !this._ignoredClick) {
           this._ignoredClick = true;
           return;
         }
 
         // Reset the "ignored click" flag
         this._ignoredClick = false;
 
-        tabContainer.tabbrowser.removeTab(bindingParent, {animate: true});
+        tabContainer.tabbrowser.removeTab(bindingParent, {animate: true, byMouse: 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
@@ -3482,16 +3615,17 @@
       <property name="hidden" readonly="true">
         <getter>
           return this.getAttribute("hidden") == "true";
         </getter>
       </property>
 
       <field name="mOverCloseButton">false</field>
       <field name="mCorrespondingMenuitem">null</field>
+      <field name="_fullyOpen">false</field>
     </implementation>
 
     <handlers>
       <handler event="mouseover">
         var anonid = event.originalTarget.getAttribute("anonid");
         if (anonid == "close-button")
           this.mOverCloseButton = true;
       </handler>
--- a/browser/themes/winstripe/browser/browser.css
+++ b/browser/themes/winstripe/browser/browser.css
@@ -1759,17 +1759,21 @@ richlistitem[type~="action"][actiontype=
 .tabs-newtab-button,
 #TabsToolbar > #new-tab-button,
 #TabsToolbar > toolbarpaletteitem > #new-tab-button {
   list-style-image: url(chrome://browser/skin/tabbrowser/newtab.png);
   -moz-image-region: rect(0, 16px, 18px, 0);
 }
 
 .tabs-newtab-button {
-  width: 30px;
+  width: 28px;
+}
+
+#TabsToolbar > #new-tab-button {
+  width: 26px;
 }
 
 .tabs-newtab-button:hover:active,
 #TabsToolbar > #new-tab-button:hover:active {
   -moz-image-region: rect(0, 32px, 18px, 16px);
 }
 
 #alltabs-button {