Bug 1482472 - Add animation when grouping tabs on drag start for multi-selected tabs. r=jaws
authorAbdoulaye O. Ly <ablayelyfondou@gmail.com>
Tue, 02 Oct 2018 19:15:11 +0000
changeset 439280 de430c23ab39f62d960537ad7ce6f7c5e0aa4185
parent 439279 4ac7cfafd5d125ed411a114270f60277c0a14606
child 439281 b2a709a740cf5851f7491862e7509129be87f567
push id34760
push userdvarga@mozilla.com
push dateWed, 03 Oct 2018 04:19:01 +0000
treeherdermozilla-central@9e0a27bf253e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1482472
milestone64.0a1
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 1482472 - Add animation when grouping tabs on drag start for multi-selected tabs. r=jaws Differential Revision: https://phabricator.services.mozilla.com/D7239
browser/base/content/browser.css
browser/base/content/tabbrowser.xml
browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -211,21 +211,26 @@ panelview[mainview] > .panel-header {
 
 #tabbrowser-tabs[movingtab] > .tabbrowser-tab[selected],
 #tabbrowser-tabs[movingtab] > .tabbrowser-tab[multiselected] {
   position: relative;
   z-index: 2;
   pointer-events: none; /* avoid blocking dragover events on scroll buttons */
 }
 
+.tabbrowser-tab[tab-grouping],
 .tabbrowser-tab[tabdrop-samewindow],
 #tabbrowser-tabs[movingtab] > .tabbrowser-tab[fadein]:not([selected]):not([multiselected]) {
   transition: transform 200ms var(--animation-easing-function);
 }
 
+.tabbrowser-tab[tab-grouping][multiselected]:not([selected]) {
+  z-index: 2;
+}
+
 /* The next 3 rules allow dragging tabs slightly outside of the tabstrip
  * to make it easier to drag tabs. */
 #TabsToolbar[movingtab] {
   padding-bottom: 15px;
 }
 
 #TabsToolbar[movingtab] > #tabbrowser-tabs {
   padding-bottom: 15px;
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -581,16 +581,17 @@
           }
         ]]></body>
       </method>
 
       <method name="_animateTabMove">
         <parameter name="event"/>
         <body><![CDATA[
           let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+          let movingTabs = draggedTab._dragData.movingTabs;
 
           if (this.getAttribute("movingtab") != "true") {
             this.setAttribute("movingtab", "true");
             this.parentNode.setAttribute("movingtab", "true");
             if (!draggedTab.multiselected)
               this.selectedItem = draggedTab;
           }
 
@@ -607,17 +608,17 @@
           draggedTab._dragData.animLastScreenX = screenX;
 
           let rtl = (window.getComputedStyle(this).direction == "rtl");
           let pinned = draggedTab.pinned;
           let numPinned = gBrowser._numPinnedTabs;
           let tabs = this._getVisibleTabs()
                          .slice(pinned ? 0 : numPinned,
                                 pinned ? numPinned : undefined);
-          let movingTabs = draggedTab._dragData.movingTabs;
+
           if (rtl) {
             tabs.reverse();
             // Copy moving tabs array to avoid infinite reversing.
             movingTabs = [...movingTabs].reverse();
           }
           let tabWidth = draggedTab.getBoundingClientRect().width;
           let shiftWidth = tabWidth * movingTabs.length;
           draggedTab._dragData.tabWidth = tabWidth;
@@ -717,16 +718,190 @@
 
           this.removeAttribute("movingtab");
           this.parentNode.removeAttribute("movingtab");
 
           this._handleTabSelect();
         ]]></body>
       </method>
 
