Bug 690227 - Back out bug 455694 (tab drag/detach animations), a=LegNeato
authorDão Gottwald <dao@mozilla.com>
Wed, 26 Oct 2011 10:35:13 -0700
changeset 79138 5bf4059fa0e0e2a300f00adf682586891e876577
parent 79137 4da434beb5377fe1a06800ff165cfb965cc04865
child 79139 a1497cd3ec8f368455cdbaa7bd07488954e81938
push id78
push userclegnitto@mozilla.com
push dateFri, 16 Dec 2011 17:32:24 +0000
treeherdermozilla-release@79d24e644fdd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersLegNeato
bugs690227, 455694
milestone9.0a2
Bug 690227 - Back out bug 455694 (tab drag/detach animations), a=LegNeato
browser/base/content/browser-places.js
browser/base/content/browser.css
browser/base/content/tabbrowser.css
browser/base/content/tabbrowser.xml
browser/base/content/utilityOverlay.js
browser/components/places/content/controller.js
browser/themes/gnomestripe/browser/browser.css
browser/themes/pinstripe/browser/browser.css
browser/themes/winstripe/browser/browser-aero.css
browser/themes/winstripe/browser/browser.css
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -850,28 +850,23 @@ var PlacesMenuDNDHandler = {
    * @param   event
    *          The DragEnter event that spawned the opening. 
    */
   onDragEnter: function PMDH_onDragEnter(event) {
     // Opening menus in a Places popup is handled by the view itself.
     if (!this._isStaticContainer(event.target))
       return;
 
-    let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
-                                PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                Ci.nsITreeView.DROP_ON);
-    if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer)) {
-      this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-      this._loadTimer.initWithCallback(function() {
-        PlacesMenuDNDHandler._loadTimer = null;
-        event.target.lastChild.setAttribute("autoopened", "true");
-        event.target.lastChild.showPopup(event.target.lastChild);
-      }, this._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT);
-      event.preventDefault();
-    }
+    this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    this._loadTimer.initWithCallback(function() {
+      PlacesMenuDNDHandler._loadTimer = null;
+      event.target.lastChild.setAttribute("autoopened", "true");
+      event.target.lastChild.showPopup(event.target.lastChild);
+    }, this._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT);
+    event.preventDefault();
     event.stopPropagation();
   },
 
   /**
    * Handles dragexit on the <menu> element.
    * @returns true if the element is a container element (menu or 
    *          menu-toolbarbutton), false otherwise.
    */
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -8,17 +8,16 @@ searchbar {
 tabbrowser {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser");
 }
 
 .tabbrowser-tabs {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabs");
 }
 
-#tabbrowser-tabs[drag=detach][closebuttons=hidden] > .tabbrowser-arrowscrollbox > .tabs-newtab-button,
 #tabbrowser-tabs:not([overflow="true"]) + #new-tab-button,
 #tabbrowser-tabs[overflow="true"] > .tabbrowser-arrowscrollbox > .tabs-newtab-button,
 #TabsToolbar[currentset]:not([currentset*="tabbrowser-tabs,new-tab-button"]) > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .tabs-newtab-button,
 #TabsToolbar[customizing="true"] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .tabs-newtab-button {
   visibility: collapse;
 }
 
 .tabbrowser-tab {
@@ -58,45 +57,16 @@ tabbrowser {
   -moz-transition: opacity 250ms;
 }
 
 .tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] {
   position: fixed !important;
   display: block; /* position:fixed already does this (bug 579776), but let's be explicit */
 }
 
-.tabbrowser-tabs[drag] > .tabbrowser-tab[selected] {
-  z-index: 2; /* ensure selected tab stays on top despite -moz-transform */
-}
-
-.tabbrowser-tabs[drag] > .tabbrowser-tab[dragged] {
-  -moz-transition: 0s; /* suppress opening animation when reattaching tab */
-}
-
-/* visibility: collapse might collapse the tab bar, so we use this instead */
-.tabbrowser-tabs[drag=detach] > .tabbrowser-tab[dragged]:not(:only-child) {
-  min-width: 0 !important;
-  max-width: 0 !important;
-  border: 0 !important;
-  opacity: 0;
-  overflow: hidden;
-  -moz-transition: max-width 150ms ease-out;
-}
-.tabbrowser-tabs[drag=detach] > .tabbrowser-tab[dragged]:only-child {
-  visibility: hidden;
-}
-
-.tabbrowser-tabs[drag=move] > .tabbrowser-tab[fadein]:not([dragged]) {
-  -moz-transition: -moz-transform 200ms ease-out;
-}
-
-.tabbrowser-tabs[drag=finish] > .tabbrowser-tab[dragged][fadein] {
-  -moz-transition: -moz-transform 100ms ease-out;
-}
-
 #alltabs-popup {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-alltabs-popup");
 }
 
 toolbar[printpreview="true"] {
   -moz-binding: url("chrome://global/content/printPreviewBindings.xml#printpreviewtoolbar");
 }
 
--- a/browser/base/content/tabbrowser.css
+++ b/browser/base/content/tabbrowser.css
@@ -27,26 +27,16 @@
 .tab-stack {
   vertical-align: top; /* for pinned tabs */
 }
 
 tabpanels {
   background-color: transparent;
 }
 
-.tab-drag-preview {
-  background: -moz-element(#content) left top;
-  background-clip: content-box;
-  background-size: cover;
-}
-
-.tab-drag-panel[target] > .tab-drag-preview {
-  display: none;
-}
-
 .tab-drop-indicator {
   position: relative;
   z-index: 2;
 }
 
 .tab-throbber:not([busy]),
 .tab-throbber[busy] + .tab-icon-image {
   display: none;
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1494,19 +1494,16 @@
               return;
             }
 
             var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
 
             if (!this._beginRemoveTab(aTab, false, null, true))
               return;
 
-            if (this.tabContainer.draggedTab == aTab)
-              this.tabContainer._endTabDrag();
-
             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 ||
@@ -1698,18 +1695,16 @@
                 this.tabContainer._positionPinnedTabs();
 
               // update tab close buttons state
               this.tabContainer.adjustTabstrip();
 
               setTimeout(function(tabs) {
                 tabs._lastTabClosedByMouse = false;
               }, 0, this.tabContainer);
-
-              this.tabContainer._handleTabDrag(); // Update drag feedback.
             }
 
             // 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
@@ -2453,17 +2448,17 @@
                 onget="return this.mCurrentBrowser.userTypedValue;"
                 onset="return this.mCurrentBrowser.userTypedValue = val;"/>
 
       <method name="createTooltip">
         <parameter name="event"/>
         <body><![CDATA[
           event.stopPropagation();
           var tab = document.tooltipNode;
-          if (tab.localName != "tab" || this.tabContainer.draggedTab) {
+          if (tab.localName != "tab") {
             event.preventDefault();
             return;
           }
           event.target.setAttribute("label", tab.mOverCloseButton ?
                                              tab.getAttribute("closetabtext") :
                                              tab.getAttribute("label"));
         ]]></body>
       </method>
@@ -2678,57 +2673,49 @@
         <body><![CDATA[
           return !tab.pinned && !tab.hidden;
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="underflow" phase="capturing"><![CDATA[
-        if (event.originalTarget != this._scrollbox || event.detail == 0)
+        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.originalTarget != this._scrollbox || event.detail == 0)
+        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"/>
     </resources>
 
-    <!-- The onpopupshowing/hiding handlers on the panel are to circumvent
-         noautohide=true disabling level=top on Linux. See bug 448929. -->
     <content>
       <xul:hbox align="end">
-        <xul:panel class="tab-drag-panel" anonid="tab-drag-panel" hidden="true" level="top"
-                   onpopupshowing="this.setAttribute('noautohide', true);"
-                   onpopuphiding="this.removeAttribute('noautohide');">
-          <xul:label class="tab-drag-label" crop="end"/>
-          <xul:box class="tab-drag-preview"/>
-        </xul:panel>
         <xul:image class="tab-drop-indicator" anonid="tab-drop-indicator" collapsed="true"/>
       </xul:hbox>
       <xul:arrowscrollbox anonid="arrowscrollbox" orient="horizontal" flex="1"
                           style="min-width: 1px;"
 #ifndef XP_MACOSX
                           clicktoscroll="true"
 #endif
                           class="tabbrowser-arrowscrollbox">
@@ -2814,476 +2801,16 @@
         }
       });]]></field>
       <field name="_blockDblClick">false</field>
 
       <field name="_tabDropIndicator">
         document.getAnonymousElementByAttribute(this, "anonid", "tab-drop-indicator");
       </field>
 
-      <method name="_positionDropIndicator">
-        <parameter name="event"/>
-        <parameter name="scrollOnly"/>
-        <body><![CDATA[
-          var effects = event.dataTransfer ? this._setEffectAllowedForDataTransfer(event) : "";
-
-          var ind = this._tabDropIndicator;
-          if (effects == "none") {
-            ind.collapsed = true;
-            return;
-          }
-          event.preventDefault();
-          event.stopPropagation();
-
-          var tabStrip = this.mTabstrip;
-          var ltr = (window.getComputedStyle(this).direction == "ltr");
-
-          // Autoscroll the tab strip if we drag over the scroll
-          // buttons, even if we aren't dragging a tab, but then
-          // return to avoid drawing the drop indicator.
-          var pixelsToScroll = 0;
-          var target = event.originalTarget;
-          if (target.ownerDocument == document &&
-              this.getAttribute("overflow") == "true") {
-            let targetAnonid = target.getAttribute("anonid");
-            switch (targetAnonid) {
-              case "scrollbutton-up":
-                pixelsToScroll = tabStrip.scrollIncrement * -1;
-                break;
-              case "scrollbutton-down":
-                pixelsToScroll = tabStrip.scrollIncrement;
-                break;
-            }
-            if (pixelsToScroll) {
-              if (effects)
-                tabStrip.scrollByPixels((ltr ? 1 : -1) * pixelsToScroll);
-              else
-                tabStrip._startScroll(pixelsToScroll < 0 ? -1 : 1);
-            }
-          }
-
-          if (scrollOnly) {
-            ind.collapsed = true;
-            return;
-          }
-
-          if (effects == "link") {
-            let tab = this._getDragTargetTab(event);
-            if (tab) {
-              if (!this._dragTime)
-                this._dragTime = Date.now();
-              if (Date.now() >= this._dragTime + this._dragOverDelay)
-                this.selectedItem = tab;
-              ind.collapsed = true;
-              return;
-            }
-          }
-
-          var newIndex = this._getDropIndex(event);
-          var scrollRect = tabStrip.scrollClientRect;
-          var rect = this.getBoundingClientRect();
-          var minMargin = scrollRect.left - rect.left;
-          var maxMargin = Math.min(minMargin + scrollRect.width,
-                                   scrollRect.right);
-          if (!ltr)
-            [minMargin, maxMargin] = [this.clientWidth - maxMargin,
-                                      this.clientWidth - minMargin];
-          var newMargin;
-          if (pixelsToScroll) {
-            // If we are scrolling, put the drop indicator at the edge,
-            // so that it doesn't jump while scrolling.
-            newMargin = (pixelsToScroll > 0) ? maxMargin : minMargin;
-          }
-          else {
-            if (newIndex == this.childNodes.length) {
-              let tabRect = this.childNodes[newIndex-1].getBoundingClientRect();
-              if (ltr)
-                newMargin = tabRect.right - rect.left;
-              else
-                newMargin = rect.right - tabRect.left;
-            }
-            else {
-              let tabRect = this.childNodes[newIndex].getBoundingClientRect();
-              if (ltr)
-                newMargin = tabRect.left - rect.left;
-              else
-                newMargin = rect.right - tabRect.right;
-            }
-          }
-
-          ind.collapsed = false;
-
-          newMargin += ind.clientWidth / 2;
-          if (!ltr)
-            newMargin *= -1;
-
-          ind.style.MozTransform = "translate(" + Math.round(newMargin) + "px)";
-          ind.style.MozMarginStart = (-ind.clientWidth) + "px";
-        ]]></body>
-      </method>
-
-      <field name="_tabDragPanel">
-        document.getAnonymousElementByAttribute(this, "anonid", "tab-drag-panel");
-      </field>
-
-      <field name="draggedTab">null</field>
-
-      <method name="_handleTabDrag">
-        <parameter name="event"/>
-        <body><![CDATA[
-          let draggedTab = this.draggedTab;
-          if (!draggedTab)
-            return;
-
-          if (event)
-            draggedTab._dragData._savedEvent = event;
-          else
-            event = draggedTab._dragData._savedEvent;
-
-          if (this._updateTabDetachState(event, draggedTab))
-            return;
-
-          // Keep the dragged tab visually within the region of like tabs.
-          let tabs = this.tabbrowser.visibleTabs;
-          let numPinned = this.tabbrowser._numPinnedTabs;
-          let leftmostTab = draggedTab.pinned ? tabs[0] : tabs[numPinned];
-          let rightmostTab = draggedTab.pinned ? tabs[numPinned-1] : tabs[tabs.length-1];
-          let tabWidth = draggedTab.getBoundingClientRect().width;
-          let ltr = (window.getComputedStyle(this).direction == "ltr");
-          if (!ltr)
-            [leftmostTab, rightmostTab] = [rightmostTab, leftmostTab];
-          let left = leftmostTab.boxObject.screenX;
-          let right = rightmostTab.boxObject.screenX + tabWidth;
-          let transformX = event.screenX - draggedTab._dragData._dragStartX;
-          if (!draggedTab.pinned)
-            transformX += this.mTabstrip.scrollPosition;
-          let tabX = draggedTab.boxObject.screenX + transformX;
-          draggedTab._dragData._dragDistX = transformX;
-          if (tabX < left)
-            transformX += left - tabX;
-          // Prevent unintended overflow, especially in RTL mode.
-          else if (tabX + tabWidth > right)
-            transformX += right - tabX - tabWidth - (ltr ? 0 : 1);
-          draggedTab.style.MozTransform = "translate(" + transformX + "px)";
-
-          let newIndex = this._getDropIndex(event, draggedTab);
-          let tabAtNewIndex = this.childNodes[newIndex > draggedTab._tPos ?
-                                              newIndex-1 : newIndex];
-          this._positionDropIndicator(event, tabAtNewIndex.pinned == draggedTab.pinned);
-
-          if (newIndex == draggedTab._dragData._dropIndex)
-            return;
-          draggedTab._dragData._dropIndex = newIndex;
-
-          if (!ltr)
-            tabWidth *= -1;
-          tabs.forEach(function(tab) {
-            if (tab == draggedTab || tab.pinned != draggedTab.pinned)
-              return;
-            else if (tab._tPos < draggedTab._tPos && tab._tPos >= newIndex)
-              tab.style.MozTransform = "translate(" + tabWidth + "px)";
-            else if (tab._tPos > draggedTab._tPos && tab._tPos < newIndex)
-              tab.style.MozTransform = "translate(" + -tabWidth + "px)";
-            else
-              tab.style.MozTransform = "";
-          });
-        ]]></body>
-      </method>
-
-      <method name="_updateTabDetachState">
-        <parameter name="event"/>
-        <parameter name="draggedTab"/>
-        <body><![CDATA[
-          let data = draggedTab._dragData;
-          if (data._targetWindow.closed) {
-            data._targetWindow = window;
-            window.focus();
-          }
-          else if (data._dropTarget) {
-            data._dropTarget._tabDropIndicator.collapsed = true;
-          }
-          delete data._dropTarget;
-
-          function isEventOutsideWindow(event, win) {
-            return (event.screenX < win.screenX || event.screenX >= win.screenX + win.outerWidth ||
-                    event.screenY < win.screenY || event.screenY >= win.screenY + win.outerHeight);
-          }
-          if (isEventOutsideWindow(event, data._targetWindow) ||
-              Services.ww.activeWindow != data._targetWindow) {
-            // Iterate through browser windows in hopefully front-to-back order.
-            let winEnum = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true);
-            let wins = [];
-            while (winEnum.hasMoreElements())
-              wins.push(winEnum.getNext());
-            // Work around broken z-order enumerator on Linux. See bug 156333.
-            if (!wins.length) {
-              winEnum = Services.wm.getEnumerator("navigator:browser");
-              while (winEnum.hasMoreElements())
-                wins.unshift(winEnum.getNext());
-            }
-            wins.every(function(win) {
-              if (win.closed || win.windowState == STATE_MINIMIZED ||
-                  isEventOutsideWindow(event, win))
-                return true;
-              data._targetWindow = win;
-              win.focus(); // Raise window when cursor moves over it.
-            });
-          }
-
-          let detached = (this.getAttribute("drag") == "detach");
-          let loneTab = (this.childElementCount == 1);
-          let bo = loneTab ? draggedTab.boxObject : this.parentNode.boxObject;
-          // Detach tab if outside window, to the left or right of tab strip, or
-          // at least one tab height above or below it.
-          if (data._targetWindow != window ||
-              event.screenX < bo.screenX || event.screenX >= bo.screenX + bo.width ||
-              event.screenY < bo.screenY - (detached || loneTab ? 0 : 1) * bo.height ||
-              event.screenY >= bo.screenY + (detached || loneTab ? 1 : 2) * bo.height) {
-            if (data._targetWindow != window &&
-                data._targetWindow.windowState != STATE_MINIMIZED) {
-              let that = data._targetWindow.gBrowser.tabContainer;
-              let bo = that.parentNode.boxObject;
-              if (event.screenX >= bo.screenX && event.screenX < bo.screenX + bo.width &&
-                  event.screenY >= bo.screenY && event.screenY < bo.screenY + bo.height) {
-                that._positionDropIndicator(event);
-                data._dropTarget = that;
-              }
-            }
-
-            let dragPanel = this._tabDragPanel;
-            if (data._dropTarget)
-              dragPanel.setAttribute("target", "true");
-            else
-              dragPanel.removeAttribute("target");
-
-            if (!detached) {
-              this.setAttribute("drag", "detach");
-              this._clearDragTransforms();
-              this._tabDropIndicator.collapsed = true;
-              if (draggedTab.style.maxWidth) {
-                data._maxWidth = draggedTab.style.maxWidth;
-                draggedTab.style.maxWidth = "";
-              }
-              delete data._dropIndex;
-              let label = dragPanel.firstChild;
-              let preview = dragPanel.lastChild;
-              label.value = draggedTab.label;
-              label.width = preview.width = Math.min(outerWidth / 2, screen.availWidth / 5);
-              let aspectRatio = this.tabbrowser.clientWidth / this.tabbrowser.clientHeight;
-              preview.height = Math.min(preview.width / aspectRatio, screen.availHeight / 5);
-              dragPanel.hidden = false;
-              dragPanel.openPopupAtScreen(event.screenX, event.screenY, false);
-            }
-            let width = dragPanel.clientWidth;
-            let [left, top] = this._getAdjustedCoords(event.screenX, event.screenY, width,
-                                                      dragPanel.clientHeight, width / 2, 12, true);
-            dragPanel.moveTo(left, top);
-            return true;
-          }
-          if (detached) { // Otherwise, put tab back in the tab strip.
-            this.setAttribute("drag", "move");
-            if (data._maxWidth) {
-              draggedTab.style.setProperty("max-width", data._maxWidth, "important");
-              delete draggedTab._maxWidth;
-            }
-            this.mTabstrip._updateScrollButtonsDisabledState();
-            this._tabDragPanel.hidePopup();
-          }
-        ]]></body>
-      </method>
-
-      <method name="_getAdjustedCoords">
-        <parameter name="aLeft"/>
-        <parameter name="aTop"/>
-        <parameter name="aWidth"/>
-        <parameter name="aHeight"/>
-        <parameter name="aOffsetX"/>
-        <parameter name="aOffsetY"/>
-        <parameter name="isPanel"/>
-        <body><![CDATA[
-          // screen.availTop et al. only check the source window's screen, but
-          // we want to look at the target window's screen.
-          let sX = {}, sY = {}, sWidth = {}, sHeight = {};
-          Cc["@mozilla.org/gfx/screenmanager;1"]
-            .getService(Ci.nsIScreenManager)
-            .screenForRect(aLeft, aTop, 1, 1)
-            .GetAvailRect(sX, sY, sWidth, sHeight);
-          // Window manager repositions panels that are too close to the right
-          // or bottom of a screen, so leave a gutter to avoid that.
-          if (isPanel) {
-            sWidth.value -= 3;
-            sHeight.value -= 3;
-          }
-          // Ensure rect will be entirely onscreen.
-          let width = Math.min(aWidth, sWidth.value);
-          let height = Math.min(aHeight, sHeight.value);
-          let left = Math.min(Math.max(aLeft - aOffsetX, sX.value),
-                              sX.value + sWidth.value - width);
-          let top = Math.min(Math.max(aTop - aOffsetY, sY.value),
-                             sY.value + sHeight.value - height);
-          return [left, top, width, height];
-        ]]></body>
-      </method>
-
-      <method name="_handleTabDrop">
-        <parameter name="event"/>
-        <body><![CDATA[
-          let draggedTab = this.draggedTab;
-          if (this.getAttribute("drag") == "move") {
-            this._slideTab(event, draggedTab);
-            return;
-          }
-
-          do {
-            let that = draggedTab._dragData._dropTarget;
-            let win = draggedTab._dragData._targetWindow;
-            if (!that || win.closed)
-              break;
-            that._tabDropIndicator.collapsed = true;
-            if (win.windowState == STATE_MINIMIZED)
-              break;
-            this._endTabDrag();
-
-            // User dropped the tab onto another window's tab strip, so swap the
-            // tab with a new one we create in that window, and then close it in
-            // this window (making it seem to have moved between windows).
-            let newIndex = that._getDropIndex(event);
-            let newTab = that.tabbrowser.addTab("about:blank");
-            let newBrowser = that.tabbrowser.getBrowserForTab(newTab);
-            newBrowser.stop(); // Stop the about:blank load.
-            newBrowser.docShell; // Make sure it has a docshell.
-            let numPinned = that.tabbrowser._numPinnedTabs;
-            if (newIndex < numPinned || draggedTab.pinned && newIndex == numPinned)
-              that.tabbrowser.pinTab(newTab);
-            that.tabbrowser.moveTabTo(newTab, newIndex);
-            that.tabbrowser.swapBrowsersAndCloseOther(newTab, draggedTab);
-
-            // We need to select the tab after we've done
-            // swapBrowsersAndCloseOther, so that the updateCurrentBrowser
-            // it triggers will correctly update our URL bar.
-            that.selectedItem = newTab;
-            return;
-          } while (false);
-
-          let [left, top, width, height] = this._getAdjustedCoords(
-            event.screenX, event.screenY, outerWidth, outerHeight,
-            this._tabDragPanel.clientWidth / 2, draggedTab._dragData._dragOffsetY);
-          this._endTabDrag();
-
-          if (this.childElementCount == 1) {
-            // Resize _before_ move to ensure the window fits the new screen. If
-            // the window is too large for its screen, the window manager may do
-            // automatic repositioning.
-            window.resizeTo(width, height);
-            window.moveTo(left, top);
-            window.focus();
-            return;
-          }
-
-          draggedTab.collapsed = true;
-          this.tabbrowser.replaceTabWithWindow(draggedTab, { screenX: left,
-                                                             screenY: top,
-#ifndef XP_WIN
-                                                             outerWidth: width,
-                                                             outerHeight: height
-#endif
-                                                             });
-        ]]></body>
-      </method>
-
-      <method name="_slideTab">
-        <parameter name="event"/>
-        <parameter name="draggedTab"/>
-        <body><![CDATA[
-          let oldIndex = draggedTab._tPos;
-          let newIndex = draggedTab._dragData._dropIndex;
-          if (newIndex > oldIndex)
-            newIndex--;
-          this.removeAttribute("drag");
-          this._endTabDrag();
-
-          if (!draggedTab.pinned && newIndex < this.tabbrowser._numPinnedTabs)
-            this.tabbrowser.pinTab(draggedTab);
-          else if (draggedTab.pinned && newIndex >= this.tabbrowser._numPinnedTabs)
-            this.tabbrowser.unpinTab(draggedTab);
-          else if (Services.prefs.getBoolPref("browser.tabs.animate")) {
-            let difference = 0;
-            // Calculate number of visible tabs between start and destination.
-            if (newIndex != oldIndex) {
-              let tabs = this.tabbrowser.visibleTabs;
-              for (let i = 0; i < tabs.length; i++) {
-                let position = tabs[i]._tPos;
-                if (position <= newIndex && position > oldIndex)
-                  difference++;
-                else if (position >= newIndex && position < oldIndex)
-                  difference--;
-              }
-            }
-            let displacement = difference * draggedTab.getBoundingClientRect().width;
-            if (window.getComputedStyle(this).direction == "rtl")
-              displacement *= -1;
-            let destination = "translate(" + displacement + "px)";
-            if (draggedTab.style.MozTransform != destination) {
-              this.setAttribute("drag", "finish");
-              draggedTab.style.MozTransform = destination;
-              draggedTab.addEventListener("transitionend", function finish(event) {
-                if (event.eventPhase != Event.AT_TARGET ||
-                    event.propertyName != "-moz-transform")
-                  return;
-                draggedTab.removeEventListener("transitionend", finish);
-                draggedTab.removeAttribute("dragged");
-                let that = draggedTab.parentNode;
-                that.removeAttribute("drag");
-                that._clearDragTransforms();
-                that.tabbrowser.moveTabTo(draggedTab, newIndex);
-              });
-              return;
-            }
-          }
-          draggedTab.removeAttribute("dragged");
-          this._clearDragTransforms();
-          this.tabbrowser.moveTabTo(draggedTab, newIndex);
-        ]]></body>
-      </method>
-
-      <method name="_endTabDrag">
-        <body><![CDATA[
-          let tab = this.draggedTab;
-          if (!tab)
-            return;
-          this.draggedTab = null;
-          delete tab._dragData;
-
-          document.removeEventListener("mousemove", this);
-          document.removeEventListener("mouseup", this);
-          this.removeEventListener("TabSelect", this);
-          this.mTabstrip.removeEventListener("scroll", this);
-
-          this._tabDragPanel.hidePopup();
-          this._tabDragPanel.hidden = true;
-          this._tabDropIndicator.collapsed = true;
-
-          if (this.hasAttribute("drag")) {
-            if (this.getAttribute("drag") == "detach")
-              this._unlockTabSizing();
-            tab.removeAttribute("dragged");
-            this.removeAttribute("drag");
-            this._clearDragTransforms();
-          }
-        ]]></body>
-      </method>
-
-      <method name="_clearDragTransforms">
-        <body>
-          this.tabbrowser.visibleTabs.forEach(function(visibleTab) {
-            visibleTab.style.MozTransform = "";
-          });
-        </body>
-      </method>
-
       <field name="_dragOverDelay">350</field>
       <field name="_dragTime">0</field>
 
       <field name="_container" readonly="true"><![CDATA[
         this.parentNode && this.parentNode.localName == "toolbar" ? this.parentNode : this;
       ]]></field>
 
       <property name="visible"