+      <!--  Regroup all selected tabs around the
+            tab in param  -->
+      <method name="_groupSelectedTabs">
+        <parameter name="tab"/>
+        <body><![CDATA[
+          let draggedTabPos = tab._tPos;
+          let selectedTabs = gBrowser.selectedTabs;
+          let animate = gBrowser.animationsEnabled;
+
+          tab.groupingTabsData = {
+            finished: !animate,
+          };
+
+
+          // Animate left selected tabs
+
+          let insertAtPos = draggedTabPos - 1;
+          for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
+            let movingTab = selectedTabs[i];
+            insertAtPos = newIndex(movingTab, insertAtPos);
+
+            if (animate) {
+              movingTab.groupingTabsData = {};
+              addAnimationData(movingTab, insertAtPos, "left");
+            } else {
+              gBrowser.moveTabTo(movingTab, insertAtPos);
+            }
+            insertAtPos--;
+          }
+
+          // Animate right selected tabs
+
+          insertAtPos = draggedTabPos + 1;
+          for (let i = selectedTabs.indexOf(tab) + 1; i < selectedTabs.length; i++) {
+            let movingTab = selectedTabs[i];
+            insertAtPos = newIndex(movingTab, insertAtPos);
+
+            if (animate) {
+              movingTab.groupingTabsData = {};
+              addAnimationData(movingTab, insertAtPos, "right");
+            } else {
+              gBrowser.moveTabTo(movingTab, insertAtPos);
+            }
+            insertAtPos++;
+          }
+
+          // Slide the relevant tabs to their new position.
+          let rtl = Services.locale.isAppLocaleRTL ? -1 : 1;
+          for (let t of this._getVisibleTabs()) {
+            if (t.groupingTabsData && t.groupingTabsData.translateX) {
+              let translateX = rtl * t.groupingTabsData.translateX;
+              t.style.transform = "translateX(" + translateX + "px)";
+            }
+          }
+
+          function newIndex(aTab, index) {
+            // Don't allow mixing pinned and unpinned tabs.
+            if (aTab.pinned) {
+              return Math.min(index, gBrowser._numPinnedTabs - 1);
+            }
+            return Math.max(index, gBrowser._numPinnedTabs);
+          }
+
+          function addAnimationData(movingTab, movingTabNewIndex, side) {
+            let movingTabOldIndex = movingTab._tPos;
+
+            if (movingTabOldIndex == movingTabNewIndex) {
+              // movingTab is already at the right position
+              // and thus don't need to be animated.
+              return;
+            }
+
+            let movingTabWidth = movingTab.boxObject.width;
+            let shift = (movingTabNewIndex - movingTabOldIndex) * movingTabWidth;
+
+            movingTab.groupingTabsData.animate = true;
+            movingTab.setAttribute("tab-grouping", "true");
+
+            movingTab.groupingTabsData.translateX = shift;
+
+            let onTransitionEnd = transitionendEvent => {
+              if (transitionendEvent.propertyName != "transform" ||
+                  transitionendEvent.originalTarget != movingTab) {
+                return;
+              }
+              movingTab.removeEventListener("transitionend", onTransitionEnd);
+              movingTab.groupingTabsData.newIndex = movingTabNewIndex;
+              movingTab.groupingTabsData.animate = false;
+            };
+
+            movingTab.addEventListener("transitionend", onTransitionEnd);
+
+            // Add animation data for tabs between movingTab (selected
+            // tab moving towards the dragged tab) and draggedTab.
+            // Those tabs in the middle should move in
+            // the opposite direction of movingTab.
+
+            let lowerIndex = Math.min(movingTabOldIndex, draggedTabPos);
+            let higherIndex = Math.max(movingTabOldIndex, draggedTabPos);
+
+            for (let i = lowerIndex + 1; i < higherIndex; i++) {
+              let middleTab = gBrowser.visibleTabs[i];
+
+              if (middleTab.pinned != movingTab.pinned) {
+                // Don't mix pinned and unpinned tabs
+                break;
+              }
+
+              if (middleTab.multiselected) {
+                // Skip because this selected tab should
+                // be shifted towards the dragged Tab.
+                continue;
+              }
+
+              if (!middleTab.groupingTabsData || !middleTab.groupingTabsData.translateX) {
+                middleTab.groupingTabsData = { translateX: 0};
+              }
+              if (side == "left") {
+                middleTab.groupingTabsData.translateX -= movingTabWidth;
+              } else {
+                middleTab.groupingTabsData.translateX += movingTabWidth;
+              }
+
+              middleTab.setAttribute("tab-grouping", "true");
+            }
+          }
+        ]]></body>
+      </method>
+
+      <method name="_finishGroupSelectedTabs">
+        <parameter name="tab"/>
+        <body><![CDATA[
+          if (!tab.groupingTabsData || tab.groupingTabsData.finished)
+            return;
+
+          tab.groupingTabsData.finished = true;
+
+          let selectedTabs = gBrowser.selectedTabs;
+          let tabIndex = selectedTabs.indexOf(tab);
+
+          // Moving left tabs
+          for (let i = tabIndex - 1; i > -1; i--) {
+            let movingTab = selectedTabs[i];
+            if (movingTab.groupingTabsData.newIndex) {
+              gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
+            }
+          }
+
+          // Moving right tabs
+          for (let i = tabIndex + 1; i < selectedTabs.length; i++) {
+            let movingTab = selectedTabs[i];
+            if (movingTab.groupingTabsData.newIndex) {
+              gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
+            }
+          }
+
+          for (let t of this._getVisibleTabs()) {
+            t.style.transform = "";
+            t.removeAttribute("tab-grouping");
+            delete t.groupingTabsData;
+          }
+        ]]></body>
+      </method>
+
+      <method name="_isGroupTabsAnimationOver">
+        <body><![CDATA[
+          for (let tab of gBrowser.selectedTabs) {
+            if (tab.groupingTabsData && tab.groupingTabsData.animate)
+              return false;
+          }
+          return true;
+        ]]></body>
+      </method>
+
       <method name="handleEvent">
         <parameter name="aEvent"/>
         <body><![CDATA[
           switch (aEvent.type) {
             case "resize":
               if (aEvent.target != window)
                 break;
 
@@ -1284,35 +1459,17 @@
         // Set the cursor to an arrow during tab drags.
         dt.mozCursor = "default";
 
         // Set the tab as the source of the drag, which ensures we have a stable
         // node to deliver the `dragend` event.  See bug 1345473.
         dt.addElement(tab);
 
         if (tab.multiselected) {
-          // Regroup all selected tabs around the dragged tab
-          // for multiple tabs dragging
-          let draggedTabPos = tab._tPos;
-
-          // Move left selected tabs
-          let insertAtPos = draggedTabPos - 1;
-          for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
-            let movingTab = selectedTabs[i];
-            gBrowser.moveTabTo(movingTab, insertAtPos);
-            insertAtPos--;
-          }
-
-          // Move right selected tabs
-          insertAtPos = draggedTabPos + 1;
-          for (let i = selectedTabs.indexOf(tab) + 1; i < selectedTabs.length; i++) {
-            let movingTab = selectedTabs[i];
-            gBrowser.moveTabTo(movingTab, insertAtPos);
-            insertAtPos++;
-          }
+          this._groupSelectedTabs(tab);
         }
 
         // Create a canvas to which we capture the current tab.
         // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
         // canvas size (in CSS pixels) to the window's backing resolution in order
         // to get a full-resolution drag image for use on HiDPI displays.
         let windowUtils = window.windowUtils;
         let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
@@ -1419,21 +1576,31 @@
             case "scrollbutton-down":
               pixelsToScroll = arrowScrollbox.scrollIncrement;
               break;
           }
           if (pixelsToScroll)
             arrowScrollbox.scrollByPixels((ltr ? 1 : -1) * pixelsToScroll, true);
         }
 