@@ -3434,34 +2961,37 @@
               tab.style.setProperty("max-width", tabWidth, "important");
               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;
-            window.addEventListener("mouseout", this);
+            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;
-          window.addEventListener("mouseout", this);
+          this.tabbrowser.addEventListener("mousemove", this, false);
+          window.addEventListener("mouseout", this, false);
         ]]></body>
       </method>
 
       <method name="_unlockTabSizing">
         <body><![CDATA[
-          window.removeEventListener("mouseout", this);
+          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;
@@ -3518,46 +3048,24 @@
                 this.adjustTabstrip();
                 this._fillTrailingGap();
                 this._handleTabSelect();
                 this.mTabstripWidth = width;
               }
               this.tabbrowser.updateWindowResizers();
               break;
             case "mouseout":
-              if (this.draggedTab)
-                break;
-              let bo = this.mTabstrip.boxObject;
-              if (aEvent.screenX >= bo.screenX && aEvent.screenX < bo.screenX + bo.width &&
-                  aEvent.screenY >= bo.screenY && aEvent.screenY < bo.screenY + bo.height)
-                break;
-              let tabContextMenu = document.getElementById("tabContextMenu");
-              if (tabContextMenu.state == "open")
-                tabContextMenu.addEventListener("popuphidden", this);
-              else
-                this._unlockTabSizing();
-              break;
-            case "popuphidden": // Tab context menu was closed.
-              if (aEvent.eventPhase != Event.AT_TARGET)
+              // 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;
-              aEvent.target.removeEventListener("popuphidden", this);
-              this._unlockTabSizing();
-              break;
             case "mousemove":
-              this._handleTabDrag(aEvent);
-              break;
-            case "mouseup":
-              this._handleTabDrop(aEvent);
-              break;
-            case "TabSelect": // Focus was stolen from dragged tab!
-              this._endTabDrag(aEvent);
-              window.focus();
-              break;
-            case "scroll": // Tab strip was scrolled.
-              this._handleTabDrag();
+              if (document.getElementById("tabContextMenu").state != "open")
+                this._unlockTabSizing();
               break;
           }
         ]]></body>
       </method>
 
       <field name="_animateElement">
         this.mTabstrip._scrollButtonDown;
       </field>
@@ -3614,74 +3122,83 @@
               return null;
           }
           return tab;
         ]]></body>
       </method>
 
       <method name="_getDropIndex">
         <parameter name="event"/>
-        <parameter name="draggedTab"/>
         <body><![CDATA[
-          function compare(a, b, lessThan) lessThan ? a < b : a > b;
-          let ltr = (window.getComputedStyle(this).direction == "ltr");
-          let eX = event.screenX;
-          let tabs = this.tabbrowser.visibleTabs;
-
-          if (draggedTab) {
-            let dist = draggedTab._dragData._dragDistX;
-            let tabX = draggedTab.boxObject.screenX + dist;
-            let draggingRight = dist > 0;
-            if (draggingRight)
-              tabX += draggedTab.boxObject.width;
-            // iterate through app tabs first, since their z-index is higher
-            else if (!draggedTab.pinned)
-              for (let i = 0, numPinned = this.tabbrowser._numPinnedTabs; i < numPinned; i++)
-                if (compare(eX, tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2, ltr))
-                  return i;
-
-            let i = tabs.indexOf(draggedTab), tab = draggedTab, next;
-            while (next = ltr ^ draggingRight ? tabs[--i] : tabs[++i]) {
-              let x = next.pinned == draggedTab.pinned ? tabX : eX;
-              let middleOfNextTab = next.boxObject.screenX + next.boxObject.width / 2;
-              if (!compare(x, middleOfNextTab, !draggingRight))
-                break;
-              // ensure an app tab is actually inside the normal tab region
-              if (draggedTab.pinned && !next.pinned &&
-                  x < this.mTabstrip._scrollButtonUp.boxObject.screenX)
-                break;
-              tab = next;
-            }
-            return tab._tPos + (ltr ^ draggingRight ? 0 : 1);
+          var tabs = this.childNodes;
+          var tab = this._getDragTargetTab(event);
+          if (window.getComputedStyle(this, null).direction == "ltr") {
+            for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
+              if (event.screenX < tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2)
+                return i;
+          } else {
+            for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
+              if (event.screenX > tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2)
+                return i;
           }
-
-          let tab = this._getDragTargetTab(event);
-          for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
-            if (compare(eX, tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2, ltr))
-              return tabs[i]._tPos;
-          return this.childElementCount;
+          return tabs.length;
         ]]></body>
       </method>
 
       <method name="_setEffectAllowedForDataTransfer">
         <parameter name="event"/>
         <body><![CDATA[
           var dt = event.dataTransfer;
           // Disallow dropping multiple items
           if (dt.mozItemCount > 1)
             return dt.effectAllowed = "none";
 
+          var types = dt.mozTypesAt(0);
+          var sourceNode = null;
+          // tabs are always added as the first type
+          if (types[0] == TAB_DROP_TYPE) {
+            var sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+            if (sourceNode instanceof XULElement &&
+                sourceNode.localName == "tab" &&
+                (sourceNode.parentNode == this ||
+                 (sourceNode.ownerDocument.defaultView instanceof ChromeWindow &&
+                  sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == "navigator:browser"))) {
+              if (sourceNode.parentNode == this &&
+                  (event.screenX >= sourceNode.boxObject.screenX &&
+                    event.screenX <= (sourceNode.boxObject.screenX +
+                                       sourceNode.boxObject.width))) {
+                return dt.effectAllowed = "none";
+              }
+
+              return dt.effectAllowed = "copyMove";
+            }
+          }
+
           if (browserDragAndDrop.canDropLink(event)) {
             // Here we need to do this manually
             return dt.effectAllowed = dt.dropEffect = "link";
           }
           return dt.effectAllowed = "none";
         ]]></body>
       </method>
 
+      <method name="_continueScroll">
+        <parameter name="event"/>
+        <body><![CDATA[
+          // Workaround for bug 481904: Dragging a tab stops scrolling at
+          // the tab's position when dragging to the first/last tab and back.
+          var t = this.selectedItem;
+          if (event.screenX >= t.boxObject.screenX &&
+              event.screenX <= t.boxObject.screenX + t.boxObject.width &&
+              event.screenY >= t.boxObject.screenY &&
+              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)
             return;
           tab._fullyOpen = true;
 
           this.adjustTabstrip();
@@ -3805,109 +3322,306 @@
             // shortcuts only.
             return;
         }
         event.stopPropagation();
         event.preventDefault();
       ]]></handler>
 
       <handler event="dragstart"><![CDATA[
-        if (this.draggedTab)
-          return;
         var tab = this._getDragTargetTab(event);
-        if (!tab || !tab._fullyOpen || tab.closing)
-          return;
-
-#ifdef XP_MACOSX
-        if (event.altKey) {
-#else
-        if (event.ctrlKey) {
-#endif
-          let dt = event.dataTransfer;
-          let browser = tab.linkedBrowser;
-          dt.setData("text/x-moz-url", browser.currentURI.spec + "\n" + browser.contentTitle);
-          let favicon = document.getAnonymousElementByAttribute(tab, "class", "tab-icon-image");
-          dt.setDragImage(favicon, 16, 16);
+        if (!tab)
           return;
-        }
-
-        this.setAttribute("drag", "move");
-        this.draggedTab = tab;
-        tab.setAttribute("dragged", "true");
-        let data = tab._dragData = {};
-        data._dragStartX = event.screenX;
-        if (!tab.pinned)
-          data._dragStartX += this.mTabstrip.scrollPosition;
-        data._dragDistX = 0;
-        data._dragOffsetY = event.screenY - window.screenY;
-        data._dropIndex = tab._tPos;
-        data._savedEvent = event;
-        data._targetWindow = window;
-
-        document.addEventListener("mousemove", this);
-        document.addEventListener("mouseup", this);
-        this.addEventListener("TabSelect", this);
-        this.mTabstrip.addEventListener("scroll", this);
+
+        let dt = event.dataTransfer;
+        dt.mozSetDataAt(TAB_DROP_TYPE, tab, 0);
+        let uri = this.tabbrowser.getBrowserForTab(tab).currentURI;
+        let spec = uri ? uri.spec : "about:blank";
+
+        // We must not set text/x-moz-url or text/plain data here,
+        // otherwise trying to deatch the tab by dropping it on the desktop
+        // may result in an "internet shortcut"
+        dt.mozSetDataAt("text/x-moz-text-internal", spec, 0);
+
+        // Set the cursor to an arrow during tab drags.
+        dt.mozCursor = "default";
+
+        let canvas = tabPreviews.capture(tab, false);
+        dt.setDragImage(canvas, 0, 0);
+
+        // _dragOffsetX/Y give the coordinates that the mouse should be
+        // positioned relative to the corner of the new window created upon
+        // dragend such that the mouse appears to have the same position
+        // relative to the corner of the dragged tab.
+        function clientX(ele) ele.getBoundingClientRect().left;
+        let tabOffsetX = clientX(tab) -
+                         clientX(this.children[0].pinned ? this.children[0] : this);
+        tab._dragOffsetX = event.screenX - window.screenX - tabOffsetX;
+        tab._dragOffsetY = event.screenY - window.screenY;
 
         event.stopPropagation();
       ]]></handler>
 
-      <handler event="dragover" action="this._positionDropIndicator(event);"/>
-
-      <handler event="drop"><![CDATA[
-        this._tabDropIndicator.collapsed = true;
-
-        let dt = event.dataTransfer;
-        if (dt.dropEffect != "link")
-          return;
-
-        let url = browserDragAndDrop.drop(event, { });
-
-        // Disallow dropping strings that contain spaces (not a valid url
-        // character). Also disallow dropping javascript: or data: urls.
-        if (!url || !url.length || url.indexOf(" ") != -1 ||
-            /^\s*(javascript|data):/.test(url))
+      <handler event="dragover"><![CDATA[
+        var effects = this._setEffectAllowedForDataTransfer(event);
+
+        var ind = this._tabDropIndicator;
+        if (effects == "" || effects == "none") {
+          ind.collapsed = true;
+          this._continueScroll(event);
           return;
-
-        let bgLoad = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
-
-        if (event.shiftKey)
-          bgLoad = !bgLoad;
-
-        let tab = this._getDragTargetTab(event);
-        if (!tab) {
-          // We're adding a new tab.
-          let newIndex = this._getDropIndex(event);
-          let newTab = this.tabbrowser.loadOneTab(getShortcutOrURI(url), {inBackground: bgLoad});
-          this.tabbrowser.moveTabTo(newTab, newIndex);
-        } else {
-          // Load in an existing tab.
-          try {
-            this.tabbrowser.getBrowserForTab(tab).loadURI(getShortcutOrURI(url));
-            if (!bgLoad)
+        }
+        event.preventDefault();
+        event.stopPropagation();
+
+        var tabStrip = this.mTabstrip;
+        var ltr = (window.getComputedStyle(this, null).direction == "ltr");
+
+        // autoscroll the tab strip if we drag over the scroll
+        // buttons, even if we aren't dragging a tab, but then
+        // return to avoid drawing the drop indicator
+        var pixelsToScroll = 0;
+        if (this.getAttribute("overflow") == "true") {
+          var targetAnonid = event.originalTarget.getAttribute("anonid");
+          switch (targetAnonid) {
+            case "scrollbutton-up":
+              pixelsToScroll = tabStrip.scrollIncrement * -1;
+              break;
+            case "scrollbutton-down":
+              pixelsToScroll = tabStrip.scrollIncrement;
+              break;
+          }
+          if (pixelsToScroll)
+            tabStrip.scrollByPixels((ltr ? 1 : -1) * pixelsToScroll);
+        }
+
+        if (effects == "link") {
+          let tab = this._getDragTargetTab(event);
+          if (tab) {
+            if (!this._dragTime)
+              this._dragTime = Date.now();
+            if (Date.now() >= this._dragTime + this._dragOverDelay)
               this.selectedItem = tab;
             ind.collapsed = true;
             return;
-          } catch(ex) {
-            // Just ignore invalid urls.
+          }
+        }
+
+        var newIndex = this._getDropIndex(event);
+        var scrollRect = tabStrip.scrollClientRect;
+        var rect = this.getBoundingClientRect();
+        var minMargin = scrollRect.left - rect.left;
+        var maxMargin = Math.min(minMargin + scrollRect.width,
+                                 scrollRect.right);
+        if (!ltr)
+          [minMargin, maxMargin] = [this.clientWidth - maxMargin,
+                                    this.clientWidth - minMargin];
+        var newMargin;
+        if (pixelsToScroll) {
+          // if we are scrolling, put the drop indicator at the edge
+          // so that it doesn't jump while scrolling
+          newMargin = (pixelsToScroll > 0) ? maxMargin : minMargin;
+        }
+        else {
+          if (newIndex == this.childNodes.length) {
+            let tabRect = this.childNodes[newIndex-1].getBoundingClientRect();
+            if (ltr)
+              newMargin = tabRect.right - rect.left;
+            else
+              newMargin = rect.right - tabRect.left;
+          }
+          else {
+            let tabRect = this.childNodes[newIndex].getBoundingClientRect();
+            if (ltr)
+              newMargin = tabRect.left - rect.left;
+            else
+              newMargin = rect.right - tabRect.right;
           }
         }
+
+        ind.collapsed = false;
+
+        newMargin += ind.clientWidth / 2;
+        if (!ltr)
+          newMargin *= -1;
+
+        ind.style.MozTransform = "translate(" + Math.round(newMargin) + "px)";
+        ind.style.MozMarginStart = (-ind.clientWidth) + "px";
+      ]]></handler>
+
+      <handler event="drop"><![CDATA[
+        var dt = event.dataTransfer;
+        var dropEffect = dt.dropEffect;
+        var draggedTab;
+        if (dropEffect != "link") { // copy or move
+          draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+          // not our drop then
+          if (!draggedTab)
+            return;
+        }
+
+        this._tabDropIndicator.collapsed = true;
+        event.stopPropagation();
+
+        if (draggedTab && (dropEffect == "copy" ||
+            draggedTab.parentNode == this)) {
+          let newIndex = this._getDropIndex(event);
+          if (dropEffect == "copy") {
+            // copy the dropped tab (wherever it's from)
+            let newTab = this.tabbrowser.duplicateTab(draggedTab);
+            this.tabbrowser.moveTabTo(newTab, newIndex);
+            if (draggedTab.parentNode != this || event.shiftKey)
+              this.selectedItem = newTab;
+          } else {
+            // move the dropped tab
+            if (newIndex > draggedTab._tPos)
+              newIndex--;
+
+            if (draggedTab.pinned) {
+              if (newIndex >= this.tabbrowser._numPinnedTabs)
+                this.tabbrowser.unpinTab(draggedTab);
+            } else {
+              if (newIndex <= this.tabbrowser._numPinnedTabs - 1)
+                this.tabbrowser.pinTab(draggedTab);
+            }
+
+            this.tabbrowser.moveTabTo(draggedTab, newIndex);
+          }
+        } else if (draggedTab) {
+          // swap the dropped tab with a new one we create and then close
+          // it in the other window (making it seem to have moved between
+          // windows)
+          let newIndex = this._getDropIndex(event);
+          let newTab = this.tabbrowser.addTab("about:blank");
+          let newBrowser = this.tabbrowser.getBrowserForTab(newTab);
+          // Stop the about:blank load
+          newBrowser.stop();
+          // make sure it has a docshell
+          newBrowser.docShell;
+
+          this.tabbrowser.moveTabTo(newTab, newIndex);
+
+          this.tabbrowser.swapBrowsersAndCloseOther(newTab, draggedTab);
+
+          // We need to select the tab after we've done
+          // swapBrowsersAndCloseOther, so that the updateCurrentBrowser
+          // it triggers will correctly update our URL bar.
+          this.tabbrowser.selectedTab = newTab;
+        } else {
+          let url = browserDragAndDrop.drop(event, { });
+
+          // valid urls don't contain spaces ' '; if we have a space it isn't a valid url.
+          // Also disallow dropping javascript: or data: urls--bail out
+          if (!url || !url.length || url.indexOf(" ", 0) != -1 ||
+              /^\s*(javascript|data):/.test(url))
+            return;
+
+          let bgLoad = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+
+          if (event.shiftKey)
+            bgLoad = !bgLoad;
+
+          let tab = this._getDragTargetTab(event);
+          if (!tab || dropEffect == "copy") {
+            // We're adding a new tab.
+            let newIndex = this._getDropIndex(event);
+            let newTab = this.tabbrowser.loadOneTab(getShortcutOrURI(url), {inBackground: bgLoad});
+            this.tabbrowser.moveTabTo(newTab, newIndex);
+          } else {
+            // Load in an existing tab.
+            try {
+              this.tabbrowser.getBrowserForTab(tab).loadURI(getShortcutOrURI(url));
+              if (!bgLoad)
+                this.selectedItem = tab;
+            } catch(ex) {
+              // Just ignore invalid urls
+            }
+          }
+        }
+
+        // these offsets are only used in dragend, but we need to free them here
+        // as well
+        delete draggedTab._dragOffsetX;
+        delete draggedTab._dragOffsetY;
+      ]]></handler>
+
+      <handler event="dragend"><![CDATA[
+        // Note: while this case is correctly handled here, this event
+        // isn't dispatched when the tab is moved within the tabstrip,
+        // see bug 460801.
+
+        // * mozUserCancelled = the user pressed ESC to cancel the drag
+        var dt = event.dataTransfer;
+        if (dt.mozUserCancelled || dt.dropEffect != "none")
+          return;
+
+        // Disable detach within the browser toolbox
+        var eX = event.screenX;
+        var eY = event.screenY;
+        var wX = window.screenX;
+        // check if the drop point is horizontally within the window
+        if (eX > wX && eX < (wX + window.outerWidth)) {
+          let bo = this.mTabstrip.boxObject;
+          // also avoid detaching if the the tab was dropped too close to
+          // the tabbar (half a tab)
+          let endScreenY = bo.screenY + 1.5 * bo.height;
+          if (eY < endScreenY && eY > window.screenY)
+            return;
+        }
+
+        var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+        // screen.availLeft et. al. only check the screen that this window is on,
+        // but we want to look at the screen the tab is being dropped onto.
+        var sX = {}, sY = {}, sWidth = {}, sHeight = {};
+        Cc["@mozilla.org/gfx/screenmanager;1"]
+          .getService(Ci.nsIScreenManager)
+          .screenForRect(eX, eY, 1, 1)
+          .GetAvailRect(sX, sY, sWidth, sHeight);
+        // ensure new window entirely within screen
+        var winWidth = Math.min(window.outerWidth, sWidth.value);
+        var winHeight = Math.min(window.outerHeight, sHeight.value);
+        var left = Math.min(Math.max(eX - draggedTab._dragOffsetX, sX.value),
+                            sX.value + sWidth.value - winWidth);
+        var top = Math.min(Math.max(eY - draggedTab._dragOffsetY, sY.value),
+                           sY.value + sHeight.value - winHeight);
+
+        delete draggedTab._dragOffsetX;
+        delete draggedTab._dragOffsetY;
+
+        if (this.tabbrowser.tabs.length == 1) {
+          // resize _before_ move to ensure the window fits the new screen.  if
+          // the window is too large for its screen, the window manager may do
+          // automatic repositioning.
+          window.resizeTo(winWidth, winHeight);
+          window.moveTo(left, top);
+          window.focus();
+        } else {
+          this.tabbrowser.replaceTabWithWindow(draggedTab, { screenX: left,
+                                                             screenY: top,
+#ifndef XP_WIN
+                                                             outerWidth: winWidth,
+                                                             outerHeight: winHeight
+#endif
+                                                             });
+        }
+        event.stopPropagation();
       ]]></handler>
 
       <handler event="dragexit"><![CDATA[
         this._dragTime = 0;
 
         // This does not work at all (see bug 458613)
         var target = event.relatedTarget;
         while (target && target != this)
           target = target.parentNode;
         if (target)
           return;
 
         this._tabDropIndicator.collapsed = true;
+        this._continueScroll(event);
         event.stopPropagation();
       ]]></handler>
     </handlers>
   </binding>
 
   <!-- close-tab-button binding
        This binding relies on the structure of the tabbrowser binding.
        Therefore it should only be used as a child of the tab or the tabs
@@ -4024,16 +3738,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>
       <field name="closing">false</field>
     </implementation>
 
     <handlers>
       <handler event="mouseover">
         var anonid = event.originalTarget.getAttribute("anonid");
         if (anonid == "close-button")
           this.mOverCloseButton = true;
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -37,16 +37,18 @@
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
 // Services = object with smart getters for common XPCOM services
 Components.utils.import("resource://gre/modules/Services.jsm");
 
+var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
+
 var gBidiUI = false;
 
 function getBrowserURL()
 {
   return "chrome://browser/content/browser.xul";
 }
 
 function getTopWin(skipPopups) {
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -1398,23 +1398,27 @@ let PlacesControllerDragHelper = {
     // Check every dragged item.
     for (let i = 0; i < dropCount; i++) {
       let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
       if (!flavor)
         return false;
 
       // Urls can be dropped on any insertionpoint.
       // XXXmano: remember that this method is called for each dragover event!
-      // Thus we shouldn't use unwrapNodes here at all if possible. I think it
-      // would be OK to accept bogus data here (this is not in our control and
-      // will just case the actual drop to be a no-op) and only rule out valid
+      // Thus we shouldn't use unwrapNodes here at all if possible.
+      // I think it would be OK to accept bogus data here (e.g. text which was
+      // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and
+      // will just case the actual drop to be a no-op), and only rule out valid
       // expected cases, which are either unsupported flavors, or items which
       // cannot be dropped in the current insertionpoint. The last case will
       // likely force us to use unwrapNodes for the private data types of
       // places.
+      if (flavor == TAB_DROP_TYPE)
+        continue;
+
       let data = dt.mozGetDataAt(flavor, i);
       let dragged;
       try {
         dragged = PlacesUtils.unwrapNodes(data, flavor)[0];
       }
       catch (e) {
         return false;
       }
@@ -1518,18 +1522,31 @@ let PlacesControllerDragHelper = {
     let movedCount = 0;
     for (let i = 0; i < dropCount; ++i) {
       let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
       if (!flavor)
         return false;
 
       let data = dt.mozGetDataAt(flavor, i);
       let unwrapped;
-      // There's only ever one in the D&D case.
-      unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0];
+      if (flavor != TAB_DROP_TYPE) {
+        // There's only ever one in the D&D case.
+        unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0];
+      }
+      else if (data instanceof XULElement && data.localName == "tab" &&
+               data.ownerDocument.defaultView instanceof ChromeWindow) {
+        let uri = data.linkedBrowser.currentURI;
+        let spec = uri ? uri.spec : "about:blank";
+        let title = data.label;
+        unwrapped = { uri: spec,
+                      title: data.label,
+                      type: PlacesUtils.TYPE_X_MOZ_URL};
+      }
+      else
+        throw("bogus data was passed as a tab")
 
       let index = insertionPoint.index;
 
       // Adjust insertion index to prevent reversal of dragged items. When you
       // drag multiple elts upward: need to increment index or each successive
       // elt will be inserted at the same index, each above the previous.
       let dragginUp = insertionPoint.itemId == unwrapped.parent &&
                       index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
@@ -1573,16 +1590,17 @@ let PlacesControllerDragHelper = {
                   PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
                   PlacesUtils.TYPE_X_MOZ_PLACE],
 
   // The order matters.
   GENERIC_VIEW_DROP_TYPES: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
                             PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
                             PlacesUtils.TYPE_X_MOZ_PLACE,
                             PlacesUtils.TYPE_X_MOZ_URL,
+                            TAB_DROP_TYPE,
                             PlacesUtils.TYPE_UNICODE],
 };
 
 
 XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
                                    "@mozilla.org/widget/dragservice;1",
                                    "nsIDragService");
 
--- a/browser/themes/gnomestripe/browser/browser.css
+++ b/browser/themes/gnomestripe/browser/browser.css
@@ -1550,20 +1550,16 @@ richlistitem[type~="action"][actiontype=
   list-style-image: url("moz-icon://stock/gtk-undelete?size=menu");
 }
 
 #context_closeTab {
   list-style-image: url("moz-icon://stock/gtk-close?size=menu");
 }
 
 /* Tab drag and drop */
-.tab-drag-label {
-  padding: 2px;
-}
-
 .tab-drop-indicator {
   list-style-image: url(chrome://browser/skin/tabbrowser/tabDragIndicator.png);
   margin-bottom: -11px;
 }
 
 /* In-tab close button */
 .tab-close-button > .toolbarbutton-icon {
   /* XXX Buttons have padding in widget/ that we don't want here but can't override with good CSS, so we must
--- a/browser/themes/pinstripe/browser/browser.css
+++ b/browser/themes/pinstripe/browser/browser.css
@@ -1557,17 +1557,16 @@ toolbarbutton.chevron > .toolbarbutton-m
                    margin 30ms ease-out 80ms;
 }
 
 .tab-stack {
   /* ensure stable tab height with and without toolbarbuttons on the tab bar */
   height: 26px;
 }
 
-.tab-drag-label,
 .tabbrowser-tab,
 .tabs-newtab-button {
   -moz-appearance: none;
   font: message-box;
   font-weight: bold;
   text-shadow: @loweredShadow@;
   margin: 0;
   padding: 0;
@@ -1836,29 +1835,16 @@ toolbarbutton.chevron > .toolbarbutton-m
 @TABSONBOTTOM_TAB_STACK@ > .tab-content {
   padding-bottom: 2px;
 }
 
 /**
  * Tab Drag and Drop
  */
 
-.tab-drag-label {
-  background: -moz-linear-gradient(#eee, #ccc);
-  padding: 4px 8px;
-  border-radius: 4px;
-  box-shadow: inset 0 1px 0 rgba(255,255,255,.6);
-}
-
-.tab-drag-panel:not([target]) > .tab-drag-label {
-  background: -moz-linear-gradient(#ddd, #bbb);
-  border-bottom: 1px solid #999;
-  border-radius: 3px 3px 0 0;
-}
-
 .tab-drop-indicator {
   list-style-image: url(chrome://browser/skin/tabbrowser/tabDragIndicator.png);
   margin-bottom: -8px;
 }
 
 /**
  * In-tab close button
  */
--- a/browser/themes/winstripe/browser/browser-aero.css
+++ b/browser/themes/winstripe/browser/browser-aero.css
@@ -6,28 +6,26 @@
 %define glassActiveBorderColor rgb(37, 44, 51)
 %define glassInactiveBorderColor rgb(102, 102, 102)
 
 @media not all and (-moz-windows-classic) {
   #main-window[sizemode="normal"] > #titlebar > #titlebar-content > #appmenu-button-container {
     margin-top: 1px;
   }
 
-  .tab-drag-preview::before,
   #appmenu-button {
     border-width: 2px;
     -moz-border-left-colors: @appMenuButtonBorderColor@;
     -moz-border-bottom-colors: @appMenuButtonBorderColor@;
     -moz-border-right-colors: @appMenuButtonBorderColor@;
     margin-bottom: 1px; /* compensate white outer border */
     box-shadow: 0 1px 0 rgba(255,255,255,.25) inset,
                 0 0 2px 1px rgba(255,255,255,.25) inset;
   }
 
-  #main-window[privatebrowsingmode=temporary] .tab-drag-preview::before,
   #main-window[privatebrowsingmode=temporary] #appmenu-button {
     -moz-border-left-colors: rgba(255,255,255,.5) rgba(43,8,65,.9);
     -moz-border-bottom-colors: rgba(255,255,255,.5) rgba(43,8,65,.9);
     -moz-border-right-colors: rgba(255,255,255,.5) rgba(43,8,65,.9);
   }
 
   #appmenu-popup {
     margin-top: -1px;
@@ -52,17 +50,16 @@
   }
 
   .tabbrowser-tab:not(:-moz-lwtheme):hover,
   .tabs-newtab-button:not(:-moz-lwtheme):hover {
     background-image: @toolbarShadowOnTab@, @bgTabTextureHover@,
                       -moz-linear-gradient(@customToolbarColor@, @customToolbarColor@);
   }
 
-  .tab-drag-label,
   .tabbrowser-tab[selected="true"]:not(:-moz-lwtheme) {
     background-image: -moz-linear-gradient(white, @toolbarHighlight@ 50%),
                       -moz-linear-gradient(@customToolbarColor@, @customToolbarColor@);
   }
 
   #main-window[tabsontop=false]:not([disablechrome]) .tabbrowser-tab[selected=true]:not(:-moz-lwtheme) {
     background-image: @toolbarShadowOnTab@,
                       -moz-linear-gradient(white, @toolbarHighlight@ 50%),
@@ -300,41 +297,16 @@
     text-shadow: white -1px -1px .35em, white -1px 1px .35em, white 1px 1px .35em, white 1px -1px .35em;
   }
 
   #tab-view:-moz-lwtheme {
     background-image: url("chrome://browser/skin/tabview/grain.png"),
                       -moz-linear-gradient(rgba(255,255,255,0), #CCD9EA 200px, #C7D5E7);
     background-attachment: fixed;
   }
-
-  .tab-drag-panel {
-    -moz-appearance: -moz-win-borderless-glass;
-  }
-  .tab-drag-label {
-    padding: 4px;
-    background-color: -moz-dialog;
-    border-radius: 3px;
-  }
-  .tab-drag-preview {
-    margin: 15px 7px 7px;
-  }
-  .tab-drag-panel:not([target]) > .tab-drag-preview {
-    display: block;
-  }
-  .tab-drag-preview::before { /* miniature appmenu button */
-    content: "";
-    display: block;
-    margin-top: -15px;
-    -moz-margin-start: -2px;
-    padding: 0;
-    width: 32px;
-    height: 7px;
-    border-radius: 0 0 3px 3px;
-  }
 }
 
 @media not all and (-moz-windows-compositor) {
   #main-window:-moz-system-metric(windows-default-theme) {
     background-color: rgb(185,209,234);
   }
   #main-window:-moz-system-metric(windows-default-theme):-moz-window-inactive {
     background-color: rgb(215,228,242);
--- a/browser/themes/winstripe/browser/browser.css
+++ b/browser/themes/winstripe/browser/browser.css
@@ -171,17 +171,16 @@
 %endif
 
 #appmenu-button:hover:active,
 #appmenu-button[open] {
   border-radius: 0;
 }
 
 %ifdef MOZ_OFFICIAL_BRANDING
-.tab-drag-preview::before,
 #appmenu-button {
   background-image: -moz-linear-gradient(rgb(247,182,82), rgb(215,98,10) 95%);
   border-color: rgba(83,42,6,.9);
   box-shadow: 0 1px 0 rgba(255,255,255,.25) inset,
               0 0 0 1px rgba(255,255,255,.25) inset;
 }
 #appmenu-button:hover:not(:active):not([open]) {
   background-image: -moz-radial-gradient(center bottom, farthest-side, rgba(252,240,89,.5) 10%, rgba(252,240,89,0) 70%),
@@ -195,17 +194,16 @@
 #appmenu-button:hover:active,
 #appmenu-button[open] {
   background-image: -moz-linear-gradient(rgb(246,170,69), rgb(209,74,0) 95%);
   box-shadow: 0 2px 3px rgba(0,0,0,.4) inset,
               0 1px 1px rgba(0,0,0,.2) inset;
 }
 %else
 %if MOZ_UPDATE_CHANNEL == aurora
-.tab-drag-preview::before,
 #appmenu-button {
   background-image: -moz-linear-gradient(hsl(208,99%,37%), hsl(214,90%,23%) 95%);
   border-color: hsla(214,89%,21%,.9);
   box-shadow: 0 1px 0 hsla(205,100%,72%,.2) inset,
               0 0 2px 1px hsla(205,100%,72%,.25) inset;
 }
 #appmenu-button:hover:not(:active):not([open]) {
   background-image: -moz-radial-gradient(center bottom, farthest-side, hsla(202,100%,85%,.5) 10%, hsla(202,100%,85%,0) 70%),