-        if (effects == "move" &&
-            this == event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0).parentNode) {
+        let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+        if ((effects == "move" || effects == "copy") &&
+            this == draggedTab.parentNode) {
           ind.collapsed = true;
-          this._animateTabMove(event);
-          return;
+
+          if (!this._isGroupTabsAnimationOver()) {
+            // Wait for grouping tabs animation to finish
+            return;
+          }
+          this._finishGroupSelectedTabs(draggedTab);
+
+          if (effects == "move") {
+            this._animateTabMove(event);
+            return;
+          }
         }
 
         this._finishAnimateTabMove();
 
         if (effects == "link") {
           let tab = this._getDragTargetTab(event, true);
           if (tab) {
             if (!this._dragTime)
@@ -1491,16 +1658,17 @@
         var draggedTab;
         let movingTabs;
         if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) { // tab copy or move
           draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
           // not our drop then
           if (!draggedTab)
             return;
           movingTabs = draggedTab._dragData.movingTabs;
+          draggedTab.parentNode._finishGroupSelectedTabs(draggedTab);
         }
 
         this._tabDropIndicator.collapsed = true;
         event.stopPropagation();
         if (draggedTab && dropEffect == "copy") {
           // copy the dropped tab (wherever it's from)
           let newIndex = this._getDropIndex(event, false);
           let draggedTabCopy;
@@ -1631,16 +1799,17 @@
         var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
 
         // Prevent this code from running if a tabdrop animation is
         // running since calling _finishAnimateTabMove would clear
         // any CSS transition that is running.
         if (draggedTab.hasAttribute("tabdrop-samewindow"))
           return;
 
+        this._finishGroupSelectedTabs(draggedTab);
         this._finishAnimateTabMove();
 
         if (dt.mozUserCancelled || dt.dropEffect != "none" || this._isCustomizing) {
           delete draggedTab._dragData;
           return;
         }
 
         // Disable detach within the browser toolbox
--- a/browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js
@@ -1,16 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
+const PREF_ANIMATION = "toolkit.cosmeticAnimations.enabled";
 
 add_task(async function setPref() {
   await SpecialPowers.pushPrefEnv({
-    set: [[PREF_MULTISELECT_TABS, true]],
+    set: [
+      [PREF_MULTISELECT_TABS, true],
+      [PREF_ANIMATION, false],
+    ],
   });
 });
 
 add_task(async function() {
   let tab0 = gBrowser.selectedTab;
   let tab1 = await addTab();
   let tab2 = await addTab();
   let tab3 = await addTab();