@@ -218,17 +216,16 @@
 }
 #appmenu-button:hover:active,
 #appmenu-button[open] {
   background-image: -moz-linear-gradient(hsl(208,95%,30%), hsl(214,85%,17%) 95%);
   box-shadow: 0 2px 3px rgba(0,0,0,.4) inset,
               0 1px 1px rgba(0,0,0,.2) inset;
 }
 %else
-.tab-drag-preview::before,
 #appmenu-button {
   background-image: -moz-linear-gradient(hsl(211,33%,32%), hsl(209,53%,10%) 95%);
   border-color: hsla(210,59%,13%,.9);
   box-shadow: 0 1px 0 hsla(210,48%,90%,.15) inset,
               0 0 2px 1px hsla(211,65%,85%,.15) inset;
 }
 #appmenu-button:hover:not(:active):not([open]) {
   background-image: -moz-radial-gradient(center bottom, farthest-side, hsla(210,48%,90%,.5) 10%, hsla(210,48%,90%,0) 70%),
@@ -243,17 +240,16 @@
 #appmenu-button[open] {
   background-image: -moz-linear-gradient(hsl(211,33%,26%), hsl(209,53%,6%) 95%);
   box-shadow: 0 2px 3px rgba(0,0,0,.4) inset,
               0 1px 1px rgba(0,0,0,.2) inset;
 }
 %endif
 %endif
 
-#main-window[privatebrowsingmode=temporary] .tab-drag-preview::before,
 #main-window[privatebrowsingmode=temporary] #appmenu-button {
   background-image: -moz-linear-gradient(rgb(153,38,211), rgb(105,19,163) 95%);
   border-color: rgba(43,8,65,.9);
 }
 
 #main-window[privatebrowsingmode=temporary] #appmenu-button:hover:not(:active):not([open]) {
   background-image: -moz-radial-gradient(center bottom, farthest-side, rgba(240,193,255,.5) 10%, rgba(240,193,255,0) 70%),
                     -moz-radial-gradient(center bottom, farthest-side, rgb(192,81,247), rgba(236,172,255,0)),
@@ -1814,35 +1810,17 @@ richlistitem[type~="action"][actiontype=
   -moz-margin-end: 5px;
 }
 
 /* tabbrowser-tab focus ring */
 .tabbrowser-tab:focus > .tab-stack {
   outline: 1px dotted;
 }
 
-/* Tab drag and drop */
-.tab-drag-panel {
-  border: 0 !important;
-}
-
-.tab-drag-label {
-  margin: 0 !important;
-  padding: 5px;
-  border: 1px solid DimGray;
-}
-
-.tab-drag-panel:not([target]) > .tab-drag-label {
-  display: none;
-}
-
-.tab-drag-preview {
-  border: 1px solid rgba(0,0,0,.5);
-}
-
+/* Tab DnD indicator */
 .tab-drop-indicator {
   list-style-image: url(chrome://browser/skin/tabbrowser/tabDragIndicator.png);
   margin-bottom: -11px;
 }
 
 /* Tab close button */
 .tab-close-button {
   -moz-appearance: none